From 8867965ed22daf2661f871a6fea2c385de028c24 Mon Sep 17 00:00:00 2001 From: Philip Stroffolino Date: Sat, 10 Jan 2026 12:35:09 -0500 Subject: [PATCH 01/11] VPLAY-12333 mp4demux hardening Reason for Change: bounds checks and support for 64 bit size Signed-off-by: Philip Stroffolino --- mp4demux/MP4Demux.cpp | 162 ++++++++++++++++++++++++++++++++++-------- mp4demux/MP4Demux.h | 8 ++- 2 files changed, 141 insertions(+), 29 deletions(-) diff --git a/mp4demux/MP4Demux.cpp b/mp4demux/MP4Demux.cpp index 09a03a946..b1e495938 100644 --- a/mp4demux/MP4Demux.cpp +++ b/mp4demux/MP4Demux.cpp @@ -164,6 +164,8 @@ Mp4Demux::Mp4Demux() : moofPtr(), ptr(), version(), flags(), baseMediaDecodeTime(), trackId(), baseDataOffset(), + endPtr(nullptr), + mdatStart(nullptr), mdatEnd(nullptr), defaultSampleDescriptionIndex(), defaultSampleDuration(), defaultSampleSize(), defaultSampleFlags(), duration(), @@ -193,10 +195,18 @@ Mp4Demux::~Mp4Demux() uint64_t Mp4Demux::ReadBytes(int n) { uint64_t rc = 0; - for (int i = 0; i < n; i++) + // Bounds check: never read beyond endPtr + if( !ptr || !endPtr || ptr+n > endPtr ) { - rc <<= 8; - rc |= *ptr++; + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + MP4_LOG_ERR("ReadBytes(%d) exceeded buffer", n); + } + else + { + for (int i = 0; i < n; i++) + { + rc = (rc<<8) | (*ptr++); + } } return rc; } @@ -263,6 +273,7 @@ void Mp4Demux::ReadHeader() */ void Mp4Demux::SkipBytes(size_t len) { + // no bounds check needed as we don't actually read anything here ptr += len; } @@ -343,7 +354,7 @@ void Mp4Demux::ParseTrackEncryptionBox() void Mp4Demux::ParseProtectionSystemSpecificHeaderBox(const uint8_t *next) { ReadHeader(); - // To prevent overflow, ensure box size is at least 16 bytes for system ID + // Must have at least systemID (16) if (ptr + PSSH_SYSTEM_ID_SIZE > next) { parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; @@ -354,18 +365,48 @@ void Mp4Demux::ParseProtectionSystemSpecificHeaderBox(const uint8_t *next) // Add new entry into protection data vector protectionData.emplace_back(); MediaProtectionInfo &psshData = protectionData.back(); - - // Format UUID to stack buffer, then assign to string (copies data) + + // Format UUID (systemID) char systemIdBuffer[FORMATTED_SYSTEM_ID_LENGTH + 1]; // 36 chars + null terminator snprintf(systemIdBuffer, sizeof(systemIdBuffer), "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", - ptr[0x0], ptr[0x1], ptr[0x2], ptr[0x3], ptr[0x4], ptr[0x5], ptr[0x6], ptr[0x7], - ptr[0x8], ptr[0x9], ptr[0xa], ptr[0xb], ptr[0xc], ptr[0xd], ptr[0xe], ptr[0xf]); + ptr[0x0], ptr[0x1], ptr[0x2], ptr[0x3], ptr[0x4], ptr[0x5], ptr[0x6], ptr[0x7], + ptr[0x8], ptr[0x9], ptr[0xa], ptr[0xb], ptr[0xc], ptr[0xd], ptr[0xe], ptr[0xf]); psshData.systemID.assign(systemIdBuffer, FORMATTED_SYSTEM_ID_LENGTH); // Copy without null terminator ptr += PSSH_SYSTEM_ID_SIZE; - size_t psshSize = next - ptr; - psshData.pssh = std::vector(ptr, ptr + psshSize); - // Updates ptr to next box - SkipBytes(psshSize); + + // Version-aware parsing: + if (version == 1) { + // KID_count (u32) + KIDs (count * 16) + if (ptr + 4 > next) + { + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + return; + } + uint32_t kidCount = ReadU32(); + size_t kidBytes = static_cast(kidCount) * 16; + if (ptr + kidBytes > next) + { + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + return; + } + // Optional: store KIDs in psshData if your MediaProtectionInfo supports it + ptr += kidBytes; + } + // data_size (u32) + data (blob) + if (ptr + 4 > next) + { + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + return; + } + uint32_t dataSize = ReadU32(); + if (ptr + dataSize > next) + { + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + MP4_LOG_ERR("PSSH payload exceeds box: dataSize=%u remaining=%zu", dataSize, next - ptr); + return; + } + psshData.pssh.assign(ptr, ptr + dataSize); + SkipBytes(dataSize); } } @@ -512,15 +553,35 @@ void Mp4Demux::ParseSampleAuxiliaryInformationOffsets() ParseProtectionSchemeInfo(); SkipBytes(4); // aux_info_type_parameter } - SkipBytes(4); // entry_count - - if( version == 0 ) + // entry_count + if (ptr + 4 > endPtr) + { + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + return; + } + uint32_t entryCount = ReadU32(); + if (entryCount == 0) + { + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + MP4_LOG_ERR("SAIO entry_count == 0"); + return; + } + // Read the first offset; if multiple, warn and consume others + if (version == 0) { auxiliaryInformationOffset = ReadU32(); + for (uint32_t i = 1; i < entryCount; ++i) + { + (void)ReadU32(); + } } else { auxiliaryInformationOffset = ReadU64(); + for (uint32_t i = 1; i < entryCount; ++i) + { + (void)ReadU64(); + } } gotAuxiliaryInformationOffset = true; } @@ -596,6 +657,14 @@ void Mp4Demux::ParseTrackRun() int32_t dataOffset = ReadI32(); dataPtr += dataOffset; } + // If we tracked an mdat range, validate dataPtr lies within it + if (mdatStart && mdatEnd) + { + if (dataPtr < mdatStart || dataPtr > mdatEnd) + { + MP4_LOG_WARN("TRUN dataPtr outside mdat range"); + } + } else { // mandatory field? should never reach here @@ -633,6 +702,15 @@ void Mp4Demux::ParseTrackRun() { // for samples where pts and dts differ (overriding 'trex') sampleCompositionTimeOffset = ReadI32(); } + // Guard: sample buffer must not overrun endPtr (or mdat) + const uint8_t* hardEnd = endPtr; + if (mdatEnd) hardEnd = mdatEnd; + if (dataPtr + sampleLen > hardEnd) + { + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + MP4_LOG_ERR("TRUN sample overruns buffer: need %u bytes", sampleLen); + return; + } newSample.mData.AppendBytes(dataPtr, sampleLen); dataPtr += sampleLen; newSample.mDts = dts / (double)timeScale; @@ -816,12 +894,17 @@ void Mp4Demux::ParseSampleDescriptionBox(const uint8_t *next) { ReadHeader(); uint32_t count = ReadU32(); - if (count != 1) - { + // Be tolerant: parse child boxes regardless of count; warn if 0 or >1 + if (count == 0) { parseError = MP4_PARSE_ERROR_UNSUPPORTED_SAMPLE_ENTRY_COUNT; - MP4_LOG_ERR("Unsupported sample description count: %u, expected 1", count); + MP4_LOG_ERR("stsd count == 0"); return; } + if (count > 1) + { + MP4_LOG_WARN("stsd count=%u; parsing entries and using first supported one", count); + } + // Parse contained sample entries/config boxes DemuxHelper(next); } @@ -879,8 +962,7 @@ int Mp4Demux::ReadLen() for (;;) { unsigned char octet = *ptr++; - rc <<= 7; - rc |= octet & 0x7f; + rc = (rc<<7) | (octet & 0x7f); if ((octet & 0x80) == 0) return rc; } } @@ -1001,16 +1083,30 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) { while (ptr < fin) { - uint32_t size = ReadU32(); - const uint8_t *next = ptr + size - 4; + uint64_t size = ReadU32(); uint32_t type = ReadU32(); - MP4_LOG_DEBUG("Box type: %s, size: %u", FourCCToString(type).c_str(), size); + const uint8_t *next = nullptr; + if( size==1 ) + { // size includes size(4)+type(4)+large_size(8) + size = ReadU64(); + if (size < 16) + { + parseError = MP4_PARSE_ERROR_INVALID_BOX; + return; + } + next = ptr + (size - 16); + } + else + { // payload after size+type + next = ptr + (size - 8); + } + MP4_LOG_DEBUG("Box type: %s, size: %" PRIu64, FourCCToString(type).c_str(), size); // Validate that the box doesn't extend beyond buffer if (next > fin) { parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("Box type %s size %u extends beyond buffer boundary", FourCCToString(type).c_str(), size); + MP4_LOG_ERR("Box type %s size %" PRIu64 " extends beyond buffer boundary", FourCCToString(type).c_str(), size); return; } @@ -1149,10 +1245,18 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) case MultiChar_Constant("styp"): // Segment Type (under file box) case MultiChar_Constant("sidx"): // Segment Index case MultiChar_Constant("udta"): // User Data (can appear under moov, trak, moof, traf) - case MultiChar_Constant("mdat"): // Movie Data (under file box) - // Skip these boxes for now ptr = next; break; + + case MultiChar_Constant("mdat"): // Movie Data (under file box) + // Track mdat payload range for TRUN validation + if (type == MultiChar_Constant("mdat")) + { + mdatStart = ptr; // start of payload (after header bytes) + mdatEnd = next; // end of payload + } + ptr = next; // skip payload + break; default: break; @@ -1196,7 +1300,9 @@ bool Mp4Demux::Parse(const void *ptr, size_t len) protectionData.clear(); gotAuxiliaryInformationOffset = false; moofPtr = NULL; - + endPtr = &((const uint8_t*)ptr)[len]; + mdatStart = nullptr; + mdatEnd = nullptr; this->ptr = (const uint8_t *)ptr; DemuxHelper(&this->ptr[len]); if (parseError == MP4_PARSE_OK) @@ -1284,4 +1390,4 @@ std::vector Mp4Demux::GetSamples() { // std::move is required here because codecInfo is a member variable. return std::move(samples); -} \ No newline at end of file +} diff --git a/mp4demux/MP4Demux.h b/mp4demux/MP4Demux.h index ec085d91d..9b5f76972 100644 --- a/mp4demux/MP4Demux.h +++ b/mp4demux/MP4Demux.h @@ -119,6 +119,7 @@ class Mp4Demux private: // Stream format and configuration uint32_t streamFormat; /**< Stream format identifier */ + const uint8_t* endPtr; /**< Absolute end of current parse buffer */ // Encryption parameters uint8_t ivSize; /**< Initialization vector size */ @@ -143,11 +144,15 @@ class Mp4Demux // Parser state const uint8_t *moofPtr; /**< Base address for sample data */ const uint8_t *ptr; /**< Current parser position */ + // MDAT range tracking (for sample data validation) + const uint8_t *mdatStart; /**< Start of current/last mdat payload */ + const uint8_t *mdatEnd; /**< End of current/last mdat payload */ // Box header fields uint8_t version; /**< Box version */ uint32_t flags; /**< Box flags */ + // Track fragment fields uint64_t baseMediaDecodeTime; /**< Base media decode time */ uint32_t trackId; /**< Track identifier */ @@ -383,6 +388,7 @@ class Mp4Demux * @brief Parse MP4 data * @param ptr Pointer to MP4 data * @param len Length of data + * endPtr is set to &((const uint8_t*)ptr)[len] for uniform bounds checking * @return true if parsing succeeded, false on error */ bool Parse(const void *ptr, size_t len); @@ -428,4 +434,4 @@ class Mp4Demux std::vector GetSamples(); }; -#endif /* __MP4_DEMUX_H__ */ \ No newline at end of file +#endif /* __MP4_DEMUX_H__ */ From 562b8814939d912c40691b866b394cd5b864138c Mon Sep 17 00:00:00 2001 From: Philip Stroffolino Date: Sat, 10 Jan 2026 13:08:14 -0500 Subject: [PATCH 02/11] Reason for Change: AC-4 support Signed-off-by: Philip Stroffolino --- mp4demux/MP4Demux.cpp | 48 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/mp4demux/MP4Demux.cpp b/mp4demux/MP4Demux.cpp index b1e495938..8dd4921c9 100644 --- a/mp4demux/MP4Demux.cpp +++ b/mp4demux/MP4Demux.cpp @@ -93,7 +93,10 @@ constexpr CodecMapping gCodecMappings[] = { { MultiChar_Constant("avcC"), GST_FORMAT_VIDEO_ES_H264 }, { MultiChar_Constant("hvcC"), GST_FORMAT_VIDEO_ES_HEVC }, { MultiChar_Constant("esds"), GST_FORMAT_AUDIO_ES_AAC_RAW }, - { MultiChar_Constant("dec3"), GST_FORMAT_AUDIO_ES_EC3 } + { MultiChar_Constant("dec3"), GST_FORMAT_AUDIO_ES_EC3 }, + + // AC-4 decoder config box + { MultiChar_Constant("dac4"), GST_FORMAT_AUDIO_ES_AC4 } }; /** @@ -195,8 +198,8 @@ Mp4Demux::~Mp4Demux() uint64_t Mp4Demux::ReadBytes(int n) { uint64_t rc = 0; - // Bounds check: never read beyond endPtr - if( !ptr || !endPtr || ptr+n > endPtr ) + // Bounds check: never read beyond endPtr; also validate n in [1,8] + if (n <= 0 || n > 8 || !ptr || !endPtr || ptr + n > endPtr) { parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; MP4_LOG_ERR("ReadBytes(%d) exceeded buffer", n); @@ -273,8 +276,15 @@ void Mp4Demux::ReadHeader() */ void Mp4Demux::SkipBytes(size_t len) { - // no bounds check needed as we don't actually read anything here - ptr += len; + if (!ptr || !endPtr || ptr + len > endPtr) + { + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + MP4_LOG_ERR("SkipBytes(%zu) exceeded buffer", len); + } + else + { + ptr += len; + } } /** @@ -931,6 +941,7 @@ void Mp4Demux::ParseStreamFormatBox(uint32_t type, const uint8_t *next) case MultiChar_Constant("mp4a"): case MultiChar_Constant("ec-3"): + case MultiChar_Constant("ac-4"): case MultiChar_Constant("enca"): ParseAudioInformation(); break; @@ -961,8 +972,14 @@ int Mp4Demux::ReadLen() int rc = 0; for (;;) { + if (!ptr || !endPtr || ptr >= endPtr) + { + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + MP4_LOG_ERR("ReadLen() exceeded buffer"); + return 0; + } unsigned char octet = *ptr++; - rc = (rc<<7) | (octet & 0x7f); + rc = (rc << 7) | (octet & 0x7f); // accumulate variable length integer if ((octet & 0x80) == 0) return rc; } } @@ -1060,6 +1077,13 @@ void Mp4Demux::ParseCodecConfigurationBox(uint32_t type, const uint8_t *next) else { size_t codecDataLen = next - ptr; + // Guard: ensure we don't run past the overall buffer even if next is sane + if (!endPtr || ptr + codecDataLen > endPtr) + { + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + MP4_LOG_ERR("Codec configuration exceeds buffer"); + return; + } // No need to read this for dec3 box. Expand the filter later if needed. if (type != MultiChar_Constant("dec3")) { @@ -1096,12 +1120,21 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) } next = ptr + (size - 16); } + else if( size == 0 ) + { // box extends to end of buffer (common + next = fin; + } else { // payload after size+type + if (size < 8) + { + parseError = MP4_PARSE_ERROR_INVALID_BOX; + MP4_LOG_ERR("Invalid box size: %" PRIu64, size); + return; + } next = ptr + (size - 8); } MP4_LOG_DEBUG("Box type: %s, size: %" PRIu64, FourCCToString(type).c_str(), size); - // Validate that the box doesn't extend beyond buffer if (next > fin) { @@ -1123,6 +1156,7 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) break; case MultiChar_Constant("hvcC"): + case MultiChar_Constant("dac4"): // AC-4 DecoderSpecific box case MultiChar_Constant("dec3"): case MultiChar_Constant("avcC"): case MultiChar_Constant("esds"): // Elementary Stream Descriptor From 789a87e5651f2c186eba441635fd54df6105958b Mon Sep 17 00:00:00 2001 From: Philip Stroffolino Date: Sat, 10 Jan 2026 17:13:00 -0500 Subject: [PATCH 03/11] Reason for Change: few additional optimizations/improvements Signed-off-by: Philip Stroffolino --- mp4demux/MP4Demux.cpp | 70 ++++++++++++++++++++++++++----------------- mp4demux/MP4Demux.h | 5 ++-- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/mp4demux/MP4Demux.cpp b/mp4demux/MP4Demux.cpp index 8dd4921c9..faf7c4d13 100644 --- a/mp4demux/MP4Demux.cpp +++ b/mp4demux/MP4Demux.cpp @@ -164,10 +164,9 @@ Mp4Demux::Mp4Demux() : samples(), defaultKid(), gotAuxiliaryInformationOffset(), auxiliaryInformationOffset(), schemeType(CIPHER_TYPE_NONE), originalMediaType(), cencAuxInfoSizes(), protectionData(), - moofPtr(), ptr(), + moofPtr(), ptr(), endPtr(nullptr), version(), flags(), baseMediaDecodeTime(), trackId(), baseDataOffset(), - endPtr(nullptr), mdatStart(nullptr), mdatEnd(nullptr), defaultSampleDescriptionIndex(), defaultSampleDuration(), defaultSampleSize(), defaultSampleFlags(), @@ -580,20 +579,35 @@ void Mp4Demux::ParseSampleAuxiliaryInformationOffsets() if (version == 0) { auxiliaryInformationOffset = ReadU32(); - for (uint32_t i = 1; i < entryCount; ++i) + if( parseError == MP4_PARSE_OK ) { - (void)ReadU32(); + for (uint32_t i = 1; i < entryCount; ++i) + { + (void)ReadU32(); + if( parseError != MP4_PARSE_OK ) + { + break; + } + } + gotAuxiliaryInformationOffset = true; } } else { auxiliaryInformationOffset = ReadU64(); - for (uint32_t i = 1; i < entryCount; ++i) + if( parseError == MP4_PARSE_OK ) { - (void)ReadU64(); + for (uint32_t i = 1; i < entryCount; ++i) + { + (void)ReadU64(); + if( parseError != MP4_PARSE_OK ) + { + break; + } + } + gotAuxiliaryInformationOffset = true; } } - gotAuxiliaryInformationOffset = true; } /** @@ -969,19 +983,22 @@ void Mp4Demux::ParseStreamFormatBox(uint32_t type, const uint8_t *next) */ int Mp4Demux::ReadLen() { - int rc = 0; - for (;;) + if( ptr && endPtr ) { - if (!ptr || !endPtr || ptr >= endPtr) - { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("ReadLen() exceeded buffer"); - return 0; + int rc = 0; + while( ptr Date: Sat, 10 Jan 2026 21:48:09 -0500 Subject: [PATCH 04/11] Reason for Change: setParseError cleanup Signed-off-by: Philip Stroffolino --- mp4demux/MP4Demux.cpp | 124 +++++------ mp4demux/MP4Demux.h | 25 ++- .../Mp4BoxParsingTests/BoxParsingTests.cpp | 200 ++++++++++++++++-- 3 files changed, 255 insertions(+), 94 deletions(-) diff --git a/mp4demux/MP4Demux.cpp b/mp4demux/MP4Demux.cpp index faf7c4d13..6e04b585f 100644 --- a/mp4demux/MP4Demux.cpp +++ b/mp4demux/MP4Demux.cpp @@ -164,7 +164,8 @@ Mp4Demux::Mp4Demux() : samples(), defaultKid(), gotAuxiliaryInformationOffset(), auxiliaryInformationOffset(), schemeType(CIPHER_TYPE_NONE), originalMediaType(), cencAuxInfoSizes(), protectionData(), - moofPtr(), ptr(), endPtr(nullptr), + moofPtr(), + ptr(), endPtr(nullptr), version(), flags(), baseMediaDecodeTime(), trackId(), baseDataOffset(), mdatStart(nullptr), mdatEnd(nullptr), @@ -185,6 +186,26 @@ Mp4Demux::~Mp4Demux() { } +void Mp4Demux::setParseError( Mp4ParseError err ) +{ + parseError = err; + const char *text[] = + { + "OK", + "INVALID_BOX", + "INVALID_CONSTANT_IV_SIZE", + "SAMPLE_COUNT_MISMATCH", + "UNSUPPORTED_ENCRYPTION_SCHEME", + "INVALID_PADDING", + "UNSUPPORTED_SAMPLE_ENTRY_COUNT", + "UNSUPPORTED_STREAM_FORMAT", + "INVALID_ESDS_TAG", + "DATA_BOUNDARY_MISMATCH", + "INVALID_INPUT" + }; + MP4_LOG_ERR( "%s", text[err] ); +} + /** * @brief Read n bytes from current position in big-endian format * Reads bytes from the current parser position and converts from @@ -200,8 +221,7 @@ uint64_t Mp4Demux::ReadBytes(int n) // Bounds check: never read beyond endPtr; also validate n in [1,8] if (n <= 0 || n > 8 || !ptr || !endPtr || ptr + n > endPtr) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("ReadBytes(%d) exceeded buffer", n); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); } else { @@ -276,10 +296,9 @@ void Mp4Demux::ReadHeader() void Mp4Demux::SkipBytes(size_t len) { if (!ptr || !endPtr || ptr + len > endPtr) - { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("SkipBytes(%zu) exceeded buffer", len); - } + { + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); + } else { ptr += len; @@ -342,8 +361,7 @@ void Mp4Demux::ParseTrackEncryptionBox() constantIvSize = *ptr++; if (constantIvSize != 8 && constantIvSize != 16) { - parseError = MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE; - MP4_LOG_ERR("Invalid constant IV size: %u, expected 8 or 16", constantIvSize); + setParseError( MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE ); return; } constantIv = std::vector(ptr, ptr + constantIvSize); @@ -366,8 +384,7 @@ void Mp4Demux::ParseProtectionSystemSpecificHeaderBox(const uint8_t *next) // Must have at least systemID (16) if (ptr + PSSH_SYSTEM_ID_SIZE > next) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("Invalid PSSH box size, remaining %zu, expected at least %d bytes for system ID", next - ptr, PSSH_SYSTEM_ID_SIZE); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); } else { @@ -388,14 +405,14 @@ void Mp4Demux::ParseProtectionSystemSpecificHeaderBox(const uint8_t *next) // KID_count (u32) + KIDs (count * 16) if (ptr + 4 > next) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } uint32_t kidCount = ReadU32(); size_t kidBytes = static_cast(kidCount) * 16; if (ptr + kidBytes > next) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } // Optional: store KIDs in psshData if your MediaProtectionInfo supports it @@ -404,14 +421,13 @@ void Mp4Demux::ParseProtectionSystemSpecificHeaderBox(const uint8_t *next) // data_size (u32) + data (blob) if (ptr + 4 > next) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } uint32_t dataSize = ReadU32(); if (ptr + dataSize > next) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("PSSH payload exceeds box: dataSize=%u remaining=%zu", dataSize, next - ptr); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } psshData.pssh.assign(ptr, ptr + dataSize); @@ -436,8 +452,7 @@ void Mp4Demux::ProcessAuxiliaryInformation() uint64_t maxSampleCount = sampleOffset + sampleCount; if (samples.size() != maxSampleCount) { - parseError = MP4_PARSE_ERROR_SAMPLE_COUNT_MISMATCH; - MP4_LOG_ERR("Sample count mismatch: expected %" PRIu64 ", got %zu", maxSampleCount, samples.size()); + setParseError( MP4_PARSE_ERROR_SAMPLE_COUNT_MISMATCH ); return; } for (auto i = sampleOffset; i < maxSampleCount; i++) @@ -535,8 +550,7 @@ void Mp4Demux::ParseProtectionSchemeInfo() auto cipher = GetCipherTypeFromFourCC(type); if (cipher == CIPHER_TYPE_NONE) { - parseError = MP4_PARSE_ERROR_UNSUPPORTED_ENCRYPTION_SCHEME; - MP4_LOG_ERR("Unsupported encryption scheme type: %s, expected 'cenc' or 'cbcs'", FourCCToString(type).c_str()); + setParseError( MP4_PARSE_ERROR_UNSUPPORTED_ENCRYPTION_SCHEME ); return; } schemeType = cipher; @@ -565,14 +579,13 @@ void Mp4Demux::ParseSampleAuxiliaryInformationOffsets() // entry_count if (ptr + 4 > endPtr) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } uint32_t entryCount = ReadU32(); if (entryCount == 0) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("SAIO entry_count == 0"); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } // Read the first offset; if multiple, warn and consume others @@ -625,8 +638,7 @@ void Mp4Demux::ParseSampleEncryption() uint64_t maxSampleCount = sampleOffset + sampleCount; if (samples.size() != maxSampleCount) { - parseError = MP4_PARSE_ERROR_SAMPLE_COUNT_MISMATCH; - MP4_LOG_ERR("Sample count mismatch in SENC: expected %" PRIu64 ", got %zu", maxSampleCount, samples.size()); + setParseError( MP4_PARSE_ERROR_SAMPLE_COUNT_MISMATCH ); return; } for (auto iSample = sampleOffset; iSample < maxSampleCount; iSample++) @@ -674,26 +686,14 @@ void Mp4Demux::ParseTrackRun() ReadHeader(); uint32_t sampleCount = ReadU32(); const uint8_t *dataPtr = moofPtr; - //0xE01 if (flags & TRUN_DATA_OFFSET_PRESENT) - { - // offset from start of Moof box field + { // offset from start of Moof box field int32_t dataOffset = ReadI32(); dataPtr += dataOffset; } - // If we tracked an mdat range, validate dataPtr lies within it - if (mdatStart && mdatEnd) - { - if (dataPtr < mdatStart || dataPtr > mdatEnd) - { - MP4_LOG_WARN("TRUN dataPtr outside mdat range"); - } - } - else + if( mdatStart && (dataPtr < mdatStart || dataPtr >= mdatEnd) ) { - // mandatory field? should never reach here - parseError = MP4_PARSE_ERROR_MISSING_DATA_OFFSET; - MP4_LOG_ERR("Missing data offset in TRUN box"); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } uint32_t sampleFlags = 0; @@ -727,12 +727,10 @@ void Mp4Demux::ParseTrackRun() sampleCompositionTimeOffset = ReadI32(); } // Guard: sample buffer must not overrun endPtr (or mdat) - const uint8_t* hardEnd = endPtr; - if (mdatEnd) hardEnd = mdatEnd; - if (dataPtr + sampleLen > hardEnd) + const uint8_t* hardEnd = mdatEnd?mdatEnd:endPtr; + if ( dataPtr + sampleLen > hardEnd ) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("TRUN sample overruns buffer: need %u bytes", sampleLen); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } newSample.mData.AppendBytes(dataPtr, sampleLen); @@ -810,9 +808,7 @@ void Mp4Demux::ParseVideoInformation() int pad = ReadU16(); if (pad != VIDEO_PADDING_MARKER) { - // TODO: Is it a critical error? - parseError = MP4_PARSE_ERROR_INVALID_PADDING; - MP4_LOG_ERR("Invalid padding value: 0x%04x, expected 0xffff", pad); + setParseError( MP4_PARSE_ERROR_INVALID_PADDING ); return; } } @@ -920,8 +916,7 @@ void Mp4Demux::ParseSampleDescriptionBox(const uint8_t *next) uint32_t count = ReadU32(); // Be tolerant: parse child boxes regardless of count; warn if 0 or >1 if (count == 0) { - parseError = MP4_PARSE_ERROR_UNSUPPORTED_SAMPLE_ENTRY_COUNT; - MP4_LOG_ERR("stsd count == 0"); + setParseError( MP4_PARSE_ERROR_UNSUPPORTED_SAMPLE_ENTRY_COUNT ); return; } if (count > 1) @@ -961,8 +956,7 @@ void Mp4Demux::ParseStreamFormatBox(uint32_t type, const uint8_t *next) break; default: - parseError = MP4_PARSE_ERROR_UNSUPPORTED_STREAM_FORMAT; - MP4_LOG_ERR("Unsupported stream format: 0x%08x", streamFormat); + setParseError( MP4_PARSE_ERROR_UNSUPPORTED_STREAM_FORMAT ); break; } // No need to continue if error occurred @@ -996,8 +990,7 @@ int Mp4Demux::ReadLen() } } } - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("ReadLen() exceeded buffer"); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return 0; } @@ -1053,8 +1046,7 @@ void Mp4Demux::ParseEsdsCodecConfigHelper(const uint8_t *next) break; default: - parseError = MP4_PARSE_ERROR_INVALID_ESDS_TAG; - MP4_LOG_ERR("Invalid ESDS tag: 0x%02x", tag); + setParseError( MP4_PARSE_ERROR_INVALID_ESDS_TAG ); break; } @@ -1065,8 +1057,7 @@ void Mp4Demux::ParseEsdsCodecConfigHelper(const uint8_t *next) } if (ptr != next) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("ESDS size mismatch: expected end at %p, current ptr at %p", next, ptr); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); } } @@ -1097,8 +1088,7 @@ void Mp4Demux::ParseCodecConfigurationBox(uint32_t type, const uint8_t *next) // Guard: ensure we don't run past the overall buffer even if next is sane if (!endPtr || ptr + codecDataLen > endPtr) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("Codec configuration exceeds buffer"); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } // No need to read this for dec3 box. Expand the filter later if needed. @@ -1132,7 +1122,7 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) size = ReadU64(); if (size < 16) { - parseError = MP4_PARSE_ERROR_INVALID_BOX; + setParseError( MP4_PARSE_ERROR_INVALID_BOX ); return; } next = ptr + (size - 16); @@ -1143,8 +1133,7 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) } else if( size<8 ) { - parseError = MP4_PARSE_ERROR_INVALID_BOX; - MP4_LOG_ERR("Invalid box size: %" PRIu64, size); + setParseError( MP4_PARSE_ERROR_INVALID_BOX ); return; } else @@ -1155,8 +1144,7 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) // Validate that the box doesn't extend beyond buffer if (next > fin) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("Box type %s size %" PRIu64 " extends beyond buffer boundary", FourCCToString(type).c_str(), size); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } @@ -1315,8 +1303,7 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) } if (ptr != next) { - parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; - MP4_LOG_ERR("Box type %s data boundary mismatch, ptr offset: %td", FourCCToString(type).c_str(), ptr - next); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } } @@ -1368,8 +1355,7 @@ bool Mp4Demux::Parse(const void *ptr, size_t len) } else { - parseError = MP4_PARSE_ERROR_INVALID_INPUT; - MP4_LOG_ERR("Invalid input to Parse: ptr=%p, len=%zu", ptr, len); + setParseError( MP4_PARSE_ERROR_INVALID_INPUT ); } return ret; } diff --git a/mp4demux/MP4Demux.h b/mp4demux/MP4Demux.h index 879def8f3..4cca027b2 100644 --- a/mp4demux/MP4Demux.h +++ b/mp4demux/MP4Demux.h @@ -79,18 +79,17 @@ enum mp4LogLevel */ enum Mp4ParseError { - MP4_PARSE_OK = 0, /**< No error */ - MP4_PARSE_ERROR_INVALID_BOX, /**< Invalid box encountered */ - MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE, /**< Invalid constant IV size */ - MP4_PARSE_ERROR_SAMPLE_COUNT_MISMATCH, /**< Sample count mismatch */ - MP4_PARSE_ERROR_UNSUPPORTED_ENCRYPTION_SCHEME, /**< Invalid auxiliary info type */ - MP4_PARSE_ERROR_MISSING_DATA_OFFSET, /**< Missing data offset in TRUN */ - MP4_PARSE_ERROR_INVALID_PADDING, /**< Invalid padding value */ - MP4_PARSE_ERROR_UNSUPPORTED_SAMPLE_ENTRY_COUNT,/**< Unsupported sample entry count */ + MP4_PARSE_OK, /**< No error */ + MP4_PARSE_ERROR_INVALID_BOX, /**< Invalid box header size */ + MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE, /**< Invalid constant IV size (expected 8 or 16) */ + MP4_PARSE_ERROR_SAMPLE_COUNT_MISMATCH, /**< Explicit sample count doesn't match implicit sample count */ + MP4_PARSE_ERROR_UNSUPPORTED_ENCRYPTION_SCHEME, /**< Expected cenc or cbcs */ + MP4_PARSE_ERROR_INVALID_PADDING, /**< Unexpected Video Padding field (should be 0xffff) */ + MP4_PARSE_ERROR_UNSUPPORTED_SAMPLE_ENTRY_COUNT,/**< Zero sample entry count */ MP4_PARSE_ERROR_UNSUPPORTED_STREAM_FORMAT, /**< Unsupported stream format */ MP4_PARSE_ERROR_INVALID_ESDS_TAG, /**< Invalid ESDS tag */ - MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH, /**< Data boundary mismatch */ - MP4_PARSE_ERROR_INVALID_INPUT /**< Invalid input to parse function */ + MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH, /**< Data boundary mismatch - referencing invalid memory */ + MP4_PARSE_ERROR_INVALID_INPUT /**< Invalid input to parse function; nullptr or zero length*/ }; /** @@ -171,6 +170,12 @@ class Mp4Demux MediaCodecInfo codecInfo; /**< Codec information */ Mp4ParseError parseError; /**< Current parse error state */ + /** + * @brief log human readable parse error and update state + * @param parseError one of Mp4ParseError + */ + void setParseError( Mp4ParseError ); + /** * @brief Read n bytes from current position in big-endian format * @param n Number of bytes to read diff --git a/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp b/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp index 767c4b480..1db7b08d0 100644 --- a/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp +++ b/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp @@ -172,20 +172,23 @@ TEST_F(Mp4DemuxFunctionalTests, ParseEncryptedFragmentWithSencBox) auto samples = mDemuxer->GetSamples(); EXPECT_EQ(samples.size(), 2) << "Should have exactly 2 samples"; - // Validate DRM metadata for each sample - EXPECT_TRUE(samples[0].mDrmMetadata.mIsEncrypted) << "Sample should be marked as encrypted"; - EXPECT_FALSE(samples[0].mDrmMetadata.mKeyId.empty()) << "Sample should have Key ID"; - EXPECT_FALSE(samples[0].mDrmMetadata.mIV.empty()) << "Sample should have IV"; - EXPECT_FALSE(samples[0].mDrmMetadata.mCipher == CIPHER_TYPE_NONE) << "Sample should have cipher type"; - EXPECT_EQ(samples[0].mDrmMetadata.mSubSamples.size(), 6) << "Sample should have subsample encryption data"; - EXPECT_EQ(samples[0].mDrmMetadata.mNumSubSamples, 1) << "Sample should have 1 subsamples"; - - EXPECT_TRUE(samples[1].mDrmMetadata.mIsEncrypted) << "Sample should be marked as encrypted"; - EXPECT_FALSE(samples[1].mDrmMetadata.mKeyId.empty()) << "Sample should have Key ID"; - EXPECT_FALSE(samples[1].mDrmMetadata.mIV.empty()) << "Sample should have IV"; - EXPECT_FALSE(samples[1].mDrmMetadata.mCipher == CIPHER_TYPE_NONE) << "Sample should have cipher type"; - EXPECT_EQ(samples[1].mDrmMetadata.mSubSamples.size(), 12) << "Sample should have subsample encryption data"; - EXPECT_EQ(samples[1].mDrmMetadata.mNumSubSamples, 2) << "Sample should have 2 subsamples"; + if( samples.size() == 2 ) + { + // Validate DRM metadata for each sample + EXPECT_TRUE(samples[0].mDrmMetadata.mIsEncrypted) << "Sample should be marked as encrypted"; + EXPECT_FALSE(samples[0].mDrmMetadata.mKeyId.empty()) << "Sample should have Key ID"; + EXPECT_FALSE(samples[0].mDrmMetadata.mIV.empty()) << "Sample should have IV"; + EXPECT_FALSE(samples[0].mDrmMetadata.mCipher == CIPHER_TYPE_NONE) << "Sample should have cipher type"; + EXPECT_EQ(samples[0].mDrmMetadata.mSubSamples.size(), 6) << "Sample should have subsample encryption data"; + EXPECT_EQ(samples[0].mDrmMetadata.mNumSubSamples, 1) << "Sample should have 1 subsamples"; + + EXPECT_TRUE(samples[1].mDrmMetadata.mIsEncrypted) << "Sample should be marked as encrypted"; + EXPECT_FALSE(samples[1].mDrmMetadata.mKeyId.empty()) << "Sample should have Key ID"; + EXPECT_FALSE(samples[1].mDrmMetadata.mIV.empty()) << "Sample should have IV"; + EXPECT_FALSE(samples[1].mDrmMetadata.mCipher == CIPHER_TYPE_NONE) << "Sample should have cipher type"; + EXPECT_EQ(samples[1].mDrmMetadata.mSubSamples.size(), 12) << "Sample should have subsample encryption data"; + EXPECT_EQ(samples[1].mDrmMetadata.mNumSubSamples, 2) << "Sample should have 2 subsamples"; + } } /** @@ -259,4 +262,171 @@ TEST_F(Mp4DemuxFunctionalTests, HandleTruncatedBox) // Either should succeed (graceful handling) or fail with error EXPECT_FALSE(result) << "Truncated box should be handled with error"; EXPECT_NE(mDemuxer->GetLastError(), MP4_PARSE_OK) << "Should report error for truncated data"; -} \ No newline at end of file +} + +/* +#include +#include +#include +#include +#include "MP4Demux.h" +#include "DemuxDataTypes.h" +*/ + +// ---- helpers (local) ---- +static void write32be(std::vector& b, uint32_t v) { + b.push_back((v >> 24) & 0xFF); + b.push_back((v >> 16) & 0xFF); + b.push_back((v >> 8) & 0xFF); + b.push_back((v >> 0) & 0xFF); +} +static void write64be(std::vector& b, uint64_t v) { + for (int i = 7; i >= 0; --i) b.push_back(uint8_t((v >> (8*i)) & 0xFF)); +} +static void write4cc(std::vector& b, const char t[4]) { + b.insert(b.end(), t, t+4); +} +struct Box { + std::vector& buf; size_t start{}; bool extended{}; + Box(std::vector& b, const char type[4], bool forceExtended=false) + : buf(b) { + start = buf.size(); + write32be(buf, 0); write4cc(buf, type); + extended = forceExtended; + if (extended) { buf[start+3] = 1; write64be(buf, 0); } + } + void close() { + uint64_t total = buf.size() - start; + if (extended) { + size_t p = start + 8; + for (int i = 7; i >= 0; --i) buf[p + (7-i)] = uint8_t((total >> (8*i)) & 0xFF); + } else { + uint32_t t32 = static_cast(total); + buf[start+0] = (t32 >> 24) & 0xFF; buf[start+1] = (t32 >> 16) & 0xFF; + buf[start+2] = (t32 >> 8) & 0xFF; buf[start+3] = (t32 >> 0) & 0xFF; + } + } +}; +static void writeFullBoxHeader(std::vector& b, uint8_t v, uint32_t f) { + b.push_back(v); b.push_back(uint8_t((f>>16)&0xFF)); b.push_back(uint8_t((f>>8)&0xFF)); b.push_back(uint8_t(f&0xFF)); +} + +// A) Extended-size box (size==1) +TEST(Mp4Demux_Gaps, ExtendedSizeBox) { + std::vector buf; + { Box ftyp(buf, "ftyp"); write4cc(buf,"isom"); write32be(buf,0); write4cc(buf,"isom"); write4cc(buf,"iso2"); ftyp.close(); } + { Box freeBox(buf, "free", /*forceExtended=*/true); freeBox.close(); } + Mp4Demux d; + ASSERT_TRUE(d.Parse(buf.data(), buf.size())) << "Extended-size box should parse cleanly"; // exercises size==1 path in DemuxHelper + EXPECT_EQ(d.GetLastError(), MP4_PARSE_OK); +} + +// B) size==0 mdat (extends to EOF) +TEST(Mp4Demux_Gaps, SizeZeroMdatToEOF) { + std::vector buf; + { Box ftyp(buf, "ftyp"); write4cc(buf,"isom"); write32be(buf,0); write4cc(buf,"isom"); write4cc(buf,"iso2"); ftyp.close(); } + write32be(buf, 0); write4cc(buf, "mdat"); // size == 0 + for (int i=0;i<32;++i) buf.push_back(uint8_t(i)); // payload + Mp4Demux d; + ASSERT_TRUE(d.Parse(buf.data(), buf.size())); + EXPECT_EQ(d.GetLastError(), MP4_PARSE_OK); +} + +// C) ESDS varint (via minimal mp4a+esds) +TEST(Mp4Demux_Gaps, EsdsVarintDecode) { + std::vector buf; + { Box moov(buf, "moov"); + { Box stsd(buf, "stsd"); writeFullBoxHeader(buf,0,0); write32be(buf,1); + { Box mp4a(buf, "mp4a"); + for (int i=0;i<16;++i) buf.push_back(0); + write32be(buf, 0x00020000u); buf.insert(buf.end(), 6, 0); write32be(buf, 0xAC440000u); + { Box esds(buf, "esds"); writeFullBoxHeader(buf,0,0); + buf.push_back(0x03); buf.push_back(0x81); buf.push_back(0x00); // len = 128 (varint) + buf.insert(buf.end(), 3, 0x00); // ES_ID + flags + buf.push_back(0x04); buf.push_back(0x0D); // dec cfg len = 13 + buf.insert(buf.end(), 13, 0); + buf.push_back(0x05); buf.push_back(0x04); // DecoderSpecificInfo len = 4 + buf.push_back(0x11); buf.push_back(0x22); buf.push_back(0x33); buf.push_back(0x44); + buf.push_back(0x06); buf.push_back(0x01); buf.push_back(0x00); + esds.close(); + } + mp4a.close(); + } + stsd.close(); + } + moov.close(); + } + Mp4Demux d; + + ASSERT_TRUE(d.Parse(buf.data(), buf.size())); + auto info = d.GetCodecInfo(); + ASSERT_EQ(info.mCodecData.size(), 4u); + EXPECT_EQ(info.mCodecData[0], 0x11); + EXPECT_EQ(info.mCodecData[3], 0x44); +} + +// D) AC-4 init: ac-4 + dac4 +TEST(Mp4Demux_Gaps, AC4InitHasCodecData) { + std::vector buf; + { Box moov(buf, "moov"); + { Box stsd(buf, "stsd"); writeFullBoxHeader(buf,0,0); write32be(buf,1); + { Box ac4(buf, "ac-4"); + for (int i=0;i<16;++i) buf.push_back(0); + write32be(buf, 0x00020000u); buf.insert(buf.end(), 6, 0); write32be(buf, 0xAC440000u); + { Box dac4(buf, "dac4"); + for (int i=0;i<5;++i) buf.push_back(uint8_t(0x10 + i)); + dac4.close(); + } + ac4.close(); + } + stsd.close(); + } + moov.close(); + } + Mp4Demux d; + ASSERT_TRUE(d.Parse(buf.data(), buf.size())); + auto info = d.GetCodecInfo(); + EXPECT_EQ(info.mCodecFormat, GST_FORMAT_AUDIO_ES_AC4); + ASSERT_EQ(info.mCodecData.size(), 5u); + EXPECT_EQ(info.mCodecData[0], 0x10); + EXPECT_EQ(info.mCodecData[4], 0x14); +} + +// E) TRUN overrun detection (negative) +TEST(Mp4Demux_Gaps, TrunOverrunDetection) { + std::vector buf; + size_t moofStartIdx; + { Box moof(buf, "moof"); moofStartIdx = moof.start; + { Box traf(buf, "traf"); + { Box tfhd(buf, "tfhd"); writeFullBoxHeader(buf,0, 0x00008 | 0x00010); // default dur+size present + write32be(buf, 1); write32be(buf, 90000/30); write32be(buf, 10); tfhd.close(); + } + { Box tfdt(buf, "tfdt"); writeFullBoxHeader(buf,0,0); write32be(buf,0); tfdt.close(); } + { Box trun(buf, "trun"); writeFullBoxHeader(buf,0, 0x0001 | 0x0200); // data_offset + sample_size + write32be(buf, 1); write32be(buf, 0); // placeholder data_offset + write32be(buf, 12); // sample size larger than payload + trun.close(); + } + traf.close(); + } + moof.close(); + } + // small mdat + size_t mdatHdr = buf.size(); + write32be(buf, 16); write4cc(buf, "mdat"); // 8 header + 8 payload + size_t mdatPayload = buf.size(); for (int i=0;i<8;++i) buf.push_back(uint8_t(i)); + // patch trun data_offset to point into mdat payload + size_t trunPos=0; for (size_t i=0;i+3>24)&0xFF); + buf[dataOffsetPos+1] = uint8_t((dataOffset>>16)&0xFF); + buf[dataOffsetPos+2] = uint8_t((dataOffset>>8)&0xFF); + buf[dataOffsetPos+3] = uint8_t((dataOffset>>0)&0xFF); + + Mp4Demux d; + bool ok = d.Parse(buf.data(), buf.size()); + EXPECT_FALSE(ok); + EXPECT_EQ(d.GetLastError(), MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); +} From 7250d0160e86d66be1f7c58052db85d11f1a319e Mon Sep 17 00:00:00 2001 From: Philip Stroffolino Date: Sun, 11 Jan 2026 09:33:04 -0500 Subject: [PATCH 05/11] Reason for Change: fix for ac-4 parsing Signed-off-by: Philip Stroffolino --- mp4demux/MP4Demux.cpp | 94 ++++++---- mp4demux/MP4Demux.h | 3 +- .../Mp4BoxParsingTests/BoxParsingTests.cpp | 169 ++++++++++-------- .../Mp4BoxParsingTests/Mp4DemuxTestData.h | 4 +- 4 files changed, 165 insertions(+), 105 deletions(-) diff --git a/mp4demux/MP4Demux.cpp b/mp4demux/MP4Demux.cpp index 6e04b585f..2e36fb331 100644 --- a/mp4demux/MP4Demux.cpp +++ b/mp4demux/MP4Demux.cpp @@ -94,9 +94,7 @@ constexpr CodecMapping gCodecMappings[] = { { MultiChar_Constant("hvcC"), GST_FORMAT_VIDEO_ES_HEVC }, { MultiChar_Constant("esds"), GST_FORMAT_AUDIO_ES_AAC_RAW }, { MultiChar_Constant("dec3"), GST_FORMAT_AUDIO_ES_EC3 }, - - // AC-4 decoder config box - { MultiChar_Constant("dac4"), GST_FORMAT_AUDIO_ES_AC4 } + { MultiChar_Constant("dac4"), GST_FORMAT_AUDIO_ES_AC4 } // AC-4 decoder config box }; /** @@ -201,7 +199,8 @@ void Mp4Demux::setParseError( Mp4ParseError err ) "UNSUPPORTED_STREAM_FORMAT", "INVALID_ESDS_TAG", "DATA_BOUNDARY_MISMATCH", - "INVALID_INPUT" + "INVALID_INPUT", + "INVALID_KID" }; MP4_LOG_ERR( "%s", text[err] ); } @@ -226,8 +225,8 @@ uint64_t Mp4Demux::ReadBytes(int n) else { for (int i = 0; i < n; i++) - { - rc = (rc<<8) | (*ptr++); + { // accumulate bytes + rc = (rc<<8) + (*ptr++); } } return rc; @@ -339,32 +338,61 @@ void Mp4Demux::ParseSchemeManagementBox() void Mp4Demux::ParseTrackEncryptionBox() { ReadHeader(); - - // Skip: reserved - ptr++; - uint8_t pattern = *ptr++; - if (schemeType == CIPHER_TYPE_CBCS) - { - cryptByteBlock = (pattern >> 4) & 0xf; - skipByteBlock = pattern & 0xf; + // Be robust to both layouts: + // 1) Standard CENC tenc (common): [reserved(2)] [isEncrypted(1)] [ivSize(1)] [KID(16)] [v1: constIV] + // 2) Legacy/variant with a "pattern" byte after one reserved byte (older cbcs flows) + const uint8_t* save = ptr; + + // Try STANDARD layout first + bool ok = true; + if (ptr + 2 > endPtr) { setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); return; } + ptr += 2; // reserved(2) + if (ptr + 2 > endPtr) { setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); return; } + uint8_t isEncrypted = *ptr++; + uint8_t ivSz = *ptr++; + if (ivSz != 8 && ivSz != 16) { + ok = false; // fall back to pattern layout below + } else if (ptr + TENC_BOX_KEY_ID_SIZE > endPtr) { + setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); + return; } - codecInfo.mIsEncrypted = *ptr++; - // This is used to ensure encrypted caps are persisted even if its clear samples - handledEncryptedSamples = true; - ivSize = *ptr++; - - defaultKid = std::vector(ptr, ptr + TENC_BOX_KEY_ID_SIZE); + + if (!ok) { + // Fallback: one reserved + pattern + isEncrypted + ivSize + KID [ + const IV for v1 ] + ptr = save; + if (ptr + 1 > endPtr) { setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); return; } + ptr++; // reserved(1) + if (ptr + 1 > endPtr) { setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); return; } + uint8_t pattern = *ptr++; + if (schemeType == CIPHER_TYPE_CBCS) { + cryptByteBlock = (pattern >> 4) & 0xF; + skipByteBlock = pattern & 0xF; + } + if (ptr + 2 > endPtr) { setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); return; } + isEncrypted = *ptr++; + ivSz = *ptr++; + if (ivSz != 8 && ivSz != 16) { + setParseError(MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE); // best available error code + return; + } + if (ptr + TENC_BOX_KEY_ID_SIZE > endPtr) {setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); return; } + } + + codecInfo.mIsEncrypted = isEncrypted; + handledEncryptedSamples = true; // keep encrypted caps if any encrypted samples were seen + ivSize = ivSz; + defaultKid.assign(ptr, ptr + TENC_BOX_KEY_ID_SIZE); ptr += TENC_BOX_KEY_ID_SIZE; - // Version 1 adds constant IV - if (version == 1) - { + + if (version == 1) { + if (ptr + 1 > endPtr) { setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); return; } constantIvSize = *ptr++; - if (constantIvSize != 8 && constantIvSize != 16) - { + if (constantIvSize != 8 && constantIvSize != 16) { setParseError( MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE ); return; } - constantIv = std::vector(ptr, ptr + constantIvSize); + if (ptr + constantIvSize > endPtr) { setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH);return; } + constantIv.assign(ptr, ptr + constantIvSize); ptr += constantIvSize; } } @@ -409,6 +437,11 @@ void Mp4Demux::ParseProtectionSystemSpecificHeaderBox(const uint8_t *next) return; } uint32_t kidCount = ReadU32(); + if( kidCount > SIZE_MAX / 16 ) + { + setParseError( MP4_PARSE_ERROR_INVALID_KID ); + return; + } size_t kidBytes = static_cast(kidCount) * 16; if (ptr + kidBytes > next) { @@ -983,7 +1016,7 @@ int Mp4Demux::ReadLen() while( ptr endPtr) - { - setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); - return; - } // No need to read this for dec3 box. Expand the filter later if needed. if (type != MultiChar_Constant("dec3")) { @@ -1155,6 +1182,7 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) case MultiChar_Constant("avc1"): case MultiChar_Constant("mp4a"): case MultiChar_Constant("ec-3"): + case MultiChar_Constant("ac-4"): case MultiChar_Constant("enca"): case MultiChar_Constant("encv"): ParseStreamFormatBox(type, next); diff --git a/mp4demux/MP4Demux.h b/mp4demux/MP4Demux.h index 4cca027b2..2d00237e1 100644 --- a/mp4demux/MP4Demux.h +++ b/mp4demux/MP4Demux.h @@ -89,7 +89,8 @@ enum Mp4ParseError MP4_PARSE_ERROR_UNSUPPORTED_STREAM_FORMAT, /**< Unsupported stream format */ MP4_PARSE_ERROR_INVALID_ESDS_TAG, /**< Invalid ESDS tag */ MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH, /**< Data boundary mismatch - referencing invalid memory */ - MP4_PARSE_ERROR_INVALID_INPUT /**< Invalid input to parse function; nullptr or zero length*/ + MP4_PARSE_ERROR_INVALID_INPUT, /**< Invalid input to parse function; nullptr or zero length */ + MP4_PARSE_ERROR_INVALID_KID /**< Invalid (huge) kidCount */ }; /** diff --git a/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp b/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp index 1db7b08d0..e65d9c881 100644 --- a/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp +++ b/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp @@ -20,7 +20,7 @@ /** * @file BoxParsingTests.cpp * @brief Functional tests for MP4 demuxer - * + * * This file validates core MP4 parsing scenarios: * 1. PSSH box parsing and protection events * 2. Initialization segment parsing (codec data, timescale) @@ -46,18 +46,18 @@ class Mp4DemuxFunctionalTests : public ::testing::Test { mDemuxer = new Mp4Demux(); } - + void TearDown() override { delete mDemuxer; } - + Mp4Demux* mDemuxer; }; /** * @brief Test 1: Parse PSSH box and validate GetProtectionEvents() - * + * * Validates that PSSH box parsing correctly extracts DRM protection data * and makes it available through GetProtectionEvents(). */ @@ -84,7 +84,7 @@ TEST_F(Mp4DemuxFunctionalTests, ParsePsshBoxAndValidateProtectionEvents) /** * @brief Test 2: Parse initialization segment and validate codec info - * + * * Validates that moov box parsing correctly extracts: * - Codec configuration data (avcC/hvcC/esds) * - Timescale from mvhd or mdhd @@ -102,10 +102,10 @@ TEST_F(Mp4DemuxFunctionalTests, ParseInitSegmentAndValidateCodecData) uint32_t timescale = mDemuxer->GetTimeScale(); EXPECT_GT(timescale, 0) << "Timescale should be greater than 0"; EXPECT_EQ(timescale, 30000) << "Timescale from MDHD should be 30000"; - + auto samples = mDemuxer->GetSamples(); EXPECT_EQ(samples.size(), 0) << "Sample count should be zero"; - + auto codecInfo = mDemuxer->GetCodecInfo(); EXPECT_EQ(codecInfo.mCodecFormat, GST_FORMAT_VIDEO_ES_H264) << "Codec format should be H.264"; EXPECT_FALSE(codecInfo.mCodecData.empty()) << "Codec data (avcC) should not be empty"; @@ -115,7 +115,7 @@ TEST_F(Mp4DemuxFunctionalTests, ParseInitSegmentAndValidateCodecData) /** * @brief Test 3: Parse fragment and validate sample extraction - * + * * Validates that moof/mdat parsing correctly extracts: * - Sample count matches expected * - Sample data is present @@ -127,7 +127,7 @@ TEST_F(Mp4DemuxFunctionalTests, ParseFragmentAndValidateSamples) bool result = mDemuxer->Parse(initSegmentWithAvcC, sizeof(initSegmentWithAvcC)); EXPECT_TRUE(result) << "Parse should succeed for valid init segment"; EXPECT_EQ(mDemuxer->GetLastError(), MP4_PARSE_OK); - + // Parse fragment with moof and mdat containing 2 samples result = mDemuxer->Parse(fragmentWithSamples, sizeof(fragmentWithSamples)); EXPECT_TRUE(result) << "Parse should succeed for valid fragment"; @@ -143,20 +143,20 @@ TEST_F(Mp4DemuxFunctionalTests, ParseFragmentAndValidateSamples) EXPECT_EQ(samples[0].mData.GetLen(), 32) << "Sample 0 should be 32 bytes"; EXPECT_EQ(samples[0].mPts, 0) << "Sample 0 PTS should be 0"; EXPECT_EQ(samples[0].mDts, 0) << "Sample 0 DTS should be 0"; - EXPECT_EQ(samples[0].mDuration, 0.1) << "Sample 0 duration should be 0.1"; + EXPECT_NEAR(samples[0].mDuration, 0.1,1e-6) << "Sample 0 duration should be 0.1"; EXPECT_FALSE(samples[0].mDrmMetadata.mIsEncrypted) << "Sample 0 should not be encrypted"; // Validate Sample 1 EXPECT_EQ(samples[1].mData.GetLen(), 64) << "Sample 1 should be 64 bytes"; - EXPECT_EQ(samples[1].mPts, 0.1) << "Sample 1 PTS should be 0.1"; + EXPECT_NEAR(samples[1].mPts, 0.1, 1e-6) << "Sample 1 PTS should be 0.1"; EXPECT_EQ(samples[1].mDts, 0.1) << "Sample 1 DTS should be 0.1"; - EXPECT_EQ(samples[1].mDuration, 0.1) << "Sample 1 duration should be 0.1"; + EXPECT_NEAR(samples[1].mDuration, 0.1, 1e-6) << "Sample 1 duration should be 0.1"; EXPECT_FALSE(samples[1].mDrmMetadata.mIsEncrypted) << "Sample 1 should not be encrypted"; } /** * @brief Test 4: Parse encrypted fragment with SENC and validate DRM metadata - * + * * Validates that encrypted fragments with SENC box correctly populate: * - mIsEncrypted flag * - mKeyId (default KID) @@ -193,7 +193,7 @@ TEST_F(Mp4DemuxFunctionalTests, ParseEncryptedFragmentWithSencBox) /** * @brief Test 5: Parse encrypted fragment with SAIO/SAIZ and validate DRM metadata - * + * * Validates that encrypted fragments with SAIO/SAIZ boxes correctly: * - Parse auxiliary information offsets * - Parse auxiliary information sizes @@ -215,7 +215,7 @@ TEST_F(Mp4DemuxFunctionalTests, ParseEncryptedFragmentWithSaioSaizBoxes) EXPECT_FALSE(samples[0].mDrmMetadata.mCipher == CIPHER_TYPE_NONE) << "Sample should have cipher type"; EXPECT_EQ(samples[0].mDrmMetadata.mSubSamples.size(), 6) << "Sample should have subsample encryption data"; EXPECT_EQ(samples[0].mDrmMetadata.mNumSubSamples, 1) << "Sample should have 1 subsamples"; - + EXPECT_TRUE(samples[1].mDrmMetadata.mIsEncrypted) << "Sample should be marked as encrypted"; EXPECT_FALSE(samples[1].mDrmMetadata.mKeyId.empty()) << "Sample should have Key ID"; EXPECT_FALSE(samples[1].mDrmMetadata.mIV.empty()) << "Sample should have IV"; @@ -264,15 +264,6 @@ TEST_F(Mp4DemuxFunctionalTests, HandleTruncatedBox) EXPECT_NE(mDemuxer->GetLastError(), MP4_PARSE_OK) << "Should report error for truncated data"; } -/* -#include -#include -#include -#include -#include "MP4Demux.h" -#include "DemuxDataTypes.h" -*/ - // ---- helpers (local) ---- static void write32be(std::vector& b, uint32_t v) { b.push_back((v >> 24) & 0xFF); @@ -280,6 +271,7 @@ static void write32be(std::vector& b, uint32_t v) { b.push_back((v >> 8) & 0xFF); b.push_back((v >> 0) & 0xFF); } + static void write64be(std::vector& b, uint64_t v) { for (int i = 7; i >= 0; --i) b.push_back(uint8_t((v >> (8*i)) & 0xFF)); } @@ -289,7 +281,7 @@ static void write4cc(std::vector& b, const char t[4]) { struct Box { std::vector& buf; size_t start{}; bool extended{}; Box(std::vector& b, const char type[4], bool forceExtended=false) - : buf(b) { + : buf(b) { start = buf.size(); write32be(buf, 0); write4cc(buf, type); extended = forceExtended; @@ -311,6 +303,12 @@ static void writeFullBoxHeader(std::vector& b, uint8_t v, uint32_t f) { b.push_back(v); b.push_back(uint8_t((f>>16)&0xFF)); b.push_back(uint8_t((f>>8)&0xFF)); b.push_back(uint8_t(f&0xFF)); } +// helper for 16-bit BE +static inline void write16be(std::vector& b, uint16_t v) { + b.push_back(uint8_t((v>>8)&0xFF)); + b.push_back(uint8_t(v&0xFF)); +} + // A) Extended-size box (size==1) TEST(Mp4Demux_Gaps, ExtendedSizeBox) { std::vector buf; @@ -336,25 +334,32 @@ TEST(Mp4Demux_Gaps, SizeZeroMdatToEOF) { TEST(Mp4Demux_Gaps, EsdsVarintDecode) { std::vector buf; { Box moov(buf, "moov"); - { Box stsd(buf, "stsd"); writeFullBoxHeader(buf,0,0); write32be(buf,1); - { Box mp4a(buf, "mp4a"); - for (int i=0;i<16;++i) buf.push_back(0); - write32be(buf, 0x00020000u); buf.insert(buf.end(), 6, 0); write32be(buf, 0xAC440000u); - { Box esds(buf, "esds"); writeFullBoxHeader(buf,0,0); - buf.push_back(0x03); buf.push_back(0x81); buf.push_back(0x00); // len = 128 (varint) - buf.insert(buf.end(), 3, 0x00); // ES_ID + flags - buf.push_back(0x04); buf.push_back(0x0D); // dec cfg len = 13 - buf.insert(buf.end(), 13, 0); - buf.push_back(0x05); buf.push_back(0x04); // DecoderSpecificInfo len = 4 - buf.push_back(0x11); buf.push_back(0x22); buf.push_back(0x33); buf.push_back(0x44); - buf.push_back(0x06); buf.push_back(0x01); buf.push_back(0x00); - esds.close(); - } - mp4a.close(); + { Box stsd(buf, "stsd"); writeFullBoxHeader(buf,0,0); write32be(buf,1); + { Box mp4a(buf, "mp4a"); + // reserved[6] + data_reference_index(2) + reserved[8] + for (int i=0;i<16;++i) buf.push_back(0); + // channel_count(2) = 2 + write16be(buf, 2); + // sample_size(2) + pre_defined/reserved(4) -> 6 bytes + buf.insert(buf.end(), 6, 0); + // sample_rate 16.16: upper16 = 0xAC44 (~44100), lower16 = 0 + write16be(buf, 0xAC44); + write16be(buf, 0x0000); + { Box esds(buf, "esds"); writeFullBoxHeader(buf,0,0); + buf.push_back(0x03); buf.push_back(0x81); buf.push_back(0x00); // len = 128 (varint) + buf.insert(buf.end(), 3, 0x00); // ES_ID + flags + buf.push_back(0x04); buf.push_back(0x0D); // dec cfg len = 13 + buf.insert(buf.end(), 13, 0); + buf.push_back(0x05); buf.push_back(0x04); // DecoderSpecificInfo len = 4 + buf.push_back(0x11); buf.push_back(0x22); buf.push_back(0x33); buf.push_back(0x44); + buf.push_back(0x06); buf.push_back(0x01); buf.push_back(0x00); + esds.close(); + } + mp4a.close(); + } + stsd.close(); } - stsd.close(); - } - moov.close(); + moov.close(); } Mp4Demux d; @@ -368,24 +373,47 @@ TEST(Mp4Demux_Gaps, EsdsVarintDecode) { // D) AC-4 init: ac-4 + dac4 TEST(Mp4Demux_Gaps, AC4InitHasCodecData) { std::vector buf; - { Box moov(buf, "moov"); - { Box stsd(buf, "stsd"); writeFullBoxHeader(buf,0,0); write32be(buf,1); - { Box ac4(buf, "ac-4"); - for (int i=0;i<16;++i) buf.push_back(0); - write32be(buf, 0x00020000u); buf.insert(buf.end(), 6, 0); write32be(buf, 0xAC440000u); - { Box dac4(buf, "dac4"); - for (int i=0;i<5;++i) buf.push_back(uint8_t(0x10 + i)); - dac4.close(); - } - ac4.close(); + // Build a minimal init segment: moov -> stsd (1 entry) -> ac-4 (AudioSampleEntry) -> dac4 (decoder-specific) + { + Box moov(buf, "moov"); + { + Box stsd(buf, "stsd"); + writeFullBoxHeader(buf, /*version*/0, /*flags*/0); + write32be(buf, /*entry_count*/1); + { + Box ac4(buf, "ac-4"); // AudioSampleEntry per ISO/IEC 14496-12 + + // reserved[6] + data_reference_index(2) + reserved[8] = 16 bytes + for (int i=0; i<16; ++i) buf.push_back(0x00); + + // channel_count(2) = 2 + write16be(buf, 2); + + // sample_size(2) + pre_defined(2) + reserved(2) = 6 bytes + buf.insert(buf.end(), 6, 0x00); + + // sample_rate (32-bit fixed-point 16.16): upper16 = 0xAC44 (~44100), lower16 = 0x0000 + write16be(buf, 0xAC44); + write16be(buf, 0x0000); + + // Decoder-specific AC-4 box: 'dac4' with 5 bytes of payload + { + Box dac4(buf, "dac4"); + for (int i=0; i<5; ++i) buf.push_back(uint8_t(0x10 + i)); + dac4.close(); + } + ac4.close(); + } + stsd.close(); } - stsd.close(); - } - moov.close(); + moov.close(); } + + // Parse and validate Mp4Demux d; ASSERT_TRUE(d.Parse(buf.data(), buf.size())); auto info = d.GetCodecInfo(); + EXPECT_EQ(info.mCodecFormat, GST_FORMAT_AUDIO_ES_AC4); ASSERT_EQ(info.mCodecData.size(), 5u); EXPECT_EQ(info.mCodecData[0], 0x10); @@ -397,22 +425,22 @@ TEST(Mp4Demux_Gaps, TrunOverrunDetection) { std::vector buf; size_t moofStartIdx; { Box moof(buf, "moof"); moofStartIdx = moof.start; - { Box traf(buf, "traf"); - { Box tfhd(buf, "tfhd"); writeFullBoxHeader(buf,0, 0x00008 | 0x00010); // default dur+size present - write32be(buf, 1); write32be(buf, 90000/30); write32be(buf, 10); tfhd.close(); + { Box traf(buf, "traf"); + { Box tfhd(buf, "tfhd"); writeFullBoxHeader(buf,0, 0x00008 | 0x00010); // default dur+size present + write32be(buf, 1); write32be(buf, 90000/30); write32be(buf, 10); tfhd.close(); + } + { Box tfdt(buf, "tfdt"); writeFullBoxHeader(buf,0,0); write32be(buf,0); tfdt.close(); } + { Box trun(buf, "trun"); writeFullBoxHeader(buf,0, 0x0001 | 0x0200); // data_offset + sample_size + write32be(buf, 1); write32be(buf, 0); // placeholder data_offset + write32be(buf, 12); // sample size larger than payload + trun.close(); + } + traf.close(); } - { Box tfdt(buf, "tfdt"); writeFullBoxHeader(buf,0,0); write32be(buf,0); tfdt.close(); } - { Box trun(buf, "trun"); writeFullBoxHeader(buf,0, 0x0001 | 0x0200); // data_offset + sample_size - write32be(buf, 1); write32be(buf, 0); // placeholder data_offset - write32be(buf, 12); // sample size larger than payload - trun.close(); - } - traf.close(); - } - moof.close(); + moof.close(); } // small mdat - size_t mdatHdr = buf.size(); + //size_t mdatHdr = buf.size(); write32be(buf, 16); write4cc(buf, "mdat"); // 8 header + 8 payload size_t mdatPayload = buf.size(); for (int i=0;i<8;++i) buf.push_back(uint8_t(i)); // patch trun data_offset to point into mdat payload @@ -424,9 +452,10 @@ TEST(Mp4Demux_Gaps, TrunOverrunDetection) { buf[dataOffsetPos+1] = uint8_t((dataOffset>>16)&0xFF); buf[dataOffsetPos+2] = uint8_t((dataOffset>>8)&0xFF); buf[dataOffsetPos+3] = uint8_t((dataOffset>>0)&0xFF); - + Mp4Demux d; bool ok = d.Parse(buf.data(), buf.size()); EXPECT_FALSE(ok); EXPECT_EQ(d.GetLastError(), MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); } + diff --git a/test/utests/tests/Mp4BoxParsingTests/Mp4DemuxTestData.h b/test/utests/tests/Mp4BoxParsingTests/Mp4DemuxTestData.h index c255beb66..393cd0f09 100644 --- a/test/utests/tests/Mp4BoxParsingTests/Mp4DemuxTestData.h +++ b/test/utests/tests/Mp4BoxParsingTests/Mp4DemuxTestData.h @@ -798,7 +798,9 @@ static const uint8_t encryptedFragmentWithSenc[] = { 0x00, // version = 0 0x00, 0x03, 0x01, // flags = 0x000301 (data-offset, sample-duration, sample-size present) 0x00, 0x00, 0x00, 0x02, // sample_count = 2 - 0x00, 0x00, 0x02, 0xD5, // data_offset = 725 (from start of moof: 555 moov + 170 moof + 8 mdat header - 8 moof header) + + 0x00,0x00,0x00,0xB2, // data_offset = 178 (from start of moof) + // Sample 0 0x00, 0x00, 0x03, 0xE8, // sample_duration = 1000 0x00, 0x00, 0x00, 0x40, // sample_size = 64 From 6d8013573e23f2fef5d310720c43a5f7371f452a Mon Sep 17 00:00:00 2001 From: Philip Stroffolino Date: Sun, 11 Jan 2026 13:03:50 -0500 Subject: [PATCH 06/11] Readon for Change: more copilot nitpicks Signed-off-by: Philip Stroffolino --- mp4demux/MP4Demux.cpp | 88 ++++++++++++++----- mp4demux/MP4Demux.h | 6 +- .../Mp4BoxParsingTests/BoxParsingTests.cpp | 8 +- 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/mp4demux/MP4Demux.cpp b/mp4demux/MP4Demux.cpp index 2e36fb331..7b6bcdc61 100644 --- a/mp4demux/MP4Demux.cpp +++ b/mp4demux/MP4Demux.cpp @@ -187,22 +187,56 @@ Mp4Demux::~Mp4Demux() void Mp4Demux::setParseError( Mp4ParseError err ) { parseError = err; - const char *text[] = + const char *text = nullptr; + switch( err ) { - "OK", - "INVALID_BOX", - "INVALID_CONSTANT_IV_SIZE", - "SAMPLE_COUNT_MISMATCH", - "UNSUPPORTED_ENCRYPTION_SCHEME", - "INVALID_PADDING", - "UNSUPPORTED_SAMPLE_ENTRY_COUNT", - "UNSUPPORTED_STREAM_FORMAT", - "INVALID_ESDS_TAG", - "DATA_BOUNDARY_MISMATCH", - "INVALID_INPUT", - "INVALID_KID" - }; - MP4_LOG_ERR( "%s", text[err] ); + default: + text = "unknown parse error"; + break; + case MP4_PARSE_OK: + text = "unexpected setParseError(PARSE_OK)"; + break; + case MP4_PARSE_ERROR_INVALID_BOX: + text = "INVALID_BOX"; + break; + case MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE: + text = "INVALID_CONSTANT_IV_SIZE"; + break; + case MP4_PARSE_ERROR_SAMPLE_COUNT_MISMATCH: + text = "SAMPLE_COUNT_MISMATCH"; + break; + case MP4_PARSE_ERROR_UNSUPPORTED_ENCRYPTION_SCHEME: + text = "UNSUPPORTED_ENCRYPTION_SCHEME"; + break; + case MP4_PARSE_ERROR_INVALID_PADDING: + text = "INVALID_PADDING"; + break; + case MP4_PARSE_ERROR_UNSUPPORTED_SAMPLE_ENTRY_COUNT: + text = "UNSUPPORTED_SAMPLE_ENTRY_COUNT"; + break; + case MP4_PARSE_ERROR_UNSUPPORTED_STREAM_FORMAT: + text = "UNSUPPORTED_STREAM_FORMAT"; + break; + case MP4_PARSE_ERROR_INVALID_ESDS_TAG: + text = "INVALID_ESDS_TAG"; + break; + case MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH: + text = "DATA_BOUNDARY_MISMATCH"; + break; + case MP4_PARSE_ERROR_INVALID_INPUT: + text = "INVALID_INPUT"; + break; + case MP4_PARSE_ERROR_INVALID_KID: + text = "INVALID_KID"; + break; + case MP4_PARSE_ERROR_INVALID_ENTRY_COUNT: + text = "INVALID_ENTRY_COUNT"; + break; + case MP4_PARSE_ERROR_VARIABLE_LENGTH_OVERFLOW: + text = "VARIABLE_LENGTH_OVERFLOW"; + break; + } + MP4_LOG_ERR( "%s", text ); } /** @@ -618,7 +652,7 @@ void Mp4Demux::ParseSampleAuxiliaryInformationOffsets() uint32_t entryCount = ReadU32(); if (entryCount == 0) { - setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); + setParseError( MP4_PARSE_ERROR_INVALID_ENTRY_COUNT ); return; } // Read the first offset; if multiple, warn and consume others @@ -947,14 +981,15 @@ void Mp4Demux::ParseSampleDescriptionBox(const uint8_t *next) { ReadHeader(); uint32_t count = ReadU32(); - // Be tolerant: parse child boxes regardless of count; warn if 0 or >1 + if( parseError != MP4_PARSE_OK ) return; + // warn if 0 or >1 if (count == 0) { setParseError( MP4_PARSE_ERROR_UNSUPPORTED_SAMPLE_ENTRY_COUNT ); return; } if (count > 1) { - MP4_LOG_WARN("stsd count=%u; parsing entries and using first supported one", count); + MP4_LOG_WARN("unexpected stsd count=%u", count ); } // Parse contained sample entries/config boxes DemuxHelper(next); @@ -1008,15 +1043,22 @@ void Mp4Demux::ParseStreamFormatBox(uint32_t type, const uint8_t *next) * * @return Length value decoded from variable-length encoding */ -int Mp4Demux::ReadLen() +uint32_t Mp4Demux::ReadLen() { if( ptr && endPtr ) { - int rc = 0; + uint32_t rc = 0; + int bits = 0; while( ptr 32 ) + { + setParseError( MP4_PARSE_ERROR_VARIABLE_LENGTH_OVERFLOW ); + return; + } if ((octet & 0x80) == 0) { return rc; @@ -1143,11 +1185,13 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) { uint64_t size = ReadU32(); uint32_t type = ReadU32(); + if( parseError !=MP4_PARSE_OK ) return; const uint8_t *next = nullptr; if( size==1 ) { // size includes size(4)+type(4)+large_size(8) size = ReadU64(); - if (size < 16) + if( parseError !=MP4_PARSE_OK ) return; + if ( size < 16) { setParseError( MP4_PARSE_ERROR_INVALID_BOX ); return; @@ -1155,7 +1199,7 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) next = ptr + (size - 16); } else if( size == 0 ) - { // box extends to end of buffer (common + { // box extends to end of buffer next = fin; } else if( size<8 ) diff --git a/mp4demux/MP4Demux.h b/mp4demux/MP4Demux.h index 2d00237e1..901ee058e 100644 --- a/mp4demux/MP4Demux.h +++ b/mp4demux/MP4Demux.h @@ -90,7 +90,9 @@ enum Mp4ParseError MP4_PARSE_ERROR_INVALID_ESDS_TAG, /**< Invalid ESDS tag */ MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH, /**< Data boundary mismatch - referencing invalid memory */ MP4_PARSE_ERROR_INVALID_INPUT, /**< Invalid input to parse function; nullptr or zero length */ - MP4_PARSE_ERROR_INVALID_KID /**< Invalid (huge) kidCount */ + MP4_PARSE_ERROR_INVALID_KID, /**< Invalid (huge) kidCount */ + MP4_PARSE_ERROR_INVALID_ENTRY_COUNT, /**< Entry count is zero */ + MP4_PARSE_ERROR_VARIABLE_LENGTH_OVERFLOW /**< Value encoded using octets exceed 32 bits */ }; /** @@ -331,7 +333,7 @@ class Mp4Demux * @brief Read length field with variable encoding * @return Length value */ - int ReadLen(); + uint32_t ReadLen(); /** * @brief Parse codec configuration helper diff --git a/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp b/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp index e65d9c881..870e53657 100644 --- a/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp +++ b/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp @@ -141,15 +141,15 @@ TEST_F(Mp4DemuxFunctionalTests, ParseFragmentAndValidateSamples) // Validate Sample 0 EXPECT_EQ(samples[0].mData.GetLen(), 32) << "Sample 0 should be 32 bytes"; - EXPECT_EQ(samples[0].mPts, 0) << "Sample 0 PTS should be 0"; - EXPECT_EQ(samples[0].mDts, 0) << "Sample 0 DTS should be 0"; - EXPECT_NEAR(samples[0].mDuration, 0.1,1e-6) << "Sample 0 duration should be 0.1"; + EXPECT_NEAR(samples[0].mPts, 0.0, 1e-6) << "Sample 0 PTS should be 0"; + EXPECT_NEAR(samples[0].mDts, 0.0, 1e-6) << "Sample 0 DTS should be 0"; + EXPECT_NEAR(samples[0].mDuration, 0.1, 1e-6) << "Sample 0 duration should be 0.1"; EXPECT_FALSE(samples[0].mDrmMetadata.mIsEncrypted) << "Sample 0 should not be encrypted"; // Validate Sample 1 EXPECT_EQ(samples[1].mData.GetLen(), 64) << "Sample 1 should be 64 bytes"; EXPECT_NEAR(samples[1].mPts, 0.1, 1e-6) << "Sample 1 PTS should be 0.1"; - EXPECT_EQ(samples[1].mDts, 0.1) << "Sample 1 DTS should be 0.1"; + EXPECT_NEAR(samples[1].mDts, 0.1, 1e-6) << "Sample 1 DTS should be 0.1"; EXPECT_NEAR(samples[1].mDuration, 0.1, 1e-6) << "Sample 1 duration should be 0.1"; EXPECT_FALSE(samples[1].mDrmMetadata.mIsEncrypted) << "Sample 1 should not be encrypted"; } From b0088101dafc2edbbfb75b455a715c04d4cbe57c Mon Sep 17 00:00:00 2001 From: Philip Stroffolino Date: Sun, 11 Jan 2026 14:19:23 -0500 Subject: [PATCH 07/11] Reason for Change: beefed up tests Signed-off-by: Philip Stroffolino --- mp4demux/MP4Demux.cpp | 30 +- mp4demux/MP4Demux.h | 2 +- .../Mp4BoxParsingTests/BoxParsingTests.cpp | 2 +- .../tests/Mp4BoxParsingTests/CMakeLists.txt | 1 + .../Mp4BoxParsingTests/EsdsReadLenTests.cpp | 684 ++++++++++++++++++ .../Mp4BoxParsingTests/FourCCMappingTest.cpp | 65 +- .../Mp4BoxParsingTests/Mp4BoxParsingTests.cpp | 2 +- 7 files changed, 740 insertions(+), 46 deletions(-) create mode 100644 test/utests/tests/Mp4BoxParsingTests/EsdsReadLenTests.cpp diff --git a/mp4demux/MP4Demux.cpp b/mp4demux/MP4Demux.cpp index 7b6bcdc61..56a16d882 100644 --- a/mp4demux/MP4Demux.cpp +++ b/mp4demux/MP4Demux.cpp @@ -2,7 +2,7 @@ * If not stated otherwise in this file or this component's license file the * following copyright and licenses apply: * - * Copyright 2025 RDK Management + * Copyright 2026 RDK Management * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ #define SENC_SUBSAMPLE_ENCRYPTION_PRESENT 0x2 // Video sample entry padding marker -#define VIDEO_PADDING_MARKER 0xffff +#define VIDEO_PREDEFINED_PADDING_MARKER 0xffff // Elementary Stream Descriptor (ESDS) tag values #define ESDS_TAG_ES_DESCRIPTOR 0x03 @@ -199,8 +199,8 @@ void Mp4Demux::setParseError( Mp4ParseError err ) case MP4_PARSE_ERROR_INVALID_BOX: text = "INVALID_BOX"; break; - case MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE: - text = "INVALID_CONSTANT_IV_SIZE"; + case MP4_PARSE_ERROR_INVALID_IV_SIZE: + text = "INVALID_IV_SIZE"; break; case MP4_PARSE_ERROR_SAMPLE_COUNT_MISMATCH: text = "SAMPLE_COUNT_MISMATCH"; @@ -406,7 +406,7 @@ void Mp4Demux::ParseTrackEncryptionBox() isEncrypted = *ptr++; ivSz = *ptr++; if (ivSz != 8 && ivSz != 16) { - setParseError(MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE); // best available error code + setParseError(MP4_PARSE_ERROR_INVALID_IV_SIZE); // best available error code return; } if (ptr + TENC_BOX_KEY_ID_SIZE > endPtr) {setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); return; } @@ -422,7 +422,7 @@ void Mp4Demux::ParseTrackEncryptionBox() if (ptr + 1 > endPtr) { setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); return; } constantIvSize = *ptr++; if (constantIvSize != 8 && constantIvSize != 16) { - setParseError( MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE ); + setParseError( MP4_PARSE_ERROR_INVALID_IV_SIZE ); return; } if (ptr + constantIvSize > endPtr) { setParseError(MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH);return; } @@ -471,12 +471,15 @@ void Mp4Demux::ParseProtectionSystemSpecificHeaderBox(const uint8_t *next) return; } uint32_t kidCount = ReadU32(); +#if SIZE_MAX <= 0xffffffff + // if size_t is 64 bit, check below will never happen if( kidCount > SIZE_MAX / 16 ) - { + { // sanity in case kidCount*16 would overflow size_t setParseError( MP4_PARSE_ERROR_INVALID_KID ); return; } - size_t kidBytes = static_cast(kidCount) * 16; +#endif + size_t kidBytes = 16*static_cast(kidCount); if (ptr + kidBytes > next) { setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); @@ -873,7 +876,7 @@ void Mp4Demux::ParseVideoInformation() // Skip: horizontal_resolution (4) + vertical_resolution (4) + reserved (4) + frame_count (2) + compressor_name (32) + depth (2) SkipBytes(48); int pad = ReadU16(); - if (pad != VIDEO_PADDING_MARKER) + if (pad != VIDEO_PREDEFINED_PADDING_MARKER) { setParseError( MP4_PARSE_ERROR_INVALID_PADDING ); return; @@ -1057,7 +1060,7 @@ uint32_t Mp4Demux::ReadLen() if( bits > 32 ) { setParseError( MP4_PARSE_ERROR_VARIABLE_LENGTH_OVERFLOW ); - return; + return 0; } if ((octet & 0x80) == 0) { @@ -1221,6 +1224,11 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) switch (type) { + case MultiChar_Constant("free"): + // ISO BMFF padding box containing unused space + ptr = next; + break; + case MultiChar_Constant("hev1"): case MultiChar_Constant("hvc1"): case MultiChar_Constant("avc1"): @@ -1406,7 +1414,7 @@ bool Mp4Demux::Parse(const void *ptr, size_t len) cencAuxInfoSizes.clear(); protectionData.clear(); gotAuxiliaryInformationOffset = false; - moofPtr = NULL; + moofPtr = nullptr; endPtr = &((const uint8_t*)ptr)[len]; mdatStart = nullptr; mdatEnd = nullptr; diff --git a/mp4demux/MP4Demux.h b/mp4demux/MP4Demux.h index 901ee058e..37583f1b5 100644 --- a/mp4demux/MP4Demux.h +++ b/mp4demux/MP4Demux.h @@ -81,7 +81,7 @@ enum Mp4ParseError { MP4_PARSE_OK, /**< No error */ MP4_PARSE_ERROR_INVALID_BOX, /**< Invalid box header size */ - MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE, /**< Invalid constant IV size (expected 8 or 16) */ + MP4_PARSE_ERROR_INVALID_IV_SIZE, /**< Invalid IV size (expected 8 or 16) */ MP4_PARSE_ERROR_SAMPLE_COUNT_MISMATCH, /**< Explicit sample count doesn't match implicit sample count */ MP4_PARSE_ERROR_UNSUPPORTED_ENCRYPTION_SCHEME, /**< Expected cenc or cbcs */ MP4_PARSE_ERROR_INVALID_PADDING, /**< Unexpected Video Padding field (should be 0xffff) */ diff --git a/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp b/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp index 870e53657..e7370353f 100644 --- a/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp +++ b/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp @@ -2,7 +2,7 @@ * If not stated otherwise in this file or this component's license file the * following copyright and licenses apply: * - * Copyright 2025 RDK Management + * Copyright 2026 RDK Management * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/utests/tests/Mp4BoxParsingTests/CMakeLists.txt b/test/utests/tests/Mp4BoxParsingTests/CMakeLists.txt index 2e8f7b8d4..3c967cb79 100644 --- a/test/utests/tests/Mp4BoxParsingTests/CMakeLists.txt +++ b/test/utests/tests/Mp4BoxParsingTests/CMakeLists.txt @@ -23,6 +23,7 @@ set(TEST_SOURCES BoxParsingTests.cpp Mp4BoxParsingTests.cpp FourCCMappingTest.cpp + EsdsReadLenTests.cpp ) set(AAMP_SOURCES diff --git a/test/utests/tests/Mp4BoxParsingTests/EsdsReadLenTests.cpp b/test/utests/tests/Mp4BoxParsingTests/EsdsReadLenTests.cpp new file mode 100644 index 000000000..cf9b1ab0c --- /dev/null +++ b/test/utests/tests/Mp4BoxParsingTests/EsdsReadLenTests.cpp @@ -0,0 +1,684 @@ +/* + * If not stated otherwise in this file or this component's license file the + * following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @file EsdsReadLenTests.cpp + * @brief Tests for ESDS variable-length parsing via Mp4Demux::Parse() + */ +#include +#include +#include +#include +#include +#include "MP4Demux.h" + +// Utility: append a big-endian 32-bit value +static void append_u32(std::vector& buf, uint32_t v) +{ + buf.push_back(static_cast((v >> 24) & 0xFF)); + buf.push_back(static_cast((v >> 16) & 0xFF)); + buf.push_back(static_cast((v >> 8) & 0xFF)); + buf.push_back(static_cast(v & 0xFF)); +} + +// Utility: append a FourCC from ASCII string of length 4 +static void append_type(std::vector& buf, const char type[4]) +{ + buf.insert(buf.end(), type, type + 4); +} + +// Utility: encode ISO BMFF descriptor length using base-128 big-endian varint +static std::vector encode_len(uint32_t value) +{ + std::vector out; + // Collect 7-bit groups in reverse + uint32_t v = value; + std::vector groups; + do { + groups.push_back(static_cast(v & 0x7F)); + v >>= 7; + } while (v != 0); + // Emit big-endian with continuation bits + for (size_t i = groups.size(); i-- > 0; ) + { + uint8_t b = groups[i]; + if (i != 0) b |= 0x80; // set continuation for all but last + out.push_back(b); + } + return out; +} + +// Build an ESDS box payload with given DecoderSpecificInfo length field bytes and payload size +static std::vector build_esds_payload(const std::vector& decSpecLenBytes, + size_t decSpecPayloadSize) +{ + std::vector esdsPayload; + // FullBox header: version(1) + flags(3) + esdsPayload.insert(esdsPayload.end(), {0x00, 0x00, 0x00, 0x00}); + + // 0x03 ES_Descriptor: minimal content length 3 (ES_ID(2) + flags(1)) + esdsPayload.push_back(0x03); // tag + auto len03 = encode_len(3); + esdsPayload.insert(esdsPayload.end(), len03.begin(), len03.end()); + esdsPayload.push_back(0x00); // ES_ID MSB + esdsPayload.push_back(0x02); // ES_ID LSB + esdsPayload.push_back(0x00); // flags + + // 0x04 DecoderConfigDescriptor: length 13, contents don't matter for parser + esdsPayload.push_back(0x04); // tag + auto len04 = encode_len(13); + esdsPayload.insert(esdsPayload.end(), len04.begin(), len04.end()); + esdsPayload.push_back(0x40); // objectTypeIndication (AAC LC) + esdsPayload.push_back(0x15); // streamType(6) + upStream(1) + reserved(1) + esdsPayload.push_back(0x00); // bufferSizeDB[24] + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); // maxBitrate[32] + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); // avgBitrate[32] + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + + // 0x05 DecoderSpecificInfo: variable length provided by decSpecLenBytes + esdsPayload.push_back(0x05); + esdsPayload.insert(esdsPayload.end(), decSpecLenBytes.begin(), decSpecLenBytes.end()); + // payload bytes (e.g., AudioSpecificConfig) + for (size_t i = 0; i < decSpecPayloadSize; ++i) + { + esdsPayload.push_back(static_cast(i & 0xFF)); + } + + // 0x06 SLConfigDescriptor: length 1, one dummy byte + esdsPayload.push_back(0x06); + auto len06 = encode_len(1); + esdsPayload.insert(esdsPayload.end(), len06.begin(), len06.end()); + esdsPayload.push_back(0x02); + + return esdsPayload; +} + +// Optional tail box ('free') with arbitrary payload to keep overall buffer large enough +static std::vector build_free_box(uint32_t payloadSize) +{ + std::vector box; + uint32_t size = 8 + payloadSize; + append_u32(box, size); + append_type(box, "free"); + box.resize(size, 0x00); + return box; +} + +// Build an mp4a sample entry with provided ESDS child payload and optional tail 'free' box +static std::vector build_mp4a_box(const std::vector& esdsPayload, + uint32_t tailFreePayloadSize = 0) +{ + std::vector mp4aPayload; + // reserved[6] + for (int i = 0; i < 6; ++i) mp4aPayload.push_back(0x00); + // data_reference_index (u16) + mp4aPayload.push_back(0x00); + mp4aPayload.push_back(0x00); + // reserved[2] (8 bytes) + for (int i = 0; i < 8; ++i) mp4aPayload.push_back(0x00); + // channelcount (u16) + mp4aPayload.push_back(0x00); + mp4aPayload.push_back(0x02); + // sample_size (u16) + mp4aPayload.push_back(0x00); + mp4aPayload.push_back(0x10); + // pre_defined/reserved (u32) + mp4aPayload.push_back(0x00); + mp4aPayload.push_back(0x00); + mp4aPayload.push_back(0x00); + mp4aPayload.push_back(0x00); + // sample_rate (32-bit 16.16 fixed): 48000.0 -> 0xBB80 0000 + mp4aPayload.push_back(0xBB); + mp4aPayload.push_back(0x80); + mp4aPayload.push_back(0x00); + mp4aPayload.push_back(0x00); + + // Child box: esds + std::vector esdsBox; + // size and type + uint32_t esdsSize = 8 + static_cast(esdsPayload.size()); + append_u32(esdsBox, esdsSize); + append_type(esdsBox, "esds"); + esdsBox.insert(esdsBox.end(), esdsPayload.begin(), esdsPayload.end()); + + // Optional tail box to keep buffer large enough for intentional over-read + std::vector tailBox; + if (tailFreePayloadSize > 0) + { + tailBox = build_free_box(tailFreePayloadSize); + } + + // Wrap mp4a box + std::vector mp4aBox; + uint32_t mp4aSize = 8 + static_cast(mp4aPayload.size() + esdsBox.size() + tailBox.size()); + append_u32(mp4aBox, mp4aSize); + append_type(mp4aBox, "mp4a"); + mp4aBox.insert(mp4aBox.end(), mp4aPayload.begin(), mp4aPayload.end()); + mp4aBox.insert(mp4aBox.end(), esdsBox.begin(), esdsBox.end()); + mp4aBox.insert(mp4aBox.end(), tailBox.begin(), tailBox.end()); + return mp4aBox; +} + +// Build an stsd box containing one mp4a sample entry +static std::vector build_stsd_with_mp4a(const std::vector& mp4aBox) +{ + std::vector stsdPayload; + // FullBox header + stsdPayload.insert(stsdPayload.end(), {0x00, 0x00, 0x00, 0x00}); + // entry count (u32 = 1) + append_u32(stsdPayload, 1); + // sample entry box + stsdPayload.insert(stsdPayload.end(), mp4aBox.begin(), mp4aBox.end()); + + // Wrap stsd box + std::vector stsdBox; + uint32_t stsdSize = 8 + static_cast(stsdPayload.size()); + append_u32(stsdBox, stsdSize); + append_type(stsdBox, "stsd"); + stsdBox.insert(stsdBox.end(), stsdPayload.begin(), stsdPayload.end()); + return stsdBox; +} + +// Build a minimal buffer that Mp4Demux can parse: just an stsd->mp4a->esds chain +static std::vector build_buffer_with_esds(const std::vector& decSpecLenBytes, + size_t decSpecPayloadSize, + uint32_t tailFreePayloadSize = 0) +{ + auto esdsPayload = build_esds_payload(decSpecLenBytes, decSpecPayloadSize); + auto mp4aBox = build_mp4a_box(esdsPayload, tailFreePayloadSize); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + // We can place stsd at top-level; DemuxHelper doesn't enforce strict nesting + std::vector buf; + buf.insert(buf.end(), stsdBox.begin(), stsdBox.end()); + return buf; +} + +class EsdsReadLenTests : public ::testing::Test {}; + +TEST_F(EsdsReadLenTests, SingleOctetLength_0x7F) +{ + // Length encoded in one octet: 0x7F = 127 + std::vector lenBytes = encode_len(0x7F); + ASSERT_EQ(lenBytes.size(), 1u); + auto buffer = build_buffer_with_esds(lenBytes, 127); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + EXPECT_TRUE(ok) << "Parse should succeed for 1-octet length"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_OK); + + auto info = demux.GetCodecInfo(); + EXPECT_EQ(info.mCodecFormat, GST_FORMAT_AUDIO_ES_AAC_RAW); + EXPECT_EQ(info.mCodecData.size(), 127u) << "AudioSpecificConfig length mismatch"; +} + +TEST_F(EsdsReadLenTests, TwoOctetLength_0x81_0x02_Value130) +{ + // Manually craft two-octet length for 130: 0x81 0x02 + std::vector lenBytes = {0x81, 0x02}; + auto buffer = build_buffer_with_esds(lenBytes, 130); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + EXPECT_TRUE(ok) << "Parse should succeed for 2-octet length"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_OK); + + auto info = demux.GetCodecInfo(); + EXPECT_EQ(info.mCodecFormat, GST_FORMAT_AUDIO_ES_AAC_RAW); + EXPECT_EQ(info.mCodecData.size(), 130u); +} + +TEST_F(EsdsReadLenTests, Overflow_FiveOctetsTriggersVariableLengthOverflow) +{ + // Provide a 5-octet varint which will exceed 32 bits in Mp4Demux::ReadLen() + // e.g., 0xFF 0xFF 0xFF 0xFF 0x7F (continuations then terminal) + std::vector lenBytes = {0xFF, 0xFF, 0xFF, 0xFF, 0x7F}; + auto buffer = build_buffer_with_esds(lenBytes, 0 /*payload not required*/); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + EXPECT_FALSE(ok) << "Parse should fail due to VARIABLE_LENGTH_OVERFLOW"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_ERROR_VARIABLE_LENGTH_OVERFLOW); +} + +TEST_F(EsdsReadLenTests, BoundaryMismatch_DecSpecificLenExceedsEsdsBox) +{ + // Declare DecoderSpecificInfo length as 256, but provide only 8 bytes of payload. + // Add a tail 'free' box to ensure ptr+len stays within the overall buffer to avoid segfault. + auto lenBytes = encode_len(256); + auto buffer = build_buffer_with_esds(lenBytes, /*actual*/ 8, /*tailFreePayloadSize*/ 1024); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + + // Expected: DemuxHelper detects that after parsing 'esds', ptr != next and sets + // MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH. + EXPECT_FALSE(ok) << "Parse should fail due to DATA_BOUNDARY_MISMATCH (esds over-read)"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); +} + + +// Build an intentionally invalid 'free' box with size < 8 (short box) +static std::vector build_free_box_invalid_short(uint32_t declaredSize) +{ + std::vector box; + // declaredSize is intentionally < 8 to trigger INVALID_BOX + append_u32(box, declaredSize); + append_type(box, "free"); + // No payload; size field itself is invalid. + return box; +} + +TEST_F(EsdsReadLenTests, InvalidShortFreeBoxTriggersInvalidBoxError) +{ + // Build a valid stsd->mp4a->esds chain with small, correct DecoderSpecific length + auto lenBytes = encode_len(8); + auto esdsPayload = build_esds_payload(lenBytes, /*payload*/ 8); + auto mp4aBox = build_mp4a_box(esdsPayload); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + // Append an invalid 'free' box whose declared size is 6 (< 8 minimum) + auto badFree = build_free_box_invalid_short(6); + + std::vector buffer; + buffer.insert(buffer.end(), stsdBox.begin(), stsdBox.end()); + buffer.insert(buffer.end(), badFree.begin(), badFree.end()); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + EXPECT_FALSE(ok) << "Parse should fail due to INVALID_BOX for short 'free'"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_ERROR_INVALID_BOX); +} + + +// Build a 'free' box using 32-bit size==1 followed by 64-bit large size +static std::vector build_free_box_large_size(uint64_t largeSize) +{ + std::vector box; + // 32-bit size==1 indicates presence of 64-bit largesize + append_u32(box, 1); + append_type(box, "free"); + // append 64-bit largesize big-endian + box.push_back(static_cast((largeSize >> 56) & 0xFF)); + box.push_back(static_cast((largeSize >> 48) & 0xFF)); + box.push_back(static_cast((largeSize >> 40) & 0xFF)); + box.push_back(static_cast((largeSize >> 32) & 0xFF)); + box.push_back(static_cast((largeSize >> 24) & 0xFF)); + box.push_back(static_cast((largeSize >> 16) & 0xFF)); + box.push_back(static_cast((largeSize >> 8) & 0xFF)); + box.push_back(static_cast(largeSize & 0xFF)); + // The remainder of the box payload is (largeSize - 16) bytes. + if (largeSize >= 16) + { + box.resize(static_cast(largeSize), 0x00); + } + return box; +} + +TEST_F(EsdsReadLenTests, LargeSizeFreeBoxInvalidWhenLargesizeBelow16) +{ + // Valid stsd->mp4a->esds first + auto lenBytes = encode_len(8); + auto esdsPayload = build_esds_payload(lenBytes, /*payload*/ 8); + auto mp4aBox = build_mp4a_box(esdsPayload); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + // Build a free box with size==1 and 64-bit largesize=8 (which is <16 and invalid) + auto badFree = build_free_box_large_size(8); + + std::vector buffer; + buffer.insert(buffer.end(), stsdBox.begin(), stsdBox.end()); + buffer.insert(buffer.end(), badFree.begin(), badFree.end()); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + EXPECT_FALSE(ok) << "Parse should fail: large-size free box with largesize < 16"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_ERROR_INVALID_BOX); +} + +TEST_F(EsdsReadLenTests, LargeSizeFreeBoxValidWhenLargesizeIs16) +{ + // Valid stsd->mp4a->esds first + auto lenBytes = encode_len(8); + auto esdsPayload = build_esds_payload(lenBytes, /*payload*/ 8); + auto mp4aBox = build_mp4a_box(esdsPayload); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + // Build a free box with size==1 and 64-bit largesize=16 (header only, no payload) + auto goodFree = build_free_box_large_size(16); + + std::vector buffer; + buffer.insert(buffer.end(), stsdBox.begin(), stsdBox.end()); + buffer.insert(buffer.end(), goodFree.begin(), goodFree.end()); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + EXPECT_TRUE(ok) << "Parse should succeed: large-size free box with minimal valid size (16)"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_OK); +} + + +TEST_F(EsdsReadLenTests, LargeSizeFreeBoxBoundaryMismatchWhenBufferShorterThanLargesize) +{ + // Build a valid stsd->mp4a->esds prelude + auto lenBytes = encode_len(8); + auto esdsPayload = build_esds_payload(lenBytes, /*payload*/ 8); + auto mp4aBox = build_mp4a_box(esdsPayload); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + // Construct a large-size free box with largesize=32 (valid), + // then intentionally truncate the appended bytes to be smaller than 32. + auto freeBoxFull = build_free_box_large_size(32); // should be 32 bytes total + + // Truncate to 24 bytes to simulate buffer ending before 'next' + std::vector freeBoxTruncated(freeBoxFull.begin(), freeBoxFull.begin() + 24); + + std::vector buffer; + buffer.insert(buffer.end(), stsdBox.begin(), stsdBox.end()); + buffer.insert(buffer.end(), freeBoxTruncated.begin(), freeBoxTruncated.end()); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + + // Expected: DemuxHelper computes next = ptr + (32 - 16) and finds next > fin, + // thus sets MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH. + EXPECT_FALSE(ok) << "Parse should fail: largesize valid but buffer shorter than declared size"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); +} + + +// Build a 'free' box with size==0, which extends to end of file/buffer +static std::vector build_free_box_size0(uint32_t payloadSize) +{ + std::vector box; + append_u32(box, 0); // size==0 => box runs to end + append_type(box, "free"); + box.resize(8 + payloadSize, 0x00); + return box; +} + +TEST_F(EsdsReadLenTests, SizeZeroFreeBoxExtendsToEndAndParsesSuccessfully) +{ + // Prelude: valid stsd->mp4a->esds + auto lenBytes = encode_len(8); + auto esdsPayload = build_esds_payload(lenBytes, /*payload*/ 8); + auto mp4aBox = build_mp4a_box(esdsPayload); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + // Create a size==0 free box with 256 bytes of payload + auto zeroFree = build_free_box_size0(256); + + std::vector buffer; + buffer.insert(buffer.end(), stsdBox.begin(), stsdBox.end()); + buffer.insert(buffer.end(), zeroFree.begin(), zeroFree.end()); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + EXPECT_TRUE(ok) << "Parse should succeed: size==0 free box extends to end of buffer"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_OK); +} + + +// Build a zero-size box with an unknown type (not handled in switch) +static std::vector build_zero_size_unknown_box(const char type[4], uint32_t payloadSize) +{ + std::vector box; + append_u32(box, 0); // size==0 -> box extends to end of buffer + append_type(box, type); + box.resize(8 + payloadSize, 0x00); + return box; +} + +TEST_F(EsdsReadLenTests, ZeroSizeUnknownTypeTriggersDataBoundaryMismatch) +{ + // Prelude: a valid stsd->mp4a->esds so parser is in a good state + auto lenBytes = encode_len(8); + auto esdsPayload = build_esds_payload(lenBytes, /*payload*/ 8); + auto mp4aBox = build_mp4a_box(esdsPayload); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + // Create a zero-size box with an unrecognized type 'zzzz' and some payload + auto unknownZero = build_zero_size_unknown_box("zzzz", 64); + + std::vector buffer; + buffer.insert(buffer.end(), stsdBox.begin(), stsdBox.end()); + buffer.insert(buffer.end(), unknownZero.begin(), unknownZero.end()); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + + // Rationale: For size==0, DemuxHelper sets next=fin. Since the type is unknown, + // it falls into default: (no ptr advance). The final ptr!=next check fires and sets + // MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH. + EXPECT_FALSE(ok) << "Parse should fail: zero-size box with unknown type leaves ptr!=next"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); +} + + +// Generic builder: size==0 box for a given valid type +static std::vector build_size0_box(const char type[4], uint32_t payloadSize) +{ + std::vector box; + append_u32(box, 0); // size==0 => extends to end of buffer + append_type(box, type); + box.resize(8 + payloadSize, 0x00); + return box; +} + +TEST_F(EsdsReadLenTests, SizeZeroMdatExtendsToEndAndParsesSuccessfully) +{ + // Prelude: valid stsd->mp4a->esds ensures DemuxHelper is functioning + auto lenBytes = encode_len(8); + auto esdsPayload = build_esds_payload(lenBytes, /*payload*/ 8); + auto mp4aBox = build_mp4a_box(esdsPayload); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + // Build an 'mdat' box with size==0 and 512 bytes payload + auto zeroMdat = build_size0_box("mdat", 512); + + std::vector buffer; + buffer.insert(buffer.end(), stsdBox.begin(), stsdBox.end()); + buffer.insert(buffer.end(), zeroMdat.begin(), zeroMdat.end()); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + + // Expected: DemuxHelper sets next=fin for size==0; in 'mdat' case it records + // mdatStart/mdatEnd and sets ptr=next. Final ptr==next check passes, parse OK. + EXPECT_TRUE(ok) << "Parse should succeed: size==0 mdat extends to end of buffer"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_OK); +} + + +TEST_F(EsdsReadLenTests, SizeZeroUdtaExtendsToEndAndParsesSuccessfully) +{ + // Prelude: valid stsd->mp4a->esds + auto lenBytes = encode_len(8); + auto esdsPayload = build_esds_payload(lenBytes, /*payload*/ 8); + auto mp4aBox = build_mp4a_box(esdsPayload); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + // Build a 'udta' box with size==0 and 128 bytes payload + auto zeroUdta = build_size0_box("udta", 128); + + std::vector buffer; + buffer.insert(buffer.end(), stsdBox.begin(), stsdBox.end()); + buffer.insert(buffer.end(), zeroUdta.begin(), zeroUdta.end()); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + + // Expected: size==0 -> next=fin. For 'udta', DemuxHelper case sets ptr=next directly. + // Final ptr==next passes, parse OK. + EXPECT_TRUE(ok) << "Parse should succeed: size==0 udta extends to end of buffer"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_OK); +} + + +TEST_F(EsdsReadLenTests, SizeZeroEsdsWithCorruptedDescriptorTagFails) +{ + // Build a top-level size==0 'esds' box whose payload starts with an invalid tag (0x01), + // which should trigger MP4_PARSE_ERROR_INVALID_ESDS_TAG inside ParseEsdsCodecConfigHelper(). + + // Construct corrupted ESDS payload: FullBox header(4) + invalid tag(0x01) + len(1) + 1 byte + std::vector corrupted; + // FullBox header: version(1) + flags(3) + corrupted.insert(corrupted.end(), {0x00, 0x00, 0x00, 0x00}); + // Invalid tag + corrupted.push_back(0x01); + // length=1 (varint) + corrupted.push_back(0x01); + // one arbitrary data byte + corrupted.push_back(0xFF); + + // Wrap into size==0 'esds' box + std::vector esdsBox; + append_u32(esdsBox, 0); // size==0 -> extends to end + append_type(esdsBox, "esds"); + esdsBox.insert(esdsBox.end(), corrupted.begin(), corrupted.end()); + + // Buffer: include a small valid stsd->mp4a->esds prelude so demuxer has context, then corrupted esds + auto lenBytes = encode_len(8); + auto goodEsdsPayload = build_esds_payload(lenBytes, /*payload*/ 8); + auto mp4aBox = build_mp4a_box(goodEsdsPayload); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + std::vector buffer; + buffer.insert(buffer.end(), stsdBox.begin(), stsdBox.end()); + buffer.insert(buffer.end(), esdsBox.begin(), esdsBox.end()); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + + // Expected: DemuxHelper sets next=fin for size==0 esds and dispatches to ParseCodecConfigurationBox. + // The first descriptor tag is invalid -> setParseError(INVALID_ESDS_TAG), Parse() returns false. + EXPECT_FALSE(ok) << "Parse should fail: size==0 esds with corrupted tag"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_ERROR_INVALID_ESDS_TAG); +} + + +TEST_F(EsdsReadLenTests, SizeZeroEsdsValidTagButShortDataTriggersBoundaryMismatch) +{ + // Build a top-level size==0 'esds' box with valid tags (0x03, 0x04, 0x05) + // but declare a DecoderSpecificInfo (0x05) length larger than available bytes. + // Expect MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH at box boundary check. + + std::vector esdsPayload; + // FullBox header: version(1) + flags(3) + esdsPayload.insert(esdsPayload.end(), {0x00, 0x00, 0x00, 0x00}); + + // 0x03 ES_Descriptor: len=3, content: ES_ID(2) + flags(1) + esdsPayload.push_back(0x03); + esdsPayload.push_back(0x03); // varint len=3 + esdsPayload.push_back(0x00); // ES_ID MSB + esdsPayload.push_back(0x02); // ES_ID LSB + esdsPayload.push_back(0x00); // flags + + // 0x04 DecoderConfigDescriptor: len=13, we provide full 13 bytes + esdsPayload.push_back(0x04); + esdsPayload.push_back(0x0D); // varint len=13 + esdsPayload.push_back(0x40); // objectTypeIndication + esdsPayload.push_back(0x15); // streamType/upStream/reserved + // bufferSizeDB (3 bytes) + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + // maxBitrate (4 bytes) + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + // avgBitrate (4 bytes) + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + esdsPayload.push_back(0x00); + + // 0x05 DecoderSpecificInfo: declare len=20 but only provide 5 bytes + esdsPayload.push_back(0x05); + esdsPayload.push_back(0x14); // varint len=20 + esdsPayload.push_back(0x11); + esdsPayload.push_back(0x22); + esdsPayload.push_back(0x33); + esdsPayload.push_back(0x44); + esdsPayload.push_back(0x55); + // No SLConfigDescriptor here; we intentionally cut short to provoke boundary mismatch + + // Wrap into size==0 'esds' box + std::vector esdsBox; + append_u32(esdsBox, 0); // size==0 -> extends to end + append_type(esdsBox, "esds"); + esdsBox.insert(esdsBox.end(), esdsPayload.begin(), esdsPayload.end()); + + // Buffer: include a valid prelude then corrupted size==0 esds + auto lenBytes = encode_len(8); + auto goodEsdsPayload = build_esds_payload(lenBytes, /*payload*/ 8); + auto mp4aBox = build_mp4a_box(goodEsdsPayload); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + std::vector buffer; + buffer.insert(buffer.end(), stsdBox.begin(), stsdBox.end()); + buffer.insert(buffer.end(), esdsBox.begin(), esdsBox.end()); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + + // Expected: ParseEsdsCodecConfigHelper copies 5 bytes for 0x05 but declared 20 bytes; ptr < next + // After returning to DemuxHelper for 'esds', final ptr!=next sets DATA_BOUNDARY_MISMATCH. + EXPECT_FALSE(ok) << "Parse should fail: valid ESDS tag with short data triggers boundary mismatch"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH); +} + + +TEST_F(EsdsReadLenTests, SizeZeroFreeWithExtraPaddingParsesSuccessfully) +{ + // Prelude: valid stsd->mp4a->esds + auto lenBytes = encode_len(8); + auto esdsPayload = build_esds_payload(lenBytes, /*payload*/ 8); + auto mp4aBox = build_mp4a_box(esdsPayload); + auto stsdBox = build_stsd_with_mp4a(mp4aBox); + + // Create a size==0 'free' box whose payload is 256 bytes + auto zeroFree = build_size0_box("free", 256); + + // Add extra padding bytes to the overall buffer (outside any explicit box structure). + // Since size==0 extends the box to the end of the buffer, these bytes are still considered + // part of the 'free' box payload and should parse without issue. + std::vector extraPadding(64, 0x00); + + std::vector buffer; + buffer.insert(buffer.end(), stsdBox.begin(), stsdBox.end()); + buffer.insert(buffer.end(), zeroFree.begin(), zeroFree.end()); + buffer.insert(buffer.end(), extraPadding.begin(), extraPadding.end()); + + Mp4Demux demux; + bool ok = demux.Parse(buffer.data(), buffer.size()); + + // Expected: next=fin for size==0, ptr set to next for 'free'; extra padding is part of the box payload. + EXPECT_TRUE(ok) << "Parse should succeed: size==0 free with extra padding still extends to end"; + EXPECT_EQ(demux.GetLastError(), MP4_PARSE_OK); +} diff --git a/test/utests/tests/Mp4BoxParsingTests/FourCCMappingTest.cpp b/test/utests/tests/Mp4BoxParsingTests/FourCCMappingTest.cpp index af6483a87..bd5b9e5bb 100644 --- a/test/utests/tests/Mp4BoxParsingTests/FourCCMappingTest.cpp +++ b/test/utests/tests/Mp4BoxParsingTests/FourCCMappingTest.cpp @@ -2,7 +2,7 @@ * If not stated otherwise in this file or this component's license file the * following copyright and licenses apply: * - * Copyright 2025 RDK Management + * Copyright 2026 RDK Management * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,45 +24,46 @@ class FourCCMappingTest : public ::testing::Test { - void SetUp() override - { - // Setup code if needed - } + void SetUp() override + { + // Setup code if needed + } - void TearDown() override - { - // Teardown code if needed - } + void TearDown() override + { + // Teardown code if needed + } }; TEST_F(FourCCMappingTest, TestCodecMappings) { - std::vector< std::pair > testCases = { - { MultiChar_Constant("avcC"), GST_FORMAT_VIDEO_ES_H264 }, - { MultiChar_Constant("hvcC"), GST_FORMAT_VIDEO_ES_HEVC }, - { MultiChar_Constant("esds"), GST_FORMAT_AUDIO_ES_AAC_RAW }, - { MultiChar_Constant("dec3"), GST_FORMAT_AUDIO_ES_EC3 }, - { MultiChar_Constant("xvid"), GST_FORMAT_UNKNOWN } // Unknown FourCC - }; + std::vector< std::pair > testCases = { + { MultiChar_Constant("avcC"), GST_FORMAT_VIDEO_ES_H264 }, + { MultiChar_Constant("hvcC"), GST_FORMAT_VIDEO_ES_HEVC }, + { MultiChar_Constant("esds"), GST_FORMAT_AUDIO_ES_AAC_RAW }, + { MultiChar_Constant("dec3"), GST_FORMAT_AUDIO_ES_EC3 }, + { MultiChar_Constant("dac4"), GST_FORMAT_AUDIO_ES_AC4}, + { MultiChar_Constant("xvid"), GST_FORMAT_UNKNOWN } // Unknown FourCC + }; - for (const auto& testCase : testCases) - { - GstStreamOutputFormat format = GetGstStreamOutputFormatFromFourCC(testCase.first); - EXPECT_EQ(format, testCase.second) << "Failed for FourCC: " << FourCCToString(testCase.first); - } + for (const auto& testCase : testCases) + { + GstStreamOutputFormat format = GetGstStreamOutputFormatFromFourCC(testCase.first); + EXPECT_EQ(format, testCase.second) << "Failed for FourCC: " << FourCCToString(testCase.first); + } } TEST_F(FourCCMappingTest, TestCipherTypeMappings) { - std::vector< std::pair > testCases = { - { MultiChar_Constant("cenc"), CIPHER_TYPE_CENC }, - { MultiChar_Constant("cbcs"), CIPHER_TYPE_CBCS }, - { MultiChar_Constant("abcd"), CIPHER_TYPE_NONE } // Unknown FourCC - }; + std::vector< std::pair > testCases = { + { MultiChar_Constant("cenc"), CIPHER_TYPE_CENC }, + { MultiChar_Constant("cbcs"), CIPHER_TYPE_CBCS }, + { MultiChar_Constant("abcd"), CIPHER_TYPE_NONE } // Unknown FourCC + }; - for (const auto& testCase : testCases) - { - CipherType cipher = GetCipherTypeFromFourCC(testCase.first); - EXPECT_EQ(cipher, testCase.second) << "Failed for FourCC: " << FourCCToString(testCase.first); - } -} \ No newline at end of file + for (const auto& testCase : testCases) + { + CipherType cipher = GetCipherTypeFromFourCC(testCase.first); + EXPECT_EQ(cipher, testCase.second) << "Failed for FourCC: " << FourCCToString(testCase.first); + } +} diff --git a/test/utests/tests/Mp4BoxParsingTests/Mp4BoxParsingTests.cpp b/test/utests/tests/Mp4BoxParsingTests/Mp4BoxParsingTests.cpp index ddea48f3e..9984a263f 100644 --- a/test/utests/tests/Mp4BoxParsingTests/Mp4BoxParsingTests.cpp +++ b/test/utests/tests/Mp4BoxParsingTests/Mp4BoxParsingTests.cpp @@ -2,7 +2,7 @@ * If not stated otherwise in this file or this component's license file the * following copyright and licenses apply: * - * Copyright 2025 RDK Management + * Copyright 2026 RDK Management * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 1762ff525f35d9d8e6db7f602ee65c8cdacc9bb3 Mon Sep 17 00:00:00 2001 From: Philip Stroffolino Date: Sun, 11 Jan 2026 14:22:13 -0500 Subject: [PATCH 08/11] Reason for Change: added support for 'skip' box (same behavior as "free" Signed-off-by: Philip Stroffolino --- mp4demux/MP4Demux.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mp4demux/MP4Demux.cpp b/mp4demux/MP4Demux.cpp index 56a16d882..02cb57c79 100644 --- a/mp4demux/MP4Demux.cpp +++ b/mp4demux/MP4Demux.cpp @@ -1225,7 +1225,8 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) switch (type) { case MultiChar_Constant("free"): - // ISO BMFF padding box containing unused space + case MultiChar_Constant("skip"): + // free and skip are ISO BMFF padding boxes containing unused space ptr = next; break; From 107adc9fe5084914c3d53569b060fed23d9a7e6c Mon Sep 17 00:00:00 2001 From: Philip Stroffolino Date: Sun, 11 Jan 2026 14:35:47 -0500 Subject: [PATCH 09/11] Reason for Change: removed no-longer needed hack from utest Signed-off-by: Philip Stroffolino --- .../Mp4BoxParsingTests/BoxParsingTests.cpp | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp b/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp index e7370353f..8911b4357 100644 --- a/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp +++ b/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp @@ -172,23 +172,21 @@ TEST_F(Mp4DemuxFunctionalTests, ParseEncryptedFragmentWithSencBox) auto samples = mDemuxer->GetSamples(); EXPECT_EQ(samples.size(), 2) << "Should have exactly 2 samples"; - if( samples.size() == 2 ) - { - // Validate DRM metadata for each sample - EXPECT_TRUE(samples[0].mDrmMetadata.mIsEncrypted) << "Sample should be marked as encrypted"; - EXPECT_FALSE(samples[0].mDrmMetadata.mKeyId.empty()) << "Sample should have Key ID"; - EXPECT_FALSE(samples[0].mDrmMetadata.mIV.empty()) << "Sample should have IV"; - EXPECT_FALSE(samples[0].mDrmMetadata.mCipher == CIPHER_TYPE_NONE) << "Sample should have cipher type"; - EXPECT_EQ(samples[0].mDrmMetadata.mSubSamples.size(), 6) << "Sample should have subsample encryption data"; - EXPECT_EQ(samples[0].mDrmMetadata.mNumSubSamples, 1) << "Sample should have 1 subsamples"; - - EXPECT_TRUE(samples[1].mDrmMetadata.mIsEncrypted) << "Sample should be marked as encrypted"; - EXPECT_FALSE(samples[1].mDrmMetadata.mKeyId.empty()) << "Sample should have Key ID"; - EXPECT_FALSE(samples[1].mDrmMetadata.mIV.empty()) << "Sample should have IV"; - EXPECT_FALSE(samples[1].mDrmMetadata.mCipher == CIPHER_TYPE_NONE) << "Sample should have cipher type"; - EXPECT_EQ(samples[1].mDrmMetadata.mSubSamples.size(), 12) << "Sample should have subsample encryption data"; - EXPECT_EQ(samples[1].mDrmMetadata.mNumSubSamples, 2) << "Sample should have 2 subsamples"; - } + + // Validate DRM metadata for each sample + EXPECT_TRUE(samples[0].mDrmMetadata.mIsEncrypted) << "Sample should be marked as encrypted"; + EXPECT_FALSE(samples[0].mDrmMetadata.mKeyId.empty()) << "Sample should have Key ID"; + EXPECT_FALSE(samples[0].mDrmMetadata.mIV.empty()) << "Sample should have IV"; + EXPECT_FALSE(samples[0].mDrmMetadata.mCipher == CIPHER_TYPE_NONE) << "Sample should have cipher type"; + EXPECT_EQ(samples[0].mDrmMetadata.mSubSamples.size(), 6) << "Sample should have subsample encryption data"; + EXPECT_EQ(samples[0].mDrmMetadata.mNumSubSamples, 1) << "Sample should have 1 subsamples"; + + EXPECT_TRUE(samples[1].mDrmMetadata.mIsEncrypted) << "Sample should be marked as encrypted"; + EXPECT_FALSE(samples[1].mDrmMetadata.mKeyId.empty()) << "Sample should have Key ID"; + EXPECT_FALSE(samples[1].mDrmMetadata.mIV.empty()) << "Sample should have IV"; + EXPECT_FALSE(samples[1].mDrmMetadata.mCipher == CIPHER_TYPE_NONE) << "Sample should have cipher type"; + EXPECT_EQ(samples[1].mDrmMetadata.mSubSamples.size(), 12) << "Sample should have subsample encryption data"; + EXPECT_EQ(samples[1].mDrmMetadata.mNumSubSamples, 2) << "Sample should have 2 subsamples"; } /** From 74227cacd16b5a9363e4cf7f7bbd89e6cd439b05 Mon Sep 17 00:00:00 2001 From: Philip Stroffolino Date: Sun, 11 Jan 2026 19:24:38 -0500 Subject: [PATCH 10/11] Reason for Change: edge case fix Signed-off-by: Philip Stroffolino --- mp4demux/MP4Demux.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/mp4demux/MP4Demux.cpp b/mp4demux/MP4Demux.cpp index 02cb57c79..0144fca02 100644 --- a/mp4demux/MP4Demux.cpp +++ b/mp4demux/MP4Demux.cpp @@ -1199,7 +1199,15 @@ void Mp4Demux::DemuxHelper(const uint8_t *fin) setParseError( MP4_PARSE_ERROR_INVALID_BOX ); return; } - next = ptr + (size - 16); + //next = ptr + (size - 16); + const uint64_t payloadSize = size - 16; + const uint64_t remaining = static_cast(fin - ptr); + if (payloadSize > remaining) + { + parseError = MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH; + return; + } + next = ptr + payloadSize; } else if( size == 0 ) { // box extends to end of buffer From f05001f181b702cc0c7d4c548ea564916aca00a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 00:40:38 +0000 Subject: [PATCH 11/11] Initial plan