diff --git a/mp4demux/MP4Demux.cpp b/mp4demux/MP4Demux.cpp index 09a03a946..0144fca02 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 @@ -93,7 +93,8 @@ 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 }, + { MultiChar_Constant("dac4"), GST_FORMAT_AUDIO_ES_AC4 } // AC-4 decoder config box }; /** @@ -161,9 +162,11 @@ Mp4Demux::Mp4Demux() : samples(), defaultKid(), gotAuxiliaryInformationOffset(), auxiliaryInformationOffset(), schemeType(CIPHER_TYPE_NONE), originalMediaType(), cencAuxInfoSizes(), protectionData(), - moofPtr(), ptr(), + moofPtr(), + ptr(), endPtr(nullptr), version(), flags(), baseMediaDecodeTime(), trackId(), baseDataOffset(), + mdatStart(nullptr), mdatEnd(nullptr), defaultSampleDescriptionIndex(), defaultSampleDuration(), defaultSampleSize(), defaultSampleFlags(), duration(), @@ -181,6 +184,61 @@ Mp4Demux::~Mp4Demux() { } +void Mp4Demux::setParseError( Mp4ParseError err ) +{ + parseError = err; + const char *text = nullptr; + switch( 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_IV_SIZE: + text = "INVALID_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 ); +} + /** * @brief Read n bytes from current position in big-endian format * Reads bytes from the current parser position and converts from @@ -193,10 +251,17 @@ 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; also validate n in [1,8] + if (n <= 0 || n > 8 || !ptr || !endPtr || ptr + n > endPtr) + { + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); + } + else { - rc <<= 8; - rc |= *ptr++; + for (int i = 0; i < n; i++) + { // accumulate bytes + rc = (rc<<8) + (*ptr++); + } } return rc; } @@ -263,7 +328,14 @@ void Mp4Demux::ReadHeader() */ void Mp4Demux::SkipBytes(size_t len) { - ptr += len; + if (!ptr || !endPtr || ptr + len > endPtr) + { + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); + } + else + { + ptr += len; + } } /** @@ -300,33 +372,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_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) - { - parseError = MP4_PARSE_ERROR_INVALID_CONSTANT_IV_SIZE; - MP4_LOG_ERR("Invalid constant IV size: %u, expected 8 or 16", constantIvSize); + if (constantIvSize != 8 && constantIvSize != 16) { + setParseError( MP4_PARSE_ERROR_INVALID_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; } } @@ -343,29 +443,65 @@ 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; - 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 { // 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) + { + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); + 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; + } +#endif + size_t kidBytes = 16*static_cast(kidCount); + if (ptr + kidBytes > next) + { + setParseError( 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) + { + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); + return; + } + uint32_t dataSize = ReadU32(); + if (ptr + dataSize > next) + { + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); + return; + } + psshData.pssh.assign(ptr, ptr + dataSize); + SkipBytes(dataSize); } } @@ -386,8 +522,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++) @@ -485,8 +620,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; @@ -512,17 +646,51 @@ void Mp4Demux::ParseSampleAuxiliaryInformationOffsets() ParseProtectionSchemeInfo(); SkipBytes(4); // aux_info_type_parameter } - SkipBytes(4); // entry_count - - if( version == 0 ) + // entry_count + if (ptr + 4 > endPtr) + { + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); + return; + } + uint32_t entryCount = ReadU32(); + if (entryCount == 0) + { + setParseError( MP4_PARSE_ERROR_INVALID_ENTRY_COUNT ); + return; + } + // Read the first offset; if multiple, warn and consume others + if (version == 0) { auxiliaryInformationOffset = ReadU32(); + if( parseError == MP4_PARSE_OK ) + { + for (uint32_t i = 1; i < entryCount; ++i) + { + (void)ReadU32(); + if( parseError != MP4_PARSE_OK ) + { + break; + } + } + gotAuxiliaryInformationOffset = true; + } } else { auxiliaryInformationOffset = ReadU64(); + if( parseError == MP4_PARSE_OK ) + { + for (uint32_t i = 1; i < entryCount; ++i) + { + (void)ReadU64(); + if( parseError != MP4_PARSE_OK ) + { + break; + } + } + gotAuxiliaryInformationOffset = true; + } } - gotAuxiliaryInformationOffset = true; } /** @@ -540,8 +708,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++) @@ -589,18 +756,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; } - 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; @@ -633,6 +796,13 @@ 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 = mdatEnd?mdatEnd:endPtr; + if ( dataPtr + sampleLen > hardEnd ) + { + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); + return; + } newSample.mData.AppendBytes(dataPtr, sampleLen); dataPtr += sampleLen; newSample.mDts = dts / (double)timeScale; @@ -706,11 +876,9 @@ 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) { - // 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; } } @@ -816,12 +984,17 @@ void Mp4Demux::ParseSampleDescriptionBox(const uint8_t *next) { ReadHeader(); uint32_t count = ReadU32(); - if (count != 1) - { - parseError = MP4_PARSE_ERROR_UNSUPPORTED_SAMPLE_ENTRY_COUNT; - MP4_LOG_ERR("Unsupported sample description count: %u, expected 1", count); + 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("unexpected stsd count=%u", count ); + } + // Parse contained sample entries/config boxes DemuxHelper(next); } @@ -848,13 +1021,13 @@ 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; 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 @@ -873,16 +1046,30 @@ 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() { - int rc = 0; - for (;;) + if( ptr && endPtr ) { - unsigned char octet = *ptr++; - rc <<= 7; - rc |= octet & 0x7f; - if ((octet & 0x80) == 0) return rc; + uint32_t rc = 0; + int bits = 0; + while( ptr 32 ) + { + setParseError( MP4_PARSE_ERROR_VARIABLE_LENGTH_OVERFLOW ); + return 0; + } + if ((octet & 0x80) == 0) + { + return rc; + } + } } + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); + return 0; } /** @@ -937,8 +1124,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; } @@ -949,8 +1135,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 ); } } @@ -1001,32 +1186,71 @@ 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); - + 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( parseError !=MP4_PARSE_OK ) return; + if ( size < 16) + { + setParseError( MP4_PARSE_ERROR_INVALID_BOX ); + return; + } + //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 + next = fin; + } + else if( size<8 ) + { + setParseError( MP4_PARSE_ERROR_INVALID_BOX ); + return; + } + 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); + setParseError( MP4_PARSE_ERROR_DATA_BOUNDARY_MISMATCH ); return; } switch (type) { + case MultiChar_Constant("free"): + case MultiChar_Constant("skip"): + // free and skip are ISO BMFF padding boxes containing unused space + ptr = next; + break; + case MultiChar_Constant("hev1"): case MultiChar_Constant("hvc1"): 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); 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 @@ -1149,10 +1373,15 @@ 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 + mdatStart = ptr; // start of payload (after header bytes) + mdatEnd = next; // end of payload + ptr = next; // skip payload + break; default: break; @@ -1163,8 +1392,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; } } @@ -1195,8 +1423,10 @@ 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; this->ptr = (const uint8_t *)ptr; DemuxHelper(&this->ptr[len]); if (parseError == MP4_PARSE_OK) @@ -1214,8 +1444,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; } @@ -1284,4 +1513,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..37583f1b5 100644 --- a/mp4demux/MP4Demux.h +++ b/mp4demux/MP4Demux.h @@ -79,18 +79,20 @@ 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_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) */ + 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 */ + 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 */ }; /** @@ -143,6 +145,10 @@ class Mp4Demux // Parser state const uint8_t *moofPtr; /**< Base address for sample data */ const uint8_t *ptr; /**< Current parser position */ + const uint8_t* endPtr; /**< Absolute end of current parse buffer */ + // 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 */ @@ -167,6 +173,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 @@ -321,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 @@ -383,6 +395,7 @@ class Mp4Demux * @brief Parse MP4 data * @param ptr Pointer to MP4 data * @param len Length of data + * @note 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 +441,4 @@ class Mp4Demux std::vector GetSamples(); }; -#endif /* __MP4_DEMUX_H__ */ \ No newline at end of file +#endif /* __MP4_DEMUX_H__ */ diff --git a/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp b/test/utests/tests/Mp4BoxParsingTests/BoxParsingTests.cpp index 767c4b480..8911b4357 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. @@ -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"; @@ -141,22 +141,22 @@ 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_EQ(samples[0].mDuration, 0.1) << "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_EQ(samples[1].mPts, 0.1) << "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].mPts, 0.1, 1e-6) << "Sample 1 PTS 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"; } /** * @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) @@ -172,6 +172,7 @@ 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"; @@ -179,7 +180,7 @@ TEST_F(Mp4DemuxFunctionalTests, ParseEncryptedFragmentWithSencBox) 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"; @@ -190,7 +191,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 @@ -212,7 +213,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"; @@ -259,4 +260,200 @@ 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 +} + +// ---- 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)); +} + +// 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; + { 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"); + // 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(); + } + 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; + // 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(); + } + 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); + 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); +} + 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. 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