mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-02 19:05:57 +02:00
fix(ac4): reject truncated AC-4 sample entries safely
Validate audio sample entry header bounds before QuickTime v1 normalization and dac4 injection so malformed MP4 trees are left unchanged or rejected instead of panicking on truncated boxes.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user