Private/ConvertFrom-SrsAviFile.ps1

function ConvertFrom-SrsAviFile {
    <#
    .SYNOPSIS
    Parses an AVI SRS file and extracts metadata and track information.

    .DESCRIPTION
    AVI SRS files are RIFF containers with embedded SRSF (File) and SRST (Track)
    chunks that store metadata about the original sample file for reconstruction.

    .PARAMETER FilePath
    Path to the AVI SRS file

    .PARAMETER Data
    Raw bytes of the AVI SRS data (alternative to FilePath)

    .OUTPUTS
    [PSCustomObject] containing FileMetadata, Tracks, RawBytes, and parsed RIFF structure
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [string]$FilePath,

        [Parameter(Mandatory = $true, ParameterSetName = 'Data')]
        [byte[]]$Data
    )

    if ($PSCmdlet.ParameterSetName -eq 'Path') {
        if (-not (Test-Path $FilePath)) {
            throw "SRS file not found: $FilePath"
        }
        $bytes = [System.IO.File]::ReadAllBytes($FilePath)
    }
    else {
        $bytes = $Data
    }

    Write-Verbose "Parsing AVI SRS: $($bytes.Length) bytes"

    $ms = [System.IO.MemoryStream]::new($bytes)
    $reader = [System.IO.BinaryReader]::new($ms)

    try {
        # Read RIFF header
        $riffMagic = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4))
        if ($riffMagic -ne 'RIFF') {
            throw "Invalid RIFF magic: expected 'RIFF', got '$riffMagic'"
        }

        $riffSize = $reader.ReadUInt32()
        $riffType = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4))

        Write-Verbose "RIFF header: size=$riffSize, type=$riffType"

        $fileMetadata = $null
        $tracks = @{}
        $moviPosition = -1
        $moviSize = 0

        # Parse chunks until we find movi LIST
        while ($ms.Position -lt $bytes.Length - 8) {
            $chunkPos = $ms.Position
            $chunkId = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4))
            $chunkSize = $reader.ReadUInt32()

            Write-Verbose "Chunk at $chunkPos : '$chunkId' ($chunkSize bytes)"

            if ($chunkId -eq 'LIST') {
                $listType = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4))
                Write-Verbose " LIST type: $listType"

                if ($listType -eq 'movi') {
                    $moviPosition = $chunkPos
                    $moviSize = $chunkSize

                    # Parse movi contents for SRSF and SRST chunks
                    $moviEnd = [Math]::Min($chunkPos + 8 + $chunkSize, $bytes.Length)

                    while ($ms.Position -lt $moviEnd - 8) {
                        $subPos = $ms.Position
                        $subId = [System.Text.Encoding]::ASCII.GetString($reader.ReadBytes(4))
                        $subSize = $reader.ReadUInt32()

                        Write-Verbose " SubChunk at $subPos : '$subId' ($subSize bytes)"

                        if ($subId -eq 'SRSF') {
                            # Parse File metadata
                            $srsData = $reader.ReadBytes($subSize)
                            $fileMetadata = ConvertFrom-SrsAviFileData -Data $srsData
                        }
                        elseif ($subId -eq 'SRST') {
                            # Parse Track metadata
                            $trackData = $reader.ReadBytes($subSize)
                            $track = ConvertFrom-SrsAviTrackData -Data $trackData
                            $tracks[$track.TrackNumber] = $track
                        }
                        else {
                            # Skip other chunks (original AVI index data, etc.)
                            $bytesToSkip = [Math]::Min($subSize, $moviEnd - $ms.Position)
                            if ($bytesToSkip -gt 0) {
                                $ms.Seek($bytesToSkip, [System.IO.SeekOrigin]::Current) | Out-Null
                            }
                        }

                        # Word align
                        if ($ms.Position % 2 -eq 1 -and $ms.Position -lt $moviEnd) {
                            $ms.Seek(1, [System.IO.SeekOrigin]::Current) | Out-Null
                        }
                    }

                    # Stop after movi - we have all we need
                    break
                }
                else {
                    # Skip other LIST contents
                    $skipSize = $chunkSize - 4
                    if ($skipSize -gt 0) {
                        $ms.Seek($skipSize, [System.IO.SeekOrigin]::Current) | Out-Null
                    }
                }
            }
            else {
                # Skip non-LIST chunks
                $ms.Seek($chunkSize, [System.IO.SeekOrigin]::Current) | Out-Null
            }

            # Word align
            if ($ms.Position % 2 -eq 1) {
                $ms.Seek(1, [System.IO.SeekOrigin]::Current) | Out-Null
            }
        }

        return [PSCustomObject]@{
            FileMetadata  = $fileMetadata
            Tracks        = $tracks
            RawBytes      = $bytes
            MoviPosition  = $moviPosition
            MoviSize      = $moviSize
            DeclaredSize  = $riffSize
            ContainerType = $riffType
        }
    }
    finally {
        $reader.Dispose()
        $ms.Dispose()
    }
}

function ConvertFrom-SrsAviFileData {
    <#
    .SYNOPSIS
    Parses SRSF chunk data containing file metadata.

    .DESCRIPTION
    SRSF structure:
    - 2 bytes: flags
    - 2 bytes: app_name_length
    - Variable: app_name (UTF-8)
    - 2 bytes: file_name_length
    - Variable: file_name (UTF-8)
    - 8 bytes: file_size (UInt64 LE)
    - 4 bytes: crc32 (UInt32 LE)
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [byte[]]$Data
    )

    $ms = [System.IO.MemoryStream]::new($Data)
    $reader = [System.IO.BinaryReader]::new($ms)

    try {
        $flags = $reader.ReadUInt16()
        $appNameLength = $reader.ReadUInt16()
        $appName = if ($appNameLength -gt 0) {
            [System.Text.Encoding]::UTF8.GetString($reader.ReadBytes($appNameLength))
        }
        else { "" }

        $fileNameLength = $reader.ReadUInt16()
        $fileName = if ($fileNameLength -gt 0) {
            [System.Text.Encoding]::UTF8.GetString($reader.ReadBytes($fileNameLength))
        }
        else { "" }

        $fileSize = $reader.ReadUInt64()
        $crc32 = $reader.ReadUInt32()

        return [PSCustomObject]@{
            Flags       = $flags
            Application = $appName
            FileName    = $fileName
            FileSize    = $fileSize
            Crc32       = $crc32
        }
    }
    finally {
        $reader.Dispose()
        $ms.Dispose()
    }
}

function ConvertFrom-SrsAviTrackData {
    <#
    .SYNOPSIS
    Parses SRST chunk data containing track metadata.

    .DESCRIPTION
    SRST structure:
    - 2 bytes: flags (BIG_FILE=0x4, BIG_TRACK_NUMBER=0x8)
    - 2 or 4 bytes: track_number (4 if BIG_TRACK_NUMBER flag set)
    - 4 or 8 bytes: data_length (8 if BIG_FILE flag set)
    - 8 bytes: match_offset (UInt64 LE)
    - 2 bytes: signature_length
    - Variable: signature_bytes
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [byte[]]$Data
    )

    $BIG_FILE = 0x4
    $BIG_TRACK_NUMBER = 0x8

    $ms = [System.IO.MemoryStream]::new($Data)
    $reader = [System.IO.BinaryReader]::new($ms)

    try {
        $flags = $reader.ReadUInt16()

        # Track number: 2 or 4 bytes based on BIG_TRACK_NUMBER flag
        $trackNumber = if ($flags -band $BIG_TRACK_NUMBER) {
            $reader.ReadUInt32()
        }
        else {
            $reader.ReadUInt16()
        }

        # Data length: 4 or 8 bytes based on BIG_FILE flag
        $dataLength = if ($flags -band $BIG_FILE) {
            $reader.ReadUInt64()
        }
        else {
            [UInt64]$reader.ReadUInt32()
        }

        # Match offset: always 8 bytes
        $matchOffset = $reader.ReadUInt64()

        # Signature bytes
        $signatureLength = $reader.ReadUInt16()
        $signatureBytes = if ($signatureLength -gt 0 -and $ms.Position + $signatureLength -le $Data.Length) {
            $reader.ReadBytes($signatureLength)
        }
        else {
            @()
        }

        return [PSCustomObject]@{
            Flags          = $flags
            TrackNumber    = $trackNumber
            DataLength     = $dataLength
            MatchOffset    = $matchOffset
            SignatureBytes = $signatureBytes
        }
    }
    finally {
        $reader.Dispose()
        $ms.Dispose()
    }
}