test(20-02): add OGG test fixture builder and CRC32 validation
- createTestOGG builds minimal valid OGG Vorbis file programmatically - TestOggCRC_KnownVectors validates CRC32 against known vectors and fixture pages - TestCreateTestOGG_Valid verifies fixture structure and metadata.ExtractTags compatibility
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
package tagwriter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"yellowjacket/backend/metadata"
|
||||
)
|
||||
|
||||
// createTestOGG writes a minimal valid OGG Vorbis file to path.
|
||||
// The file contains:
|
||||
// - Page 0 (bos): Vorbis identification header packet.
|
||||
// - Page 1: Vorbis comment header packet (empty) + setup header packet (synthetic).
|
||||
// - Page 2 (eos): Synthetic audio data page.
|
||||
//
|
||||
// The file is valid enough for writeOggTags and metadata.ExtractTags.
|
||||
func createTestOGG(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
|
||||
const serialNo uint32 = 0x12345678
|
||||
|
||||
// --- Page 0: Identification header (bos) ---
|
||||
identPacket := buildVorbisIdentPacket()
|
||||
identSegs := splitPacketIntoSegments(identPacket)
|
||||
identPage := oggPage{
|
||||
headerType: 0x02, // bos
|
||||
granulePos: 0,
|
||||
serialNo: serialNo,
|
||||
seqNo: 0,
|
||||
segmentTable: identSegs,
|
||||
data: identPacket,
|
||||
}
|
||||
|
||||
// --- Page 1: Comment + Setup headers ---
|
||||
commentPacket := buildEmptyVorbisCommentPacket()
|
||||
setupPacket := buildSyntheticSetupPacket()
|
||||
|
||||
headerPages := buildHeaderPages(commentPacket, setupPacket, serialNo)
|
||||
|
||||
// Assign sequence numbers starting from 1.
|
||||
for i := range headerPages {
|
||||
headerPages[i].seqNo = uint32(i + 1)
|
||||
}
|
||||
|
||||
// --- Page 2+N: Audio page (eos) ---
|
||||
audioData := make([]byte, 64) // Synthetic audio (never decoded).
|
||||
for i := range audioData {
|
||||
audioData[i] = byte(i)
|
||||
}
|
||||
|
||||
audioSegs := splitPacketIntoSegments(audioData)
|
||||
audioPage := oggPage{
|
||||
headerType: 0x04, // eos
|
||||
granulePos: 4096,
|
||||
serialNo: serialNo,
|
||||
seqNo: uint32(len(headerPages) + 1),
|
||||
segmentTable: audioSegs,
|
||||
data: audioData,
|
||||
}
|
||||
|
||||
// Write all pages.
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err := writeOggPage(&buf, identPage); err != nil {
|
||||
t.Fatalf("write ident page: %v", err)
|
||||
}
|
||||
|
||||
for _, hp := range headerPages {
|
||||
if err := writeOggPage(&buf, hp); err != nil {
|
||||
t.Fatalf("write header page: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeOggPage(&buf, audioPage); err != nil {
|
||||
t.Fatalf("write audio page: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
|
||||
t.Fatalf("write test OGG: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// buildVorbisIdentPacket builds a minimal Vorbis identification header packet.
|
||||
// Layout: type(1) + "vorbis"(6) + version(4) + channels(1) + sample_rate(4)
|
||||
//
|
||||
// - bitrate_max(4) + bitrate_nom(4) + bitrate_min(4) + blocksize(1) + framing(1)
|
||||
//
|
||||
// = 30 bytes total.
|
||||
func buildVorbisIdentPacket() []byte {
|
||||
buf := make([]byte, 30)
|
||||
|
||||
buf[0] = 0x01 // packet type: identification
|
||||
copy(buf[1:7], "vorbis")
|
||||
|
||||
// Version 0.
|
||||
binary.LittleEndian.PutUint32(buf[7:11], 0)
|
||||
|
||||
// 1 channel.
|
||||
buf[11] = 1
|
||||
|
||||
// Sample rate 44100.
|
||||
binary.LittleEndian.PutUint32(buf[12:16], 44100)
|
||||
|
||||
// Bitrate max/nominal/min = 0 (unset).
|
||||
binary.LittleEndian.PutUint32(buf[16:20], 0)
|
||||
binary.LittleEndian.PutUint32(buf[20:24], 0)
|
||||
binary.LittleEndian.PutUint32(buf[24:28], 0)
|
||||
|
||||
// Blocksize: upper nibble = blocksize1 (log2), lower = blocksize0 (log2).
|
||||
// Using 8 and 6 → 0x86 (256 and 64 samples).
|
||||
buf[28] = 0x86
|
||||
|
||||
// Framing bit.
|
||||
buf[29] = 0x01
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// buildEmptyVorbisCommentPacket builds a Vorbis Comment packet with
|
||||
// an empty vendor string and zero comments.
|
||||
func buildEmptyVorbisCommentPacket() []byte {
|
||||
vendor := []byte("YellowJacket test")
|
||||
vc := &oggVorbisComment{
|
||||
vendor: vendor,
|
||||
entries: nil,
|
||||
}
|
||||
|
||||
return serializeVorbisCommentPacket(vc)
|
||||
}
|
||||
|
||||
// buildSyntheticSetupPacket builds a synthetic Vorbis setup header packet.
|
||||
// It has the correct 7-byte magic (\x05vorbis) followed by synthetic data.
|
||||
// This data is never decoded by the audio codec — only the header magic
|
||||
// is validated during tag writing.
|
||||
func buildSyntheticSetupPacket() []byte {
|
||||
// Vorbis setup header magic + padding.
|
||||
const setupSize = 64
|
||||
|
||||
buf := make([]byte, setupSize)
|
||||
buf[0] = 0x05 // packet type: setup
|
||||
copy(buf[1:7], "vorbis")
|
||||
|
||||
// Fill the rest with deterministic data.
|
||||
for i := 7; i < setupSize; i++ {
|
||||
buf[i] = byte(i * 3)
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CRC32 and fixture validation tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestOggCRC_KnownVectors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Vector 1: Empty input → CRC = 0.
|
||||
got := oggCRC(nil)
|
||||
if got != 0 {
|
||||
t.Errorf("empty input: got 0x%08x, want 0x00000000", got)
|
||||
}
|
||||
|
||||
// Vector 2: "OggS" → known value computed by libogg reference.
|
||||
gotOggS := oggCRC([]byte("OggS"))
|
||||
if gotOggS != 0x5fb0a94f {
|
||||
t.Errorf("OggS: got 0x%08x, want 0x5fb0a94f", gotOggS)
|
||||
}
|
||||
|
||||
// Vector 3: Bytes {1..8} → known value.
|
||||
gotSeq := oggCRC([]byte{1, 2, 3, 4, 5, 6, 7, 8})
|
||||
if gotSeq != 0x7d0f3681 {
|
||||
t.Errorf("{1..8}: got 0x%08x, want 0x7d0f3681", gotSeq)
|
||||
}
|
||||
|
||||
// Vector 4: Self-consistency — compute CRC of test fixture pages.
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "crc_test.ogg")
|
||||
createTestOGG(t, path)
|
||||
|
||||
pages, err := parseOggPages(testLogger(), path)
|
||||
if err != nil {
|
||||
t.Fatalf("parseOggPages: %v", err)
|
||||
}
|
||||
|
||||
for i, page := range pages {
|
||||
// Serialize page, zero CRC field, recompute.
|
||||
raw := pageBytes(page)
|
||||
if len(raw) < 26 {
|
||||
t.Fatalf("page %d: too short (%d bytes)", i, len(raw))
|
||||
}
|
||||
|
||||
// Extract stored CRC.
|
||||
storedCRC := binary.LittleEndian.Uint32(raw[22:26])
|
||||
|
||||
// Zero CRC field and recompute.
|
||||
raw[22] = 0
|
||||
raw[23] = 0
|
||||
raw[24] = 0
|
||||
raw[25] = 0
|
||||
|
||||
computedCRC := oggCRC(raw)
|
||||
if computedCRC != storedCRC {
|
||||
t.Errorf("page %d: CRC mismatch: stored=0x%08x computed=0x%08x",
|
||||
i, storedCRC, computedCRC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTestOGG_Valid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "valid.ogg")
|
||||
createTestOGG(t, path)
|
||||
|
||||
pages, err := parseOggPages(testLogger(), path)
|
||||
if err != nil {
|
||||
t.Fatalf("parseOggPages: %v", err)
|
||||
}
|
||||
|
||||
if len(pages) < 3 {
|
||||
t.Fatalf("expected at least 3 pages, got %d", len(pages))
|
||||
}
|
||||
|
||||
// All pages should have the same serial number.
|
||||
serialNo := pages[0].serialNo
|
||||
for i, p := range pages {
|
||||
if p.serialNo != serialNo {
|
||||
t.Errorf("page %d: serial 0x%08x, want 0x%08x", i, p.serialNo, serialNo)
|
||||
}
|
||||
}
|
||||
|
||||
// First page should be bos.
|
||||
if pages[0].headerType&0x02 == 0 {
|
||||
t.Error("first page is not bos")
|
||||
}
|
||||
|
||||
// First page should have Vorbis identification header.
|
||||
data := pages[0].data
|
||||
if len(data) < 7 || data[0] != 0x01 || string(data[1:7]) != "vorbis" {
|
||||
t.Error("first page does not contain Vorbis identification header")
|
||||
}
|
||||
|
||||
// Verify metadata.ExtractTags can read the fixture (even with no tags).
|
||||
_, err = metadata.ExtractTags(path)
|
||||
if err != nil {
|
||||
t.Fatalf("metadata.ExtractTags on empty fixture: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user