diff --git a/go_backend/ac4_config.go b/go_backend/ac4_config.go index efeeea0e..cdf82a3e 100644 --- a/go_backend/ac4_config.go +++ b/go_backend/ac4_config.go @@ -87,22 +87,26 @@ func findBoxBySignature(data []byte, start, end int64, typ string) (mp4Box, bool } // audioSampleEntryHeaderLen returns the byte length of the fixed audio sample -// entry header (from the box body start) before child boxes begin. -func audioSampleEntryHeaderLen(data []byte, entry mp4Box) int64 { +// entry header (from the box body start) before child boxes begin. ok is false +// for malformed/truncated entries whose declared header is not fully present. +func audioSampleEntryHeaderLen(data []byte, entry mp4Box) (hdrLen int64, ok bool) { // 6 bytes reserved + 2 bytes data_reference_index, then the audio fields. base := entry.body() if base+10 > entry.end() { - return 8 + 20 + return 0, false } version := binary.BigEndian.Uint16(data[base+8 : base+10]) + hdrLen = 8 + 20 switch version { case 1: - return 8 + 20 + 16 + hdrLen += 16 case 2: - return 8 + 20 + 36 - default: - return 8 + 20 + hdrLen += 36 } + if base+hdrLen > entry.end() { + return 0, false + } + return hdrLen, true } type ac4Location struct { @@ -232,13 +236,16 @@ func normalizeQuickTimeAudioToMP4(data []byte) []byte { return data // already v0 (or v2, left untouched) } - binary.BigEndian.PutUint16(data[verPos:verPos+2], 0) // The v1 QuickTime sound extension is the 16 bytes following the 20-byte v0 // audio fields (samplesPerPacket, bytesPerPacket, bytesPerFrame, bytesPerSample). extStart := entry.body() + 8 + 20 extEnd := extStart + 16 + if extEnd > entry.end() { + return data + } delta := int64(-16) + binary.BigEndian.PutUint16(data[verPos:verPos+2], 0) shiftChunkOffsets(data, loc.chain[0], extStart, delta) for _, b := range loc.chain { growBoxSize(data, b, delta) @@ -273,7 +280,10 @@ func EnsureAC4ConfigBox(decryptedPath, sourcePath string) error { return nil } - hdrLen := audioSampleEntryHeaderLen(dst, loc.entry) + hdrLen, ok := audioSampleEntryHeaderLen(dst, loc.entry) + if !ok { + return fmt.Errorf("malformed ac-4 sample entry") + } childStart := loc.entry.body() + hdrLen if _, has := findChildMP4(dst, childStart, loc.entry.end(), "dac4"); has { // Already has dac4; still persist any normalization changes. diff --git a/go_backend/ac4_config_test.go b/go_backend/ac4_config_test.go new file mode 100644 index 00000000..53dacd23 --- /dev/null +++ b/go_backend/ac4_config_test.go @@ -0,0 +1,76 @@ +package gobackend + +import ( + "bytes" + "encoding/binary" + "os" + "path/filepath" + "testing" +) + +func mp4TestBox(typ string, body []byte) []byte { + out := make([]byte, 8+len(body)) + binary.BigEndian.PutUint32(out[:4], uint32(len(out))) + copy(out[4:8], typ) + copy(out[8:], body) + return out +} + +func mp4TestAC4Tree(entryBody []byte) []byte { + entry := mp4TestBox("ac-4", entryBody) + stsdBody := append([]byte{ + 0, 0, 0, 0, // version/flags + 0, 0, 0, 1, // entry_count + }, entry...) + stsd := mp4TestBox("stsd", stsdBody) + stbl := mp4TestBox("stbl", stsd) + minf := mp4TestBox("minf", stbl) + mdia := mp4TestBox("mdia", minf) + trak := mp4TestBox("trak", mdia) + moov := mp4TestBox("moov", trak) + return moov +} + +func shortAC4SampleEntryBody(version uint16) []byte { + body := make([]byte, 10) + binary.BigEndian.PutUint16(body[8:10], version) + return body +} + +func TestNormalizeQuickTimeAudioToMP4IgnoresTruncatedAC4Entry(t *testing.T) { + input := mp4TestAC4Tree(shortAC4SampleEntryBody(1)) + + defer func() { + if r := recover(); r != nil { + t.Fatalf("normalizeQuickTimeAudioToMP4 panicked: %v", r) + } + }() + + got := normalizeQuickTimeAudioToMP4(append([]byte{}, input...)) + if !bytes.Equal(got, input) { + t.Fatal("truncated QuickTime AC-4 entry should be left unchanged") + } +} + +func TestEnsureAC4ConfigBoxRejectsTruncatedAC4Entry(t *testing.T) { + dir := t.TempDir() + decryptedPath := filepath.Join(dir, "decrypted.mp4") + sourcePath := filepath.Join(dir, "source.mp4") + + if err := os.WriteFile(decryptedPath, mp4TestAC4Tree(shortAC4SampleEntryBody(2)), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(sourcePath, mp4TestBox("moov", mp4TestBox("dac4", []byte{1, 2, 3, 4})), 0o644); err != nil { + t.Fatal(err) + } + + defer func() { + if r := recover(); r != nil { + t.Fatalf("EnsureAC4ConfigBox panicked: %v", r) + } + }() + + if err := EnsureAC4ConfigBox(decryptedPath, sourcePath); err == nil { + t.Fatal("expected malformed AC-4 sample entry error") + } +}