Public/ConvertFrom-SrsFileMetadata.ps1

function ConvertFrom-SrsFileMetadata {
    <#
    .SYNOPSIS
        Parse EBML SRS file to extract track metadata and match offsets.

    .DESCRIPTION
        Reads SRS file structure to extract:
        - FileData: original file size, CRC32, filename
        - TrackData: match_offset, data_length, signature_bytes for each track

        This metadata is used to know WHERE and HOW MUCH to extract from the main file.

    .PARAMETER SrsFilePath
        Path to the extracted .srs file (EBML format).

    .EXAMPLE
        $metadata = ConvertFrom-SrsFileMetadata -SrsFilePath "sample.srs"
        $metadata.Tracks | ForEach-Object { "Track $($_.TrackNumber): offset $($_.MatchOffset)" }

        Parses the SRS file and displays the match offset for each track.

    .OUTPUTS
        Hashtable with keys: FileData, Tracks (array), SrsSize
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$SrsFilePath
    )

    if (-not (Test-Path $SrsFilePath)) {
        throw "SRS file not found: $SrsFilePath"
    }

    try {
        $fs = [System.IO.File]::OpenRead($SrsFilePath)
        $br = [System.IO.BinaryReader]::new($fs)
        $srsSize = $fs.Length

        $metadata = @{
            FileData = $null
            Tracks = @()
            SrsSize = $srsSize
        }

        # Helper functions to parse EBML variable-length integers and IDs
        function Get-UIntLength {
            param([byte]$LengthDescriptor)
            for ($i = 0; $i -lt 8; $i++) {
                $bit = 0x80 -shr $i
                if (($LengthDescriptor -band $bit) -ne 0) { return $i + 1 }
            }
            return 0
        }

        function Read-EbmlVarInt {
            param([System.IO.BinaryReader]$Reader)
            $firstByte = $Reader.ReadByte()
            $bytes = Get-UIntLength -LengthDescriptor $firstByte
            [uint64]$mask = 0xFF -shr $bytes
            [uint64]$value = ($firstByte -band $mask)
            for ($i = 1; $i -lt $bytes; $i++) { $value = ($value -shl 8) + $Reader.ReadByte() }
            return @{ Value = $value; Bytes = $bytes }
        }

        function Read-EbmlElementId {
            param([System.IO.BinaryReader]$Reader)
            $firstByte = $Reader.ReadByte()
            $len = Get-UIntLength -LengthDescriptor $firstByte
            $id = New-Object byte[] $len
            $id[0] = $firstByte
            for ($i = 1; $i -lt $len; $i++) { $id[$i] = $Reader.ReadByte() }
            return $id
        }

        # EBML IDs from pyrescene
        $EbmlId_Resample = [byte[]]@(0x1F,0x69,0x75,0x76)
        $EbmlId_ResampleFile = [byte[]]@(0x6A,0x75)
        $EbmlId_ResampleTrack = [byte[]]@(0x6B,0x75)

        function Test-BytesEqual {
            param([byte[]]$A,[byte[]]$B)
            if ($A.Length -ne $B.Length) { return $false }
            for ($i=0; $i -lt $A.Length; $i++) { if ($A[$i] -ne $B[$i]) { return $false } }
            return $true
        }

        # Parse EBML: read EBML header, then Segment, then inner ReSample elements
        $null = Read-EbmlElementId -Reader $br
        $ebmlSizeInfo = Read-EbmlVarInt -Reader $br
        $fs.Seek([int64]$ebmlSizeInfo.Value, [System.IO.SeekOrigin]::Current) | Out-Null

        # Segment
        $null = Read-EbmlElementId -Reader $br
        $segSizeInfo = Read-EbmlVarInt -Reader $br
        $segmentStart = $fs.Position
        $segUnknownMax = [uint64]([math]::Pow(2, 7 * $segSizeInfo.Bytes) - 1)
        if ($segSizeInfo.Value -eq $segUnknownMax) { $segmentEnd = $srsSize } else { $segmentEnd = $segmentStart + [int64]$segSizeInfo.Value }

        while ($fs.Position -lt $segmentEnd) {
            $remaining = $segmentEnd - $fs.Position
            if ($remaining -le 0) { break }
            try {
                $idBytes = Read-EbmlElementId -Reader $br
                $sizeInfo = Read-EbmlVarInt -Reader $br
                $dataSize = [int64]$sizeInfo.Value
                if ($dataSize -gt ($segmentEnd - $fs.Position)) { break }

                if (Test-BytesEqual -A $idBytes -B $EbmlId_Resample) {
                    # ReSample container: iterate its children
                    $containerStart = $fs.Position
                    $containerEnd = $containerStart + $dataSize
                    while ($fs.Position -lt $containerEnd) {
                        $cRemain = $containerEnd - $fs.Position
                        if ($cRemain -le 0) { break }
                        try {
                            $cid = Read-EbmlElementId -Reader $br
                            $csizeInfo = Read-EbmlVarInt -Reader $br
                            $cdataSize = [int64]$csizeInfo.Value

                            if (Test-BytesEqual -A $cid -B $EbmlId_ResampleFile) {
                                $data = $br.ReadBytes([int]$cdataSize)
                                $flags = [BitConverter]::ToUInt16($data, 0)
                                $appLen = [BitConverter]::ToUInt16($data, 2)
                                $off = 4
                                $appName = [System.Text.Encoding]::UTF8.GetString($data, $off, $appLen)
                                $off += $appLen
                                $nameLen = [BitConverter]::ToUInt16($data, $off)
                                $off += 2
                                $sampleName = [System.Text.Encoding]::UTF8.GetString($data, $off, $nameLen)
                                $off += $nameLen
                                $originalSize = [BitConverter]::ToUInt64($data, $off)
                                $off += 8
                                $crc32 = [BitConverter]::ToUInt32($data, $off)

                                $metadata.FileData = @{
                                    Flags = $flags
                                    AppName = $appName
                                    SampleName = $sampleName
                                    OriginalSize = $originalSize
                                    CRC32 = $crc32
                                }
                            }
                            elseif (Test-BytesEqual -A $cid -B $EbmlId_ResampleTrack) {
                                $data = $br.ReadBytes([int]$cdataSize)
                                $off = 0
                                $flags = [BitConverter]::ToUInt16($data, $off)
                                $off += 2
                                $bigTrack = ($flags -band 0x8) -ne 0
                                if ($bigTrack) { $trackNumber = [BitConverter]::ToUInt32($data, $off); $off += 4 } else { $trackNumber = [BitConverter]::ToUInt16($data, $off); $off += 2 }
                                $bigFile = ($flags -band 0x4) -ne 0
                                if ($bigFile) { $dataLength = [BitConverter]::ToUInt64($data, $off); $off += 8 } else { $dataLength = [BitConverter]::ToUInt32($data, $off); $off += 4 }
                                $matchOffset = [BitConverter]::ToUInt64($data, $off); $off += 8
                                $sigLen = [BitConverter]::ToUInt16($data, $off); $off += 2
                                $sigBytes = New-Object byte[] $sigLen
                                if ($sigLen -gt 0 -and ($off + $sigLen) -le $data.Length) { [System.Array]::Copy($data, $off, $sigBytes, 0, $sigLen) }
                                $metadata.Tracks += @{
                                    Flags = $flags
                                    TrackNumber = $trackNumber
                                    DataLength = [uint64]$dataLength
                                    MatchOffset = [uint64]$matchOffset
                                    SignatureBytesLength = $sigLen
                                    SignatureBytes = $sigBytes
                                }
                            }
                            else {
                                $fs.Seek($cdataSize, [System.IO.SeekOrigin]::Current) | Out-Null
                            }
                        }
                        catch { break }
                    }
                }
                elseif (Test-BytesEqual -A $idBytes -B $EbmlId_ResampleFile) {
                    $data = $br.ReadBytes([int]$dataSize)
                    $flags = [BitConverter]::ToUInt16($data, 0)
                    $appLen = [BitConverter]::ToUInt16($data, 2)
                    $off = 4
                    $appName = [System.Text.Encoding]::UTF8.GetString($data, $off, $appLen)
                    $off += $appLen
                    $nameLen = [BitConverter]::ToUInt16($data, $off)
                    $off += 2
                    $sampleName = [System.Text.Encoding]::UTF8.GetString($data, $off, $nameLen)
                    $off += $nameLen
                    $originalSize = [BitConverter]::ToUInt64($data, $off)
                    $off += 8
                    $crc32 = [BitConverter]::ToUInt32($data, $off)

                    $metadata.FileData = @{
                        Flags = $flags
                        AppName = $appName
                        SampleName = $sampleName
                        OriginalSize = $originalSize
                        CRC32 = $crc32
                    }
                }
                elseif (Test-BytesEqual -A $idBytes -B $EbmlId_ResampleTrack) {
                    $data = $br.ReadBytes([int]$dataSize)
                    $off = 0
                    $flags = [BitConverter]::ToUInt16($data, $off)
                    $off += 2

                    $bigTrack = ($flags -band 0x8) -ne 0
                    if ($bigTrack) { $trackNumber = [BitConverter]::ToUInt32($data, $off); $off += 4 }
                    else { $trackNumber = [BitConverter]::ToUInt16($data, $off); $off += 2 }

                    $bigFile = ($flags -band 0x4) -ne 0
                    if ($bigFile) { $dataLength = [BitConverter]::ToUInt64($data, $off); $off += 8 }
                    else { $dataLength = [BitConverter]::ToUInt32($data, $off); $off += 4 }

                    $matchOffset = [BitConverter]::ToUInt64($data, $off); $off += 8
                    $sigLen = [BitConverter]::ToUInt16($data, $off); $off += 2
                    $sigBytes = New-Object byte[] $sigLen
                    if ($sigLen -gt 0 -and ($off + $sigLen) -le $data.Length) {
                        [System.Array]::Copy($data, $off, $sigBytes, 0, $sigLen)
                    }

                    $metadata.Tracks += @{
                        Flags = $flags
                        TrackNumber = $trackNumber
                        DataLength = [uint64]$dataLength
                        MatchOffset = [uint64]$matchOffset
                        SignatureBytesLength = $sigLen
                        SignatureBytes = $sigBytes
                    }
                }
                else {
                    $fs.Seek($dataSize, [System.IO.SeekOrigin]::Current) | Out-Null
                }
            }
            catch {
                break
            }
        }

        # Fallback: scan entire file for ReSample container if none parsed
        if (($metadata.Tracks.Count -eq 0) -or (-not $metadata.FileData)) {
            $pos = 0
            while ($pos -lt $srsSize - 2) {
                try {
                    $fs.Seek($pos, [System.IO.SeekOrigin]::Begin) | Out-Null
                    $peek = $br.ReadByte()
                    if ($peek -eq 0xC0) {
                        $sizeInfo = Read-EbmlVarInt -Reader $br
                        $containerStart = $fs.Position
                        $containerEnd = $containerStart + [int64]$sizeInfo.Value
                        while ($fs.Position -lt $containerEnd) {
                            if (($containerEnd - $fs.Position) -le 0) { break }
                            $cid = Read-EbmlElementId -Reader $br
                            $csize = Read-EbmlVarInt -Reader $br
                            $clen = [int64]$csize.Value
                            if ($cid.Length -eq 1 -and $cid[0] -eq 0xC1) {
                                $data = $br.ReadBytes([int]$clen)
                                $flags = [BitConverter]::ToUInt16($data, 0)
                                $appLen = [BitConverter]::ToUInt16($data, 2)
                                $off = 4
                                $appName = [System.Text.Encoding]::UTF8.GetString($data, $off, $appLen)
                                $off += $appLen
                                $nameLen = [BitConverter]::ToUInt16($data, $off)
                                $off += 2
                                $sampleName = [System.Text.Encoding]::UTF8.GetString($data, $off, $nameLen)
                                $off += $nameLen
                                $originalSize = [BitConverter]::ToUInt64($data, $off)
                                $off += 8
                                $crc32 = [BitConverter]::ToUInt32($data, $off)
                                $metadata.FileData = @{
                                    Flags = $flags
                                    AppName = $appName
                                    SampleName = $sampleName
                                    OriginalSize = $originalSize
                                    CRC32 = $crc32
                                }
                            }
                            elseif ($cid.Length -eq 1 -and $cid[0] -eq 0xC2) {
                                $data = $br.ReadBytes([int]$clen)
                                $off = 0
                                $flags = [BitConverter]::ToUInt16($data, $off)
                                $off += 2
                                $bigTrack = ($flags -band 0x8) -ne 0
                                if ($bigTrack) { $trackNumber = [BitConverter]::ToUInt32($data, $off); $off += 4 } else { $trackNumber = [BitConverter]::ToUInt16($data, $off); $off += 2 }
                                $bigFile = ($flags -band 0x4) -ne 0
                                if ($bigFile) { $dataLength = [BitConverter]::ToUInt64($data, $off); $off += 8 } else { $dataLength = [BitConverter]::ToUInt32($data, $off); $off += 4 }
                                $matchOffset = [BitConverter]::ToUInt64($data, $off); $off += 8
                                $sigLen = [BitConverter]::ToUInt16($data, $off); $off += 2
                                $sigBytes = New-Object byte[] $sigLen
                                if ($sigLen -gt 0 -and ($off + $sigLen) -le $data.Length) { [System.Array]::Copy($data, $off, $sigBytes, 0, $sigLen) }
                                $metadata.Tracks += @{
                                    Flags = $flags
                                    TrackNumber = $trackNumber
                                    DataLength = [uint64]$dataLength
                                    MatchOffset = [uint64]$matchOffset
                                    SignatureBytesLength = $sigLen
                                    SignatureBytes = $sigBytes
                                }
                            }
                            else {
                                $fs.Seek($clen, [System.IO.SeekOrigin]::Current) | Out-Null
                            }
                        }
                        if ($metadata.Tracks.Count -gt 0 -or $metadata.FileData) { break }
                    }
                }
                catch {
                    Write-Verbose "Parse error at position $pos - continuing scan"
                }
                $pos += 1
            }
        }

        $br.Dispose()
        $fs.Close()

        Write-Verbose "Parsed SRS: FileData CRC32=0x$('{0:X8}' -f $metadata.FileData.CRC32), Tracks=$($metadata.Tracks.Count)"
        return $metadata
    }
    catch {
        throw "Failed to parse SRS file: $_"
    }
}