snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s

This commit is contained in:
upstream-mirror
2026-09-15 20:06:07 +05:00
commit 8acb1a3a29
394 changed files with 78746 additions and 0 deletions

View File

@@ -0,0 +1,489 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package oggreader implements the Ogg media container reader
package oggreader
import (
"encoding/binary"
"errors"
"fmt"
"io"
"strings"
)
const (
pageHeaderTypeBeginningOfStream = 0x02
pageHeaderSignature = "OggS"
idPageBasePayloadLength = 19
pageHeaderLen = 27
)
var (
errNilStream = errors.New("stream is nil")
errBadIDPageSignature = errors.New("bad header signature")
errBadOpusTagsSignature = errors.New("bad opus tags signature")
errBadIDPageType = errors.New("wrong header, expected beginning of stream")
errBadIDPageLength = errors.New("payload for id page must be 19 bytes")
errBadIDPagePayloadSignature = errors.New("bad payload signature")
errShortPageHeader = errors.New("not enough data for payload header")
errChecksumMismatch = errors.New("expected and actual checksum do not match")
errUnsupportedChannelMappingFamily = errors.New("unsupported channel mapping family")
)
// OggReader is used to read Ogg files and return page payloads.
type OggReader struct {
stream io.Reader
bytesReadSuccesfully int64
checksumTable *[256]uint32
doChecksum bool
}
// OggHeader contains Opus codec metadata parsed from an Opus ID page.
// This header is extracted from an Ogg page payload that starts with the OpusHead
// signature (the first page of an Opus stream in an Ogg container).
//
// Use OggPageHeader.OpusPacketType() to classify a page payload as OpusHead,
// and OggPageHeader.ParseOpusHeader() to parse the OpusHead payload.
//
// https://tools.ietf.org/html/rfc7845.html#section-3
type OggHeader struct {
ChannelMap uint8
Channels uint8
OutputGain uint16
PreSkip uint16
SampleRate uint32
Version uint8
StreamCount uint8
CoupledCount uint8
// ChannelMapping we store it as a string to be comparable (maps/struct equality)
// while still holding raw bytes.
ChannelMapping string
}
// ParseOpusHead parses an Opus head from the page payload.
func ParseOpusHead(payload []byte) (*OggHeader, error) {
if len(payload) < idPageBasePayloadLength {
return nil, errBadIDPageLength
}
header := parseBasicHeaderFields(payload)
if err := parseChannelMapping(header, payload); err != nil {
return nil, err
}
return header, nil
}
// OggPageHeader is the metadata for a Page
// Pages are the fundamental unit of multiplexing in an Ogg stream
//
// https://tools.ietf.org/html/rfc7845.html#section-1
type OggPageHeader struct {
GranulePosition uint64
sig [4]byte
version uint8
headerType uint8
Serial uint32
index uint32
segmentsCount uint8
}
type HeaderType string
const (
headerUnknown HeaderType = ""
HeaderOpusID HeaderType = "OpusHead"
HeaderOpusTags HeaderType = "OpusTags"
)
func opusPayloadSignature(payload []byte) (HeaderType, bool) {
if len(payload) < 8 {
return headerUnknown, false
}
sig := HeaderType(payload[:8])
if sig == HeaderOpusID || sig == HeaderOpusTags {
return sig, true
}
return headerUnknown, false
}
// HeaderType classifies the page.
func (p *OggPageHeader) HeaderType(payload []byte) (HeaderType, bool) {
sig, ok := opusPayloadSignature(payload)
if !ok || (sig == HeaderOpusID && p.headerType != pageHeaderTypeBeginningOfStream) {
return headerUnknown, false
}
return sig, true
}
type Option func(*OggReader) error
// NewWith returns a new Ogg reader and Ogg header
// with an io.Reader input.
//
// Warning: NewWith only parses the first OpusHead (a single logical bitstream/track)
// and returns a single OggHeader. If you need to handle Ogg containers with multiple
// Opus headers/tracks, use NewWithOptions and scan pages (e.g. via ParseNextPage)
// to find and parse each OpusHead.
func NewWith(in io.Reader) (*OggReader, *OggHeader, error) {
return newWith(in /* doChecksum */, true)
}
// NewWithOptions returns a new Ogg reader.
func NewWithOptions(in io.Reader, options ...Option) (*OggReader, error) {
reader := &OggReader{
stream: in,
checksumTable: generateChecksumTable(),
doChecksum: true,
}
for _, option := range options {
if err := option(reader); err != nil {
return nil, err
}
}
return reader, nil
}
// WithDoChecksum is an option to set the doChecksum flag
// Default is true.
func WithDoChecksum(doChecksum bool) Option {
return func(o *OggReader) error {
o.doChecksum = doChecksum
return nil
}
}
func newWith(in io.Reader, doChecksum bool) (*OggReader, *OggHeader, error) {
if in == nil {
return nil, nil, errNilStream
}
reader := &OggReader{
stream: in,
checksumTable: generateChecksumTable(),
doChecksum: doChecksum,
}
header, err := reader.readOpusHeader()
if err != nil {
return nil, nil, err
}
return reader, header, nil
}
func (o *OggReader) readOpusHeader() (*OggHeader, error) {
payload, pageHeader, err := o.ParseNextPage()
if err != nil {
return nil, err
}
if err := validateOpusPageHeader(pageHeader, payload); err != nil {
return nil, err
}
header := parseBasicHeaderFields(payload)
if err := parseChannelMapping(header, payload); err != nil {
return nil, err
}
return header, nil
}
func validateOpusPageHeader(pageHeader *OggPageHeader, payload []byte) error {
if string(pageHeader.sig[:]) != pageHeaderSignature {
return errBadIDPageSignature
}
if pageHeader.headerType != pageHeaderTypeBeginningOfStream {
return errBadIDPageType
}
if len(payload) < idPageBasePayloadLength {
return errBadIDPageLength
}
if sig, ok := opusPayloadSignature(payload); !ok || sig != HeaderOpusID {
return fmt.Errorf("%w: expected OpusHead, got %s", errBadIDPagePayloadSignature, sig)
}
return nil
}
func parseBasicHeaderFields(payload []byte) *OggHeader {
header := &OggHeader{}
header.Version = payload[8]
header.Channels = payload[9]
header.PreSkip = binary.LittleEndian.Uint16(payload[10:12])
header.SampleRate = binary.LittleEndian.Uint32(payload[12:16])
header.OutputGain = binary.LittleEndian.Uint16(payload[16:18])
header.ChannelMap = payload[18]
return header
}
// parseChannelMapping parses channel mapping data based on the channel map family.
// https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1
// family mapping of 2 and 3 are defined in https://datatracker.ietf.org/doc/html/rfc8486
func parseChannelMapping(header *OggHeader, payload []byte) error {
switch header.ChannelMap {
case 0:
return validatePayloadLength(payload, idPageBasePayloadLength)
case 1, 2, 255:
return parseExtendedChannelMapping(header, payload)
case 3:
return fmt.Errorf("%w: ambisonics family type 3 is not supported", errUnsupportedChannelMappingFamily)
default:
return errUnsupportedChannelMappingFamily
}
}
func validatePayloadLength(payload []byte, expectedLen int) error {
if len(payload) != expectedLen {
return errBadIDPageLength
}
return nil
}
func parseExtendedChannelMapping(header *OggHeader, payload []byte) error {
expectedPayloadLen := 21 + int(header.Channels)
if err := validatePayloadLength(payload, expectedPayloadLen); err != nil {
return err
}
header.StreamCount = payload[19]
header.CoupledCount = payload[20]
header.ChannelMapping = string(payload[21:expectedPayloadLen])
return nil
}
// ParseNextPage reads from stream and returns Ogg page payload, header,
// and an error if there is incomplete page data.
func (o *OggReader) ParseNextPage() ([]byte, *OggPageHeader, error) { //nolint:cyclop
header := make([]byte, pageHeaderLen)
n, err := io.ReadFull(o.stream, header)
if err != nil {
return nil, nil, err
} else if n < len(header) {
return nil, nil, errShortPageHeader
}
pageHeader := &OggPageHeader{
sig: [4]byte{header[0], header[1], header[2], header[3]},
}
pageHeader.version = header[4]
pageHeader.headerType = header[5]
pageHeader.GranulePosition = binary.LittleEndian.Uint64(header[6 : 6+8])
pageHeader.Serial = binary.LittleEndian.Uint32(header[14 : 14+4])
pageHeader.index = binary.LittleEndian.Uint32(header[18 : 18+4])
pageHeader.segmentsCount = header[26]
sizeBuffer := make([]byte, pageHeader.segmentsCount)
if _, err = io.ReadFull(o.stream, sizeBuffer); err != nil {
return nil, nil, err
}
payloadSize := 0
for _, s := range sizeBuffer {
payloadSize += int(s)
}
payload := make([]byte, payloadSize)
if _, err = io.ReadFull(o.stream, payload); err != nil {
return nil, nil, err
}
if o.doChecksum {
var checksum uint32
updateChecksum := func(v byte) {
checksum = (checksum << 8) ^ o.checksumTable[byte(checksum>>24)^v]
}
for index := range header {
// Don't include expected checksum in our generation
if index > 21 && index < 26 {
updateChecksum(0)
continue
}
updateChecksum(header[index])
}
for _, s := range sizeBuffer {
updateChecksum(s)
}
for index := range payload {
updateChecksum(payload[index])
}
if binary.LittleEndian.Uint32(header[22:22+4]) != checksum {
return nil, nil, errChecksumMismatch
}
}
o.bytesReadSuccesfully += int64(len(header) + len(sizeBuffer) + len(payload))
return payload, pageHeader, nil
}
// ResetReader resets the internal stream of OggReader. This is useful
// for live streams, where the end of the file might be read without the
// data being finished.
func (o *OggReader) ResetReader(reset func(bytesRead int64) io.Reader) {
o.stream = reset(o.bytesReadSuccesfully)
}
func generateChecksumTable() *[256]uint32 {
var table [256]uint32
const poly = 0x04c11db7
for i := range table {
r := uint32(i) << 24 //nolint:gosec // G115
for range 8 {
if (r & 0x80000000) != 0 {
r = (r << 1) ^ poly
} else {
r <<= 1
}
}
table[i] = (r & 0xffffffff) //nolint:gosec // no out of bounds access here.
}
return &table
}
// OpusTags is the metadata for an OpusTags page.
// https://www.xiph.org/vorbis/doc/v-comment.html
type OpusTags struct {
Vendor string
UserComments []UserComment
}
// UserComment is a key-value pair of a vorbis comment.
type UserComment struct {
Comment string
Value string
}
// ParseOpusTags parses an OpusTags from the page payload.
// https://datatracker.ietf.org/doc/html/rfc7845#section-5.2
func ParseOpusTags(payload []byte) (*OpusTags, error) {
const (
headerMagicLen = 8
u32Size = 4
minHeaderLen = headerMagicLen + u32Size + u32Size
)
if err := validateOpusTagsHeader(payload, minHeaderLen); err != nil {
return nil, err
}
vendor, vendorEnd, err := parseVendorString(payload, headerMagicLen, u32Size, minHeaderLen)
if err != nil {
return nil, err
}
userComments, err := parseUserComments(payload, vendorEnd, u32Size)
if err != nil {
return nil, err
}
return &OpusTags{
Vendor: vendor,
UserComments: userComments,
}, nil
}
func validateOpusTagsHeader(payload []byte, minHeaderLen int) error {
if len(payload) < minHeaderLen {
return fmt.Errorf("%w: payload too short", errBadOpusTagsSignature)
}
got := HeaderType(payload[:8])
if got != HeaderOpusTags {
return fmt.Errorf("%w: expected %q, got %q", errBadOpusTagsSignature, HeaderOpusTags, got)
}
return nil
}
func parseVendorString(payload []byte, headerMagicLen, u32Size, minHeaderLen int) (string, int, error) {
vendorLen32 := binary.LittleEndian.Uint32(payload[headerMagicLen : headerMagicLen+u32Size])
if int(vendorLen32) > len(payload)-minHeaderLen {
return "", 0, fmt.Errorf("%w: payload too short for vendor string", errBadOpusTagsSignature)
}
vendorLen := int(vendorLen32)
vendorStart := headerMagicLen + u32Size
vendorEnd := vendorStart + vendorLen
if vendorEnd+u32Size > len(payload) {
return "", 0, fmt.Errorf("%w: payload too short for vendor+comment count", errBadOpusTagsSignature)
}
vendor := string(payload[vendorStart:vendorEnd])
return vendor, vendorEnd, nil
}
func parseUserComments(payload []byte, vendorEnd, u32Size int) ([]UserComment, error) {
userCommentCount32 := binary.LittleEndian.Uint32(payload[vendorEnd : vendorEnd+u32Size])
if int(userCommentCount32) > (len(payload)-vendorEnd)/u32Size {
return nil, fmt.Errorf("%w: unreasonable comment count", errBadOpusTagsSignature)
}
userCommentCount := int(userCommentCount32)
pos := vendorEnd + u32Size
userComments := make([]UserComment, userCommentCount)
for i := range userComments {
comment, nextPos, err := parseSingleUserComment(payload, pos, u32Size, i)
if err != nil {
return nil, err
}
userComments[i] = comment
pos = nextPos
}
return userComments, nil
}
func parseSingleUserComment(payload []byte, pos, u32Size, index int) (UserComment, int, error) {
if pos+u32Size > len(payload) {
return UserComment{}, 0, fmt.Errorf("%w: payload too short for comment len %d", errBadOpusTagsSignature, index)
}
commentLen32 := binary.LittleEndian.Uint32(payload[pos : pos+u32Size])
pos += u32Size
commentLen := int(commentLen32)
if commentLen < 0 || pos+commentLen > len(payload) {
return UserComment{}, 0, fmt.Errorf("%w: payload too short for comment %d", errBadOpusTagsSignature, index)
}
comment := string(payload[pos : pos+commentLen])
pos += commentLen
parts := strings.SplitN(comment, "=", 2)
if len(parts) != 2 {
return UserComment{}, 0, fmt.Errorf("%w: invalid comment %d", errBadOpusTagsSignature, index)
}
return UserComment{
Comment: parts[0],
Value: parts[1],
}, pos, nil
}

View File

@@ -0,0 +1,922 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package oggreader
import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"io"
"testing"
"github.com/stretchr/testify/assert"
)
// buildOggFile generates a valid oggfile that can
// be used for tests.
func buildOggContainer() []byte {
return []byte{
0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x8e, 0x9b, 0x20, 0xaa, 0x00, 0x00,
0x00, 0x00, 0x61, 0xee, 0x61, 0x17, 0x01, 0x13, 0x4f, 0x70,
0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x02, 0x00, 0x0f,
0x80, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4f, 0x67, 0x67,
0x53, 0x00, 0x00, 0xda, 0x93, 0xc2, 0xd9, 0x00, 0x00, 0x00,
0x00, 0x8e, 0x9b, 0x20, 0xaa, 0x02, 0x00, 0x00, 0x00, 0x49,
0x97, 0x03, 0x37, 0x01, 0x05, 0x98, 0x36, 0xbe, 0x88, 0x9e,
}
}
// buildSurroundOggContainerShort has mapping family 1 but omits the mapping table (invalid length).
func buildSurroundOggContainerShort() []byte {
return []byte{
0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x58, 0x49, 0xac, 0xe2, 0x00, 0x00,
0x00, 0x00, 0xc1, 0xa8, 0x7d, 0x4e, 0x01, 0x13, 0x4f, 0x70,
0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x06, 0x38, 0x01,
0x80, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x01,
}
}
// buildUnknownMappingFamilyContainer creates an ID page with an unrecognized channel mapping family.
func buildUnknownMappingFamilyContainer(mappingFamily, channels uint8) []byte {
payload := []byte{
0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, // "OpusHead"
0x01, // version
channels, // channel count
0x38, 0x01, // preskip (0x0138)
0x80, 0xbb, 0x00, 0x00, // sample rate (48000)
0x00, 0x00, // output gain
mappingFamily,
}
segmentTable := []byte{byte(len(payload))} //nolint:gosec // G115: test-only, sized by construction.
header := []byte{
0x4f, 0x67, 0x67, 0x53, // "OggS"
0x00, // version
0x02, // beginning of stream
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // granule position
0x00, 0x00, 0x00, 0x00, // bitstream serial number
0x00, 0x00, 0x00, 0x00, // page sequence number
0x00, 0x00, 0x00, 0x00, // checksum (ignored with checksum disabled)
0x01, // page segments
}
packet := make([]byte, 0, len(header)+len(segmentTable)+len(payload))
packet = append(packet, header...)
packet = append(packet, segmentTable...)
packet = append(packet, payload...)
return packet
}
// buildChannelMappingFamilyContainer builds an Opus ID page for mapping families that
// follow the Figure 3 layout (families 1, 2, 3, 255).
func buildChannelMappingFamilyContainer(
mappingFamily, channels, streamCount, coupledCount uint8,
mapping []byte,
) []byte {
payload := []byte{
0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, // "OpusHead"
0x01, // version
channels, // channel count
0x38, 0x01, // preskip (0x0138)
0x80, 0xbb, 0x00, 0x00, // sample rate (48000)
0x00, 0x00, // output gain
mappingFamily,
streamCount,
coupledCount,
}
payload = append(payload, mapping...)
segmentTable := []byte{byte(len(payload))} //nolint:gosec // G115: test-only, sized by construction.
header := []byte{
0x4f, 0x67, 0x67, 0x53, // "OggS"
0x00, // version
0x02, // header type (beginning of stream)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // granule position
0x00, 0x00, 0x00, 0x00, // bitstream serial number
0x00, 0x00, 0x00, 0x00, // page sequence number
0x00, 0x00, 0x00, 0x00, // checksum (ignored when checksum disabled)
0x01, // page segments
}
packet := make([]byte, 0, len(header)+len(segmentTable)+len(payload))
packet = append(packet, header...)
packet = append(packet, segmentTable...)
packet = append(packet, payload...)
return packet
}
func TestOggReader_ParseValidHeader(t *testing.T) {
reader, header, err := NewWith(bytes.NewReader(buildOggContainer()))
assert.NoError(t, err)
assert.NotNil(t, reader)
assert.NotNil(t, header)
assert.EqualValues(t, header.ChannelMap, 0)
assert.EqualValues(t, header.Channels, 2)
assert.EqualValues(t, header.OutputGain, 0)
assert.EqualValues(t, header.PreSkip, 0xf00)
assert.EqualValues(t, header.SampleRate, 48000)
assert.EqualValues(t, header.Version, 1)
}
func TestOggReader_ParseNextPage(t *testing.T) {
ogg := bytes.NewReader(buildOggContainer())
reader, _, err := NewWith(ogg)
assert.NoError(t, err)
assert.NotNil(t, reader)
assert.Equal(t, int64(47), reader.bytesReadSuccesfully)
payload, _, err := reader.ParseNextPage()
assert.Equal(t, []byte{0x98, 0x36, 0xbe, 0x88, 0x9e}, payload)
assert.NoError(t, err)
assert.Equal(t, int64(80), reader.bytesReadSuccesfully)
_, _, err = reader.ParseNextPage()
assert.Equal(t, err, io.EOF)
}
func TestOggReader_ParseErrors(t *testing.T) {
t.Run("Assert that Reader isn't nil", func(t *testing.T) {
_, _, err := NewWith(nil)
assert.Equal(t, err, errNilStream)
})
t.Run("Invalid ID Page Header Signature", func(t *testing.T) {
ogg := buildOggContainer()
ogg[0] = 0
_, _, err := newWith(bytes.NewReader(ogg), false)
assert.ErrorIs(t, err, errBadIDPageSignature)
})
t.Run("Invalid ID Page Header Type", func(t *testing.T) {
ogg := buildOggContainer()
ogg[5] = 0
_, _, err := newWith(bytes.NewReader(ogg), false)
assert.ErrorIs(t, err, errBadIDPageType)
})
t.Run("Invalid ID Page Payload Length", func(t *testing.T) {
ogg := buildOggContainer()
ogg[27] = 0
_, _, err := newWith(bytes.NewReader(ogg), false)
assert.ErrorIs(t, err, errBadIDPageLength)
})
t.Run("Invalid ID Page Payload Length", func(t *testing.T) {
ogg := buildOggContainer()
ogg[35] = 0
_, _, err := newWith(bytes.NewReader(ogg), false)
assert.ErrorIs(t, err, errBadIDPagePayloadSignature)
})
t.Run("Invalid Page Checksum", func(t *testing.T) {
ogg := buildOggContainer()
ogg[22] = 0
_, _, err := NewWith(bytes.NewReader(ogg))
assert.ErrorIs(t, err, errChecksumMismatch)
})
t.Run("Invalid Multichannel ID Page Payload Length", func(t *testing.T) {
_, _, err := newWith(bytes.NewReader(buildSurroundOggContainerShort()), false)
assert.ErrorIs(t, err, errBadIDPageLength)
})
t.Run("Unsupported Channel Mapping Family", func(t *testing.T) {
_, _, err := newWith(bytes.NewReader(buildUnknownMappingFamilyContainer(4, 2)), false)
assert.ErrorIs(t, err, errUnsupportedChannelMappingFamily)
})
}
func TestOggReader_ChannelMappingFamily1(t *testing.T) {
type testCase struct {
name string
channels uint8
streams uint8
coupled uint8
channelMap []byte
}
cases := []testCase{
{name: "1-mono", channels: 1, streams: 1, coupled: 0, channelMap: []byte{0}},
{name: "2-stereo", channels: 2, streams: 1, coupled: 1, channelMap: []byte{0, 1}},
{name: "3-linear-surround", channels: 3, streams: 2, coupled: 1, channelMap: []byte{0, 2, 1}},
{name: "4-quad", channels: 4, streams: 2, coupled: 2, channelMap: []byte{0, 1, 2, 3}},
{name: "5-5.0", channels: 5, streams: 3, coupled: 2, channelMap: []byte{0, 1, 2, 3, 4}},
{name: "6-5.1", channels: 6, streams: 4, coupled: 2, channelMap: []byte{0, 4, 1, 2, 3, 5}},
{name: "7-6.1", channels: 7, streams: 4, coupled: 3, channelMap: []byte{0, 1, 2, 3, 4, 5, 6}},
{name: "8-7.1", channels: 8, streams: 5, coupled: 3, channelMap: []byte{0, 1, 2, 3, 4, 5, 6, 7}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
reader, err := NewWithOptions(
bytes.NewReader(buildChannelMappingFamilyContainer(1, tc.channels, tc.streams, tc.coupled, tc.channelMap)),
WithDoChecksum(false),
)
assert.NoError(t, err)
assert.NotNil(t, reader)
payload, pageHeader, err := reader.ParseNextPage()
assert.NoError(t, err)
sig, ok := pageHeader.HeaderType(payload)
assert.True(t, ok)
assert.Equal(t, HeaderOpusID, sig)
header, err := ParseOpusHead(payload)
assert.NoError(t, err)
assert.NotNil(t, header)
assert.EqualValues(t, 1, header.Version)
assert.EqualValues(t, tc.channels, header.Channels)
assert.EqualValues(t, 0x138, header.PreSkip)
assert.EqualValues(t, 48e3, header.SampleRate)
assert.EqualValues(t, 0, header.OutputGain)
assert.EqualValues(t, 1, header.ChannelMap)
assert.EqualValues(t, tc.streams, header.StreamCount)
assert.EqualValues(t, tc.coupled, header.CoupledCount)
assert.Equal(t, string(tc.channelMap), header.ChannelMapping)
})
}
}
func TestOggReader_KnownChannelMappingFamilies(t *testing.T) {
cases := []struct {
name string
mappingFamily uint8
channels uint8
streams uint8
coupled uint8
channelMap []byte
}{
{name: "family-2", mappingFamily: 2, channels: 2, streams: 1, coupled: 1, channelMap: []byte{0, 1}},
{name: "family-255", mappingFamily: 255, channels: 2, streams: 1, coupled: 1, channelMap: []byte{0, 1}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
container := buildChannelMappingFamilyContainer(
tc.mappingFamily, tc.channels, tc.streams, tc.coupled, tc.channelMap,
)
reader, err := NewWithOptions(bytes.NewReader(container), WithDoChecksum(false))
assert.NoError(t, err)
assert.NotNil(t, reader)
payload, pageHeader, err := reader.ParseNextPage()
assert.NoError(t, err)
sig, ok := pageHeader.HeaderType(payload)
assert.True(t, ok)
assert.Equal(t, HeaderOpusID, sig)
header, err := ParseOpusHead(payload)
assert.NoError(t, err)
assert.NotNil(t, header)
assert.EqualValues(t, tc.mappingFamily, header.ChannelMap)
assert.EqualValues(t, tc.channels, header.Channels)
assert.EqualValues(t, 0x138, header.PreSkip)
assert.EqualValues(t, 48e3, header.SampleRate)
assert.EqualValues(t, 0, header.OutputGain)
})
}
}
func TestOggReader_ParseExtraFieldsForNonZeroMappingFamily(t *testing.T) {
cases := []struct {
name string
mappingFamily uint8
channels uint8
streams uint8
coupled uint8
channelMap []byte
}{
{name: "family-1-stereo", mappingFamily: 1, channels: 2, streams: 1, coupled: 1, channelMap: []byte{0, 1}},
{name: "family-1-5.1", mappingFamily: 1, channels: 6, streams: 4, coupled: 2, channelMap: []byte{0, 4, 1, 2, 3, 5}},
{
name: "family-1-7.1",
mappingFamily: 1,
channels: 8,
streams: 5,
coupled: 3,
channelMap: []byte{0, 1, 2, 3, 4, 5, 6, 7},
},
{name: "family-2", mappingFamily: 2, channels: 4, streams: 2, coupled: 2, channelMap: []byte{0, 1, 2, 3}},
{name: "family-255", mappingFamily: 255, channels: 5, streams: 3, coupled: 2, channelMap: []byte{0, 1, 2, 3, 4}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
container := buildChannelMappingFamilyContainer(
tc.mappingFamily, tc.channels, tc.streams, tc.coupled, tc.channelMap,
)
reader, err := NewWithOptions(bytes.NewReader(container), WithDoChecksum(false))
assert.NoError(t, err)
assert.NotNil(t, reader)
payload, pageHeader, err := reader.ParseNextPage()
assert.NoError(t, err)
sig, ok := pageHeader.HeaderType(payload)
assert.True(t, ok)
assert.Equal(t, HeaderOpusID, sig)
header, err := ParseOpusHead(payload)
assert.NoError(t, err)
assert.NotNil(t, header)
assert.EqualValues(t, tc.mappingFamily, header.ChannelMap)
assert.EqualValues(t, tc.channels, header.Channels)
assert.EqualValues(t, tc.streams, header.StreamCount)
assert.EqualValues(t, tc.coupled, header.CoupledCount)
assert.Equal(t, string(tc.channelMap), header.ChannelMapping)
})
}
}
func TestOggReader_NewWithOptions(t *testing.T) {
t.Run("With checksum enabled (default)", func(t *testing.T) {
reader, err := NewWithOptions(bytes.NewReader(buildOggContainer()))
assert.NoError(t, err)
assert.NotNil(t, reader)
assert.True(t, reader.doChecksum)
payload, pageHeader, err := reader.ParseNextPage()
assert.NoError(t, err)
assert.NotNil(t, payload)
assert.NotNil(t, pageHeader)
assert.Equal(t, string(HeaderOpusID), string(payload[:8]))
})
t.Run("With checksum enabled explicitly", func(t *testing.T) {
reader, err := NewWithOptions(bytes.NewReader(buildOggContainer()), WithDoChecksum(true))
assert.NoError(t, err)
assert.NotNil(t, reader)
assert.True(t, reader.doChecksum)
ogg := buildOggContainer()
ogg[22] = 0
reader2, err := NewWithOptions(bytes.NewReader(ogg), WithDoChecksum(true))
assert.NoError(t, err)
assert.NotNil(t, reader2)
_, _, err = reader2.ParseNextPage()
assert.Equal(t, errChecksumMismatch, err)
})
t.Run("With checksum disabled", func(t *testing.T) {
reader, err := NewWithOptions(bytes.NewReader(buildOggContainer()), WithDoChecksum(false))
assert.NoError(t, err)
assert.NotNil(t, reader)
assert.False(t, reader.doChecksum)
ogg := buildOggContainer()
ogg[22] = 0
reader2, err := NewWithOptions(bytes.NewReader(ogg), WithDoChecksum(false))
assert.NoError(t, err)
assert.NotNil(t, reader2)
payload, pageHeader, err := reader2.ParseNextPage()
assert.NoError(t, err)
assert.NotNil(t, payload)
assert.NotNil(t, pageHeader)
})
}
// buildMultiTrackOggContainer generates a minimal two-track Ogg file
// with two Opus ID header pages (one for each track).
func buildMultiTrackOggContainer(
firstSerial, secondSerial uint32,
channels uint8,
sampleRate uint32,
preskip uint16,
version uint8,
channelMap uint8,
outputGain uint16,
) []byte {
firstSerialBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(firstSerialBytes, firstSerial)
secondSerialBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(secondSerialBytes, secondSerial)
preskipBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(preskipBytes, preskip)
sampleRateBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(sampleRateBytes, sampleRate)
outputGainBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(outputGainBytes, outputGain)
firstPageHeader := []byte{
0x4f, 0x67, 0x67, 0x53, // "OggS"
0x00, // version
0x02, // header type (beginning of stream)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // granule position
firstSerialBytes[0], firstSerialBytes[1], firstSerialBytes[2], firstSerialBytes[3], // serial number
0x00, 0x00, 0x00, 0x00, // page sequence number
0xd7, 0xb7, 0x51, 0x4a, // checksum
0x01, // page segments
0x13, // segment size (19 bytes)
}
firstPayload := []byte{
0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, // "OpusHead"
version, // version
channels, // channels
preskipBytes[0], preskipBytes[1], // preskip
sampleRateBytes[0], sampleRateBytes[1], sampleRateBytes[2], sampleRateBytes[3], // sample rate
outputGainBytes[0], outputGainBytes[1], // output gain
channelMap, // channel mapping family
}
// Second track: Opus ID page
// Ogg page header (27 bytes)
secondPageHeader := []byte{
0x4f, 0x67, 0x67, 0x53, // "OggS"
0x00, // version
0x02, // header type (beginning of stream)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // granule position
secondSerialBytes[0], secondSerialBytes[1], secondSerialBytes[2], secondSerialBytes[3], // serial number
0x00, 0x00, 0x00, 0x00, // page sequence number
0xaf, 0xaa, 0x01, 0x8b, // checksum
0x01, // page segments
0x13, // segment size (19 bytes)
}
// Second track: OpusHead payload (19 bytes)
secondPayload := []byte{
0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, // "OpusHead"
version, // version
channels, // channels
preskipBytes[0], preskipBytes[1], // preskip
sampleRateBytes[0], sampleRateBytes[1], sampleRateBytes[2], sampleRateBytes[3], // sample rate
outputGainBytes[0], outputGainBytes[1], // output gain
channelMap, // channel mapping family
}
container := make([]byte, 0, len(firstPageHeader)+len(firstPayload)+len(secondPageHeader)+len(secondPayload))
container = append(container, firstPageHeader...)
container = append(container, firstPayload...)
container = append(container, secondPageHeader...)
container = append(container, secondPayload...)
return container
}
func TestOggReader_MultiTrackFile(t *testing.T) {
firstSerial := uint32(0xd03ed35d)
secondSerial := uint32(0xfa6e13f0)
channels := uint8(1)
sampleRate := uint32(48000)
preskip := uint16(0x0138)
version := uint8(1)
channelMap := uint8(0)
outputGain := uint16(0)
data := buildMultiTrackOggContainer(
firstSerial, secondSerial,
channels, sampleRate, preskip,
version, channelMap, outputGain,
)
reader, err := NewWithOptions(bytes.NewReader(data), WithDoChecksum(false))
assert.NoError(t, err)
assert.NotNil(t, reader)
var headers []*OggHeader
var pageHeaders []*OggPageHeader
for {
payload, pageHeader, err := reader.ParseNextPage()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
assert.NoError(t, err, "Error reading page")
break
}
sig, ok := pageHeader.HeaderType(payload)
assert.True(t, ok)
assert.Equal(t, HeaderOpusID, sig)
header, err2 := ParseOpusHead(payload)
assert.NoError(t, err2)
assert.NotNil(t, header)
headers = append(headers, header)
pageHeaders = append(pageHeaders, pageHeader)
t.Logf("Found header %d: Channels=%d, SampleRate=%d, Serial=%d",
len(headers), header.Channels, header.SampleRate, pageHeader.Serial)
}
assert.Equal(t, 2, len(headers), "Should find exactly 2 headers")
assert.Equal(t, channels, headers[0].Channels, "First track should be mono")
assert.Equal(t, channels, headers[1].Channels, "Second track should be mono")
assert.Equal(t, sampleRate, headers[0].SampleRate, "First track should be 48kHz")
assert.Equal(t, sampleRate, headers[1].SampleRate, "Second track should be 48kHz")
assert.Equal(t, firstSerial, pageHeaders[0].Serial, "First track serial should match")
assert.Equal(t, secondSerial, pageHeaders[1].Serial, "Second track serial should match")
assert.NotEqual(t, pageHeaders[0].Serial, pageHeaders[1].Serial, "Serial numbers should be different")
t.Logf("Multi-track file: found %d headers", len(headers))
}
// buildOpusTagsPayload builds an OpusTags payload.
func buildOpusTagsPayload(vendor string, comments []UserComment) []byte {
payload := []byte("OpusTags")
vendorBytes := []byte(vendor)
vendorLen := make([]byte, 4)
//nolint:gosec // G115: test-only, sized by construction
binary.LittleEndian.PutUint32(vendorLen, uint32(len(vendorBytes)))
payload = append(payload, vendorLen...)
payload = append(payload, vendorBytes...)
commentCount := make([]byte, 4)
//nolint:gosec // G115: test-only, sized by construction
binary.LittleEndian.PutUint32(commentCount, uint32(len(comments)))
payload = append(payload, commentCount...)
for _, c := range comments {
comment := c.Comment + "=" + c.Value
commentBytes := []byte(comment)
commentLen := make([]byte, 4)
//nolint:gosec // G115: test-only, sized by construction
binary.LittleEndian.PutUint32(commentLen, uint32(len(commentBytes)))
payload = append(payload, commentLen...)
payload = append(payload, commentBytes...)
}
return payload
}
// buildOggPage builds a complete Ogg page with header, segment table, and payload.
func buildOggPage(serial uint32, pageIndex uint32, headerType uint8, payload []byte) []byte {
serialBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(serialBytes, serial)
indexBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(indexBytes, pageIndex)
// Build segment table (single segment containing entire payload)
segmentTable := []byte{byte(len(payload))} //nolint:gosec // G115: test-only, sized by construction.
// Build page header (27 bytes)
header := []byte{
0x4f, 0x67, 0x67, 0x53, // "OggS"
0x00, // version
headerType, // header type
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // granule position
serialBytes[0], serialBytes[1], serialBytes[2], serialBytes[3], // serial number
indexBytes[0], indexBytes[1], indexBytes[2], indexBytes[3], // page sequence number
0x00, 0x00, 0x00, 0x00, // checksum (will be zero, checksum disabled in test)
0x01, // page segments count
}
page := make([]byte, 0, len(header)+len(segmentTable)+len(payload))
page = append(page, header...)
page = append(page, segmentTable...)
page = append(page, payload...)
return page
}
// buildOpusHeadPayload builds an OpusHead payload.
func buildOpusHeadPayload(
version, channels uint8,
preskip uint16,
sampleRate uint32,
outputGain uint16,
channelMap uint8,
) []byte {
payload := []byte("OpusHead")
payload = append(payload, version)
payload = append(payload, channels)
preskipBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(preskipBytes, preskip)
payload = append(payload, preskipBytes...)
sampleRateBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(sampleRateBytes, sampleRate)
payload = append(payload, sampleRateBytes...)
outputGainBytes := make([]byte, 2)
binary.LittleEndian.PutUint16(outputGainBytes, outputGain)
payload = append(payload, outputGainBytes...)
payload = append(payload, channelMap)
return payload
}
// buildTwoTrackOggContainer builds a complete two-track Ogg container.
// Track 1: OpusHead (index 0) + OpusTags (index 1).
// Track 2: OpusHead (index 0) + OpusTags (index 1).
func buildTwoTrackOggContainer(
serial1, serial2 uint32,
track1Comments, track2Comments []UserComment,
) []byte {
opusHeadPayload := buildOpusHeadPayload(1, 2, 0x0138, 48000, 0, 0)
vendor := "TestVendor"
track1TagsPayload := buildOpusTagsPayload(vendor, track1Comments)
track2TagsPayload := buildOpusTagsPayload(vendor, track2Comments)
track1OpusHeadPage := buildOggPage(serial1, 0, pageHeaderTypeBeginningOfStream, opusHeadPayload)
track1OpusTagsPage := buildOggPage(serial1, 1, 0, track1TagsPayload)
track2OpusHeadPage := buildOggPage(serial2, 0, pageHeaderTypeBeginningOfStream, opusHeadPayload)
track2OpusTagsPage := buildOggPage(serial2, 1, 0, track2TagsPayload)
totalLen := len(track1OpusHeadPage) + len(track1OpusTagsPage) +
len(track2OpusHeadPage) + len(track2OpusTagsPage)
container := make([]byte, 0, totalLen)
container = append(container, track1OpusHeadPage...)
container = append(container, track1OpusTagsPage...)
container = append(container, track2OpusHeadPage...)
container = append(container, track2OpusTagsPage...)
return container
}
func processPages(reader *OggReader) ([]HeaderType, []*OpusTags, error) {
var headersFound []HeaderType
var opusTagsFound []*OpusTags
for {
payload, pageHeader, err := reader.ParseNextPage()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, nil, err
}
sig, ok := pageHeader.HeaderType(payload)
if !ok {
continue
}
headersFound = append(headersFound, sig)
if sig == HeaderOpusTags {
tags, err := ParseOpusTags(payload)
if err != nil {
return nil, nil, err
}
if tags != nil {
opusTagsFound = append(opusTagsFound, tags)
}
}
}
return headersFound, opusTagsFound, nil
}
func countHeaderTypes(headersFound []HeaderType) (int, int) {
opusIDCount := 0
opusTagsCount := 0
for _, h := range headersFound {
switch h {
case HeaderOpusID:
opusIDCount++
case HeaderOpusTags:
opusTagsCount++
default:
}
}
return opusIDCount, opusTagsCount
}
func userCommentsToMap(comments []UserComment) map[string]string {
out := make(map[string]string, len(comments))
for _, c := range comments {
out[c.Comment] = c.Value
}
return out
}
func TestOggReader_DetectHeadersAndTags(t *testing.T) {
serial1 := uint32(0xd03ed35d)
serial2 := uint32(0xfa6e13f0)
track1Title := hex.EncodeToString([]byte{
0x6e, 0x65, 0x76, 0x65, 0x72, 0x20, 0x67, 0x6f, 0x6e, 0x6e, 0x61, 0x20,
0x67, 0x69, 0x76, 0x65, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x75, 0x70,
})
track1Comments := []UserComment{
{Comment: "title", Value: track1Title},
{Comment: "encoder", Value: "test-encoder-v1.0"},
}
track2Comments := []UserComment{
{Comment: "title", Value: "Noise Track 2"},
{Comment: "encoder", Value: "test-encoder-v1.0"},
}
data := buildTwoTrackOggContainer(serial1, serial2, track1Comments, track2Comments)
reader, err := NewWithOptions(bytes.NewReader(data), WithDoChecksum(false))
assert.NoError(t, err)
assert.NotNil(t, reader)
headersFound, opusTagsFound, err := processPages(reader)
assert.NoError(t, err)
assert.Greater(t, len(headersFound), 0, "Should find at least one header or tag")
opusIDCount, opusTagsCount := countHeaderTypes(headersFound)
assert.Equal(t, 2, opusIDCount, "Should find exactly 2 OpusHead pages")
assert.Equal(t, 2, opusTagsCount, "Should find exactly 2 OpusTags pages")
assert.Equal(t, 2, len(opusTagsFound), "Should parse 2 OpusTags")
assert.Equal(t, "TestVendor", opusTagsFound[0].Vendor)
assert.Equal(t, "TestVendor", opusTagsFound[1].Vendor)
track1 := userCommentsToMap(opusTagsFound[0].UserComments)
track2 := userCommentsToMap(opusTagsFound[1].UserComments)
assert.Equal(t, track1Title, track1["title"])
assert.Equal(t, "test-encoder-v1.0", track1["encoder"])
assert.Equal(t, "Noise Track 2", track2["title"])
assert.Equal(t, "test-encoder-v1.0", track2["encoder"])
}
func TestParseOpusTagsErrors(t *testing.T) {
makeHeader := func(length int) []byte {
payload := make([]byte, length)
copy(payload, []byte(HeaderOpusTags))
return payload
}
tests := []struct {
name string
payload []byte
errMessage string
}{
{
name: "payload too short",
payload: []byte("short"),
errMessage: "payload too short",
},
{
name: "bad signature",
payload: append([]byte("OpusHead"), make([]byte, 8)...), // length 16, wrong magic
errMessage: "expected \"OpusTags\"",
},
{
name: "vendor length longer than payload",
payload: func() []byte {
payload := makeHeader(20)
binary.LittleEndian.PutUint32(payload[8:], 10) // vendor length larger than remaining bytes
return payload
}(),
errMessage: "vendor string",
},
{
name: "unreasonable comment count",
payload: func() []byte {
payload := makeHeader(17) // 8 (magic) + 4 (vendor len) + 1 (vendor) + 4 (comment count)
binary.LittleEndian.PutUint32(payload[8:], 1)
payload[12] = 'v'
binary.LittleEndian.PutUint32(payload[13:], 3) // comment count too large for remaining payload
return payload
}(),
errMessage: "unreasonable comment count",
},
{
name: "payload too short for first comment length",
payload: func() []byte {
payload := makeHeader(16) // exactly header + vendor len + comment count, but no room for comment len
binary.LittleEndian.PutUint32(payload[8:], 0)
binary.LittleEndian.PutUint32(payload[12:], 1)
return payload
}(),
errMessage: "comment len 0",
},
{
name: "payload too short for comment data",
payload: func() []byte {
payload := makeHeader(20) // room for comment len, but not the comment itself
binary.LittleEndian.PutUint32(payload[8:], 0)
binary.LittleEndian.PutUint32(payload[12:], 1)
binary.LittleEndian.PutUint32(payload[16:], 10) // comment claims 10 bytes, none available
return payload
}(),
errMessage: "comment 0",
},
{
name: "invalid comment format",
payload: func() []byte {
comment := []byte("novalue")
payload := makeHeader(20 + len(comment)) // 8 magic + 4 vendor len + 4 comment count + 4 comment len + comment
binary.LittleEndian.PutUint32(payload[8:], 0) // vendor length
binary.LittleEndian.PutUint32(payload[12:], 1) // one comment
binary.LittleEndian.PutUint32(payload[16:], uint32(len(comment))) //nolint:gosec
copy(payload[20:], comment) // missing '=' separator
return payload
}(),
errMessage: "invalid comment 0",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tags, err := ParseOpusTags(tc.payload)
assert.Nil(t, tags)
assert.Error(t, err)
assert.ErrorIs(t, err, errBadOpusTagsSignature)
assert.ErrorContains(t, err, tc.errMessage)
})
}
}
func TestParseVendorStringMissingCommentCount(t *testing.T) {
const (
headerMagicLen = 8
u32Size = 4
)
// Build payload with just enough room for magic, vendor length, and vendor string
// but not enough for the comment count field to trigger the vendor error path.
payload := make([]byte, headerMagicLen+u32Size+1) // 13 bytes total
copy(payload, []byte(HeaderOpusTags))
binary.LittleEndian.PutUint32(payload[headerMagicLen:], 1) // vendor length
payload[headerMagicLen+u32Size] = 'v' // single vendor byte
vendor, end, err := parseVendorString(payload, headerMagicLen, u32Size, headerMagicLen+u32Size)
assert.Empty(t, vendor)
assert.Zero(t, end)
assert.ErrorIs(t, err, errBadOpusTagsSignature)
assert.ErrorContains(t, err, "vendor+comment count")
}
func TestParseOpusHead_EmptyPayload_NoPanic(t *testing.T) {
_, err := ParseOpusHead([]byte{})
assert.Error(t, err)
}
func TestParseOpusHead_ChannelMappingSliceOverflow_NoPanic(t *testing.T) {
const channels uint8 = 235
payload := makeOpusHeadWithChannelMapping(channels, 1)
h, err := ParseOpusHead(payload)
assert.NoError(t, err)
assert.Equal(t, len(h.ChannelMapping), int(channels))
}
func makeOpusHeadWithChannelMapping(channels uint8, mappingFamily uint8) []byte {
baseLen := 19
totalLen := baseLen
if mappingFamily != 0 {
totalLen = 21 + int(channels)
}
pack := make([]byte, totalLen)
copy(pack[0:8], []byte("OpusHead"))
pack[8] = 1
pack[9] = channels
binary.LittleEndian.PutUint16(pack[10:12], 0)
binary.LittleEndian.PutUint32(pack[12:16], 48000)
binary.LittleEndian.PutUint16(pack[16:18], 0)
pack[18] = mappingFamily
if mappingFamily != 0 {
pack[19] = channels
pack[20] = 0
for i := 0; i < int(channels); i++ {
pack[21+i] = uint8(i) //nolint:gosec // G115: test-only, uint8(i) is in range
}
}
return pack
}