Repository: Eyevinn/mp4ff
Pull request #361 (2024-07-07 17:40:40)

fix: elng should be full box
The `elng` box was errononously not reading/writing full box headers. This is now changed, but one can still read the old errononous type.

This should solve the re-opened issue #245 where the `moov` box could not be parsed.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0bf3777..e3c41b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Fixed
 
 - Overflow in calculating sample decode time
+- elng box errononously did not include full box headers
 
 ## [0.45.0] - 2024-06-06
 
diff --git a/mp4/container.go b/mp4/container.go
index ce82d5d..d52e872 100644
--- a/mp4/container.go
+++ b/mp4/container.go
@@ -111,7 +111,11 @@ func DecodeContainerChildren(hdr BoxHeader, startPos, endPos uint64, r io.Reader
 		if pos == endPos {
 			return children, nil
 		} else if pos > endPos {
-			return nil, fmt.Errorf("non-matching children box sizes")
+			msg := ""
+			for _, c := range children {
+				msg += fmt.Sprintf("%s:%d ", c.Type(), c.Size())
+			}
+			return nil, fmt.Errorf("non-matching children box sizes, parentSize=%d, %s", endPos-startPos, msg)
 		}
 	}
 }
@@ -123,7 +127,11 @@ func DecodeContainerChildrenSR(hdr BoxHeader, startPos, endPos uint64, sr bits.S
 	initPos := sr.GetPos()
 	for {
 		if pos > endPos {
-			return nil, fmt.Errorf("non matching children box sizes")
+			msg := ""
+			for _, c := range children {
+				msg += fmt.Sprintf("%s:%d ", c.Type(), c.Size())
+			}
+			return nil, fmt.Errorf("non-matching children box sizes, parentSize=%d, %s", endPos-startPos, msg)
 		}
 		if pos == endPos {
 			break
diff --git a/mp4/elng.go b/mp4/elng.go
index 5663dcd..a2fcb8c 100644
--- a/mp4/elng.go
+++ b/mp4/elng.go
@@ -1,19 +1,36 @@
 package mp4
 
 import (
+	"fmt"
 	"io"
 
 	"github.com/Eyevinn/mp4ff/bits"
 )
 
 // ElngBox - Extended Language Box
+// Defined in ISO/IEC 14496-12 Section 8.4.6
+// It should be a full box, but was erronously implemented
+// as a normal box. For backwards compatibility, the
+// erronous box without full header can still be decoded.
+// The method MissingFullBoxBytes() returns true if that is the case.
 type ElngBox struct {
-	Language string
+	missingFullBox bool
+	Version        byte
+	Flags          uint32
+	Language       string
+}
+
+// MissingFullBoxBytes indicates that the box is errornously not including the 4 full box header bytes
+func (b *ElngBox) MissingFullBoxBytes() bool {
+	return b.missingFullBox
 }
 
 // CreateElng - Create an Extended Language Box
 func CreateElng(language string) *ElngBox {
-	return &ElngBox{Language: language}
+	return &ElngBox{
+		Version:  0,
+		Flags:    0,
+		Language: language}
 }
 
 // DecodeElng - box-specific decode
@@ -22,18 +39,27 @@ func DecodeElng(hdr BoxHeader, startPos uint64, r io.Reader) (Box, error) {
 	if err != nil {
 		return nil, err
 	}
-	b := &ElngBox{
-		Language: string(data[:len(data)-1]),
-	}
-	return b, nil
+	sr := bits.NewFixedSliceReader(data)
+	return DecodeElngSR(hdr, startPos, sr)
 }
 
 // DecodeElngSR - box-specific decode
 func DecodeElngSR(hdr BoxHeader, startPos uint64, sr bits.SliceReader) (Box, error) {
-	b := &ElngBox{
-		Language: string(sr.ReadZeroTerminatedString(hdr.payloadLen())),
+	b := ElngBox{}
+	plLen := hdr.payloadLen()
+	if plLen < 7 { // Less than 4 byte flag and version + 2 letters + 0 termination
+		b.missingFullBox = true
+		b.Language = string(sr.ReadZeroTerminatedString(plLen))
+		return &b, nil
+	}
+	versionAndFlags := sr.ReadUint32()
+	if versionAndFlags != 0 {
+		return nil, fmt.Errorf("version and flags are not zero")
 	}
-	return b, sr.AccError()
+	b.Version = byte(versionAndFlags >> 24)
+	b.Flags = versionAndFlags & 0xffffff
+	b.Language = string(sr.ReadZeroTerminatedString(plLen - 4))
+	return &b, sr.AccError()
 }
 
 // Type - box type
@@ -43,7 +69,16 @@ func (b *ElngBox) Type() string {
 
 // Size - calculated size of box
 func (b *ElngBox) Size() uint64 {
-	return uint64(boxHeaderSize + len(b.Language) + 1)
+	size := uint64(boxHeaderSize + 4 + len(b.Language) + 1)
+	if b.missingFullBox {
+		size -= 4
+	}
+	return size
+}
+
+// FixMissingFullBoxBytes adds missing bytes version and flags bytes.
+func (b *ElngBox) FixMissingFullBoxBytes() {
+	b.missingFullBox = false
 }
 
 // Encode - write box to w
@@ -63,13 +98,17 @@ func (b *ElngBox) EncodeSW(sw bits.SliceWriter) error {
 	if err != nil {
 		return err
 	}
+	if !b.missingFullBox {
+		versionAndFlags := uint32(b.Version)<<24 | b.Flags
+		sw.WriteUint32(versionAndFlags)
+	}
 	sw.WriteString(b.Language, true)
 	return sw.AccError()
 }
 
 // Info - write box-specific information
 func (b *ElngBox) Info(w io.Writer, specificBoxLevels, indent, indentStep string) error {
-	bd := newInfoDumper(w, indent, b, -1, 0)
+	bd := newInfoDumper(w, indent, b, int(b.Version), b.Flags)
 	bd.write(" - language: %s", b.Language)
 	return bd.err
 }


Test patch:
diff --git a/mp4/elng_test.go b/mp4/elng_test.go
index 7b58816..e2a679c 100644
--- a/mp4/elng_test.go
+++ b/mp4/elng_test.go
@@ -1,6 +1,7 @@
 package mp4
 
 import (
+	"bytes"
 	"testing"
 )
 
@@ -9,3 +10,48 @@ func TestDecodeElng(t *testing.T) {
 	elng := &ElngBox{Language: "en-US"}
 	boxDiffAfterEncodeAndDecode(t, elng)
 }
+
+// TestElngWithoutFullBox tests erronous case where full box headers are not present.
+func TestElngWithoutFullBox(t *testing.T) {
+	data := []byte("\x00\x00\x00\x0belngdk\x00")
+	bufIn := bytes.NewBuffer(data)
+	box, err := DecodeBox(0, bufIn)
+	if err != nil {
+		t.Errorf("could not decode elng")
+	}
+	elng := box.(*ElngBox)
+	if !elng.MissingFullBoxBytes() {
+		t.Errorf("missing full box not set")
+	}
+	bufOut := bytes.Buffer{}
+	err = elng.Encode(&bufOut)
+	if err != nil {
+		t.Errorf("error encoding elng")
+	}
+	if !bytes.Equal(bufOut.Bytes(), data) {
+		t.Errorf("encoded elng differs from input")
+	}
+}
+
+func TestFixElngMissingFullBoxBytes(t *testing.T) {
+	dataIn := []byte("\x00\x00\x00\x0belngdk\x00")
+	dataOut := []byte("\x00\x00\x00\x0felng\x00\x00\x00\x00dk\x00")
+	bufIn := bytes.NewBuffer(dataIn)
+	box, err := DecodeBox(0, bufIn)
+	if err != nil {
+		t.Errorf("could not decode elng")
+	}
+	elng := box.(*ElngBox)
+	if !elng.MissingFullBoxBytes() {
+		t.Errorf("missing full box not set")
+	}
+	outBuf := bytes.Buffer{}
+	elng.FixMissingFullBoxBytes()
+	err = elng.Encode(&outBuf)
+	if err != nil {
+		t.Errorf("error encoding elng")
+	}
+	if !bytes.Equal(outBuf.Bytes(), dataOut) {
+		t.Errorf("encoded elng differs from input")
+	}
+}