diff --git a/CHANGELOG.md b/CHANGELOG.md index f826ebf1..3d174ea1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 in a wave child of an audio sample entry becomes reachable as `Wave.Esds`. Children that are not well-formed boxes (such as the spec-mandated terminator atom when written with size zero) are preserved verbatim +- `WaveBox.GetChildren`, so a wave box satisfies the `ContainerBox` interface - `DecoderConfigDescriptor.StreamTypeValue` and `UpStream` decompose the packed streamType byte, and named constants cover the common stream types (ISO/IEC 14496-1 Table 6) and object type indications (mp4ra.org), so @@ -30,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- `Info()` escapes control characters in box types as `\xNN`, so a type such + as the four zero bytes of the QuickTime wave terminator atom stays visible - audio sample entries whose reserved bytes claim a QuickTime version whose layout does not parse fall back to the plain ISO interpretation, so previously decodable files keep decoding; the overlaid QuickTime bytes diff --git a/mp4/infodumper.go b/mp4/infodumper.go index b4527db3..b918e92e 100644 --- a/mp4/infodumper.go +++ b/mp4/infodumper.go @@ -32,12 +32,43 @@ func fixStartingCopyrightChar(boxType string) string { // © is 0xa9 in latin1 (and in Apple boxes/atoms) // In UTF-8 it is two bytes: 0xc2 0xa9 bType := []byte(boxType) - if bType[0] == 0xa9 { + if len(bType) > 0 && bType[0] == 0xa9 { bType = append([]byte{0xc2}, bType...) } return string(bType) } +// displayBoxType - make a box type safe to print. Control characters are +// escaped as \xNN, so that a type such as the four zero bytes of the +// QuickTime wave terminator atom stays visible in the output. +func displayBoxType(boxType string) string { + boxType = fixStartingCopyrightChar(boxType) + nrControlChars := 0 + for i := 0; i < len(boxType); i++ { + if isControlChar(boxType[i]) { + nrControlChars++ + } + } + if nrControlChars == 0 { + return boxType + } + var sb strings.Builder + sb.Grow(len(boxType) + 3*nrControlChars) + for i := 0; i < len(boxType); i++ { + if c := boxType[i]; isControlChar(c) { + fmt.Fprintf(&sb, "\\x%02x", c) + } else { + sb.WriteByte(c) + } + } + return sb.String() +} + +// isControlChar - C0 control character or DEL +func isControlChar(c byte) bool { + return c < 0x20 || c == 0x7f +} + // newInfoDumper - make an infoDumper with indent // write version if >= 0 // set Version to -1 if not present for box @@ -45,7 +76,7 @@ func fixStartingCopyrightChar(boxType string) string { // set Version to -3 for descriptors func newInfoDumper(w io.Writer, indent string, b boxLike, version int, flags uint32) *infoDumper { bd := infoDumper{w, indent, b, nil} - utf8BoxType := fixStartingCopyrightChar(b.Type()) + utf8BoxType := displayBoxType(b.Type()) switch { case version >= 0: bd.write("[%s] size=%d version=%d flags=%06x", utf8BoxType, b.Size(), version, flags) diff --git a/mp4/wave.go b/mp4/wave.go index 7cdcb256..e086f9f3 100644 --- a/mp4/wave.go +++ b/mp4/wave.go @@ -17,7 +17,19 @@ type WaveBox struct { Frma *FrmaBox Esds *EsdsBox Children []Box - RawTail []byte + // RawTail holds the bytes from the first position where a well-formed box + // header could not be read to the end of the wave payload. Since the + // terminator atom comes last, this is normally either empty or just that + // atom, but a malformed atom anywhere in the payload puts everything from + // it onwards here, so any boxes after it are not decoded into Children. + // Encode writes RawTail verbatim after the children. + RawTail []byte +} + +// GetChildren - list of child boxes. Note that the bytes in RawTail are not +// part of the children, so a generic container encode would drop them. +func (b *WaveBox) GetChildren() []Box { + return b.Children } // AddChild - add a child box diff --git a/mp4/wave_test.go b/mp4/wave_test.go index c0676d55..0bccb43c 100644 --- a/mp4/wave_test.go +++ b/mp4/wave_test.go @@ -3,6 +3,7 @@ package mp4_test import ( "bytes" "encoding/binary" + "strings" "testing" "github.com/Eyevinn/mp4ff/mp4" @@ -63,6 +64,36 @@ func TestWaveBox(t *testing.T) { } } +func TestWaveBoxIsContainer(t *testing.T) { + wave := &mp4.WaveBox{} + var container mp4.ContainerBox = wave // wave must satisfy the container interface + frma := &mp4.FrmaBox{DataFormat: "mp4a"} + wave.AddChild(frma) + children := container.GetChildren() + if len(children) != 1 || children[0] != frma { + t.Errorf("GetChildren gave %v, wanted the single added frma", children) + } +} + +func TestWaveBoxTerminatorInInfo(t *testing.T) { + terminator := waveChildBytes(string([]byte{0, 0, 0, 0}), nil) + box, err := mp4.DecodeBox(0, bytes.NewReader(waveBytes(terminator))) + if err != nil { + t.Fatal(err) + } + var buf bytes.Buffer + if err := box.Info(&buf, "all:1", "", " "); err != nil { + t.Fatal(err) + } + info := buf.String() + if strings.ContainsRune(info, 0) { + t.Errorf("info output has a raw NUL byte in a box type: %q", info) + } + if !strings.Contains(info, `[\x00\x00\x00\x00]`) { + t.Errorf("terminator box type not escaped in info output: %q", info) + } +} + func TestWaveBoxMalformedEsds(t *testing.T) { badEsds := waveChildBytes("esds", []byte{0, 0}) // truncated esds body data := waveBytes(badEsds)