Repository: Eyevinn/mp4ff
Pull request #335 (2024-03-09 17:49:07)

fix: allow absent optional fields in stpp
Handle the case where there is not even an terminal zero bytes for optional utf8lists in stpp box.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8c40f8..5e0f11c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Fixed
 
 - kind box full-box header
+- stpp support when the optional fields do not even have a zero-termination byte
 
 ## [0.42.0] - 2024-01-26
 
diff --git a/mp4/stpp.go b/mp4/stpp.go
index 031f646..6f4625c 100644
--- a/mp4/stpp.go
+++ b/mp4/stpp.go
@@ -12,12 +12,13 @@ import (
 //
 // Contained in : Media Information Box (minf)
 type StppBox struct {
-	Namespace          string   // Mandatory
-	SchemaLocation     string   // Optional but at least a zero-byte termination
-	AuxiliaryMimeTypes string   // Optional, but required if auxiliary types present
-	Btrt               *BtrtBox // Optional
-	Children           []Box
-	DataReferenceIndex uint16
+	Namespace                 string   // Mandatory
+	SchemaLocation            string   // Optional
+	AuxiliaryMimeTypes        string   // Optional, but required if auxiliary types present
+	Btrt                      *BtrtBox // Optional
+	Children                  []Box
+	DataReferenceIndex        uint16
+	nrMissingOptionalEndBytes byte // 0, 1, or 2 depending on whether SchemaLocation and AuxiliaryMimeTypes have a zero end byte
 }
 
 // NewStppBox - Create new stpp box
@@ -25,10 +26,11 @@ type StppBox struct {
 // schemaLocation and auxiliaryMimeTypes are optional but must at least have a zero byte.
 func NewStppBox(namespace, schemaLocation, auxiliaryMimeTypes string) *StppBox {
 	return &StppBox{
-		Namespace:          namespace,
-		DataReferenceIndex: 1,
-		SchemaLocation:     schemaLocation,
-		AuxiliaryMimeTypes: auxiliaryMimeTypes,
+		Namespace:                 namespace,
+		DataReferenceIndex:        1,
+		SchemaLocation:            schemaLocation,
+		AuxiliaryMimeTypes:        auxiliaryMimeTypes,
+		nrMissingOptionalEndBytes: 0,
 	}
 }
 
@@ -71,10 +73,14 @@ func DecodeStppSR(hdr BoxHeader, startPos uint64, sr bits.SliceReader) (Box, err
 	b.Namespace = sr.ReadZeroTerminatedString(hdr.payloadLen() - 8)
 	if maxLen := remainingBytes(sr, initPos, payloadLen); maxLen > 0 {
 		b.SchemaLocation = sr.ReadZeroTerminatedString(maxLen)
+	} else {
+		b.nrMissingOptionalEndBytes++
 	}
 
 	if maxLen := remainingBytes(sr, initPos, payloadLen); maxLen > 0 {
 		b.AuxiliaryMimeTypes = sr.ReadZeroTerminatedString(maxLen)
+	} else {
+		b.nrMissingOptionalEndBytes++
 	}
 	if err := sr.AccError(); err != nil {
 		return nil, fmt.Errorf("DecodeStpp: %w", err)
@@ -110,41 +116,25 @@ func (b *StppBox) Size() uint64 {
 	totalSize := uint64(boxHeaderSize + nrSampleEntryBytes + len(b.Namespace) + 1)
 	totalSize += uint64(len(b.SchemaLocation)) + 1
 	totalSize += uint64(len(b.AuxiliaryMimeTypes)) + 1
+	totalSize -= uint64(b.nrMissingOptionalEndBytes)
 	for _, child := range b.Children {
 		totalSize += child.Size()
 	}
 	return totalSize
 }
 
-// Encode - write box to w
+// Encode - write box to w via a SliceWriter
 func (b *StppBox) Encode(w io.Writer) error {
-	err := EncodeHeader(b, w)
+	sw := bits.NewFixedSliceWriter(int(b.Size()))
+	err := b.EncodeSW(sw)
 	if err != nil {
 		return err
 	}
-	buf := makebuf(b)
-	sw := bits.NewFixedSliceWriterFromSlice(buf)
-	sw.WriteZeroBytes(6)
-	sw.WriteUint16(b.DataReferenceIndex)
-	sw.WriteString(b.Namespace, true)
-	sw.WriteString(b.SchemaLocation, true)
-	sw.WriteString(b.AuxiliaryMimeTypes, true)
-	_, err = w.Write(buf[:sw.Offset()]) // Only write written bytes
-	if err != nil {
-		return err
-	}
-
-	// Next output child boxes in order
-	for _, child := range b.Children {
-		err = child.Encode(w)
-		if err != nil {
-			return err
-		}
-	}
+	_, err = w.Write(sw.Bytes())
 	return err
 }
 
-// Encode - write box to w
+// EncodeSW - write box to sw
 func (b *StppBox) EncodeSW(sw bits.SliceWriter) error {
 	err := EncodeHeaderSW(b, sw)
 	if err != nil {
@@ -153,8 +143,8 @@ func (b *StppBox) EncodeSW(sw bits.SliceWriter) error {
 	sw.WriteZeroBytes(6)
 	sw.WriteUint16(b.DataReferenceIndex)
 	sw.WriteString(b.Namespace, true)
-	sw.WriteString(b.SchemaLocation, true)
-	sw.WriteString(b.AuxiliaryMimeTypes, true)
+	sw.WriteString(b.SchemaLocation, b.nrMissingOptionalEndBytes < 2)
+	sw.WriteString(b.AuxiliaryMimeTypes, b.nrMissingOptionalEndBytes < 1)
 
 	// Next output child boxes in order
 	for _, child := range b.Children {


Test patch:
diff --git a/mp4/stpp_test.go b/mp4/stpp_test.go
index b5ec4a4..07af404 100644
--- a/mp4/stpp_test.go
+++ b/mp4/stpp_test.go
@@ -3,85 +3,109 @@ package mp4
 import (
 	"bytes"
 	"encoding/hex"
+	"strings"
 	"testing"
 
 	"github.com/Eyevinn/mp4ff/bits"
 )
 
-func TestStppEncDec(t *testing.T) {
-	testCases := []struct {
-		namespace      string
-		schemaLocation string
-		mimeTypes      string
-		hasBtrt        bool
-	}{
-		{
-			namespace:      "NS",
-			schemaLocation: "location",
-			mimeTypes:      "image/png image/jpg",
-			hasBtrt:        false,
-		},
-		{
-			namespace:      "NS",
-			schemaLocation: "location",
-			mimeTypes:      "image/png image/jpg",
-			hasBtrt:        true,
-		},
-		{
-			namespace:      "NS",
-			schemaLocation: "",
-			mimeTypes:      "",
-			hasBtrt:        false,
-		},
-		{
-			namespace:      "NS",
-			schemaLocation: "",
-			mimeTypes:      "",
-			hasBtrt:        true,
-		},
-	}
-	for _, tc := range testCases {
-		stpp := NewStppBox(tc.namespace, tc.schemaLocation, tc.mimeTypes)
-		if tc.hasBtrt {
-			btrt := &BtrtBox{}
-			stpp.AddChild(btrt)
-			if stpp.Btrt != btrt {
-				t.Error("Btrt link is broken")
+func TestStpp(t *testing.T) {
+	t.Run("encode and decode", func(t *testing.T) {
+		testCases := []struct {
+			namespace      string
+			schemaLocation string
+			mimeTypes      string
+			hasBtrt        bool
+		}{
+			{
+				namespace:      "NS",
+				schemaLocation: "location",
+				mimeTypes:      "image/png image/jpg",
+				hasBtrt:        false,
+			},
+			{
+				namespace:      "NS",
+				schemaLocation: "location",
+				mimeTypes:      "image/png image/jpg",
+				hasBtrt:        true,
+			},
+			{
+				namespace:      "NS",
+				schemaLocation: "",
+				mimeTypes:      "",
+				hasBtrt:        false,
+			},
+			{
+				namespace:      "NS",
+				schemaLocation: "",
+				mimeTypes:      "",
+				hasBtrt:        true,
+			},
+		}
+		for _, tc := range testCases {
+			t.Logf("Test case: %+v", tc)
+			stpp := NewStppBox(tc.namespace, tc.schemaLocation, tc.mimeTypes)
+			if tc.hasBtrt {
+				btrt := &BtrtBox{}
+				stpp.AddChild(btrt)
+				if stpp.Btrt != btrt {
+					t.Error("Btrt link is broken")
+				}
 			}
+			boxDiffAfterEncodeAndDecode(t, stpp)
 		}
-		boxDiffAfterEncodeAndDecode(t, stpp)
-	}
-}
-
-func TestStppWithEmtptyLists(t *testing.T) {
-	const hexData = ("00000040737470700000000000000001" +
-		"687474703a2f2f7777772e77332e6f72" +
-		"672f6e732f74746d6c00000000000014" +
-		"62747274000003ce00003b5800000430")
-	data, err := hex.DecodeString(hexData)
-	if err != nil {
-		t.Error(err)
-	}
-	sr := bits.NewFixedSliceReader(data)
-	box, err := DecodeBoxSR(0, sr)
-	if err != nil {
-		t.Error(err)
-	}
-	stpp, ok := box.(*StppBox)
-	if !ok {
-		t.Error("not an stpp box")
-	}
-	if int(stpp.Size()) != len(data) {
-		t.Errorf("stpp size %d not same as %d", stpp.Size(), len(data))
-	}
-	buf := bytes.Buffer{}
-	err = stpp.Encode(&buf)
-	if err != nil {
-		t.Error(err)
-	}
-	outData := buf.Bytes()
-	if !bytes.Equal(data, outData) {
-		t.Error("written stpp box differs from read")
-	}
-
+	})
+	t.Run("empty lists", func(t *testing.T) {
+		const hexData = ("00000040737470700000000000000001" +
+			"687474703a2f2f7777772e77332e6f72" +
+			"672f6e732f74746d6c00000000000014" +
+			"62747274000003ce00003b5800000430")
+		data, err := hex.DecodeString(hexData)
+		if err != nil {
+			t.Error(err)
+		}
+		sr := bits.NewFixedSliceReader(data)
+		box, err := DecodeBoxSR(0, sr)
+		if err != nil {
+			t.Error(err)
+		}
+		stpp, ok := box.(*StppBox)
+		if !ok {
+			t.Error("not an stpp box")
+		}
+		if int(stpp.Size()) != len(data) {
+			t.Errorf("stpp size %d not same as %d", stpp.Size(), len(data))
+		}
+		buf := bytes.Buffer{}
+		err = stpp.Encode(&buf)
+		if err != nil {
+			t.Error(err)
+		}
+		outData := buf.Bytes()
+		if !bytes.Equal(data, outData) {
+			t.Error("written stpp box differs from read")
+		}
+	})
+	t.Run("decode completely missing auxiliary mime types", func(t *testing.T) {
+		hexData := ("0000002b737470700000000000000001" +
+			"687474703a2f2f7777772e77332e6f72" +
+			"672f6e732f74746d6c0000")
+		hexData = strings.Replace(hexData, " ", "", -1)
+		data, err := hex.DecodeString(hexData)
+		if err != nil {
+			t.Error(err)
+		}
+		sr := bits.NewFixedSliceReader(data)
+		box, err := DecodeBoxSR(0, sr)
+		if err != nil {
+			t.Error(err)
+		}
+		stpp, ok := box.(*StppBox)
+		if !ok {
+			t.Error("not an stpp box")
+		}
+		if int(stpp.Size()) != len(data) {
+			t.Errorf("stpp size %d not same as %d", stpp.Size(), len(data))
+		}
+	})
 }