From f7eaafae08165a1c58cc6900cd6ae9a4ea3d2247 Mon Sep 17 00:00:00 2001 From: Matthew Gregan Date: Sat, 13 Jun 2026 11:38:54 +1200 Subject: [PATCH] Tolerate nclx colr boxes missing the full_range_flag byte Some muxers write an 18-byte nclx colr box that omits the trailing full_range_flag/reserved byte, matching the layout of the QTFF 'nclc' colour type. read_colr failed on these with a fatal BitReaderError, rejecting the whole file, while ffmpeg, Chrome and Safari accept such boxes and treat the range as limited. Default full_range_flag to false in this case and only reject the short box under ParseStrictness::Strict. Fixes bug 2047239. --- mp4parse/src/lib.rs | 41 +++++++++++++++++++------------ mp4parse/src/tests.rs | 57 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/mp4parse/src/lib.rs b/mp4parse/src/lib.rs index 6a404556..3d077e5d 100644 --- a/mp4parse/src/lib.rs +++ b/mp4parse/src/lib.rs @@ -3850,22 +3850,31 @@ fn read_colr( let transfer_characteristics = be_u16(src)?.try_into()?; let matrix_coefficients = be_u16(src)?.try_into()?; let bytes = src.read_into_try_vec()?; - let mut bit_reader = BitReader::new(&bytes); - let full_range_flag = bit_reader.read_bool()?; - if bit_reader.remaining() != NUM_RESERVED_BITS.into() { - error!( - "read_colr expected {} reserved bits, found {}", - NUM_RESERVED_BITS, - bit_reader.remaining() - ); - return Status::ColrBadSize.into(); - } - if bit_reader.read_u8(NUM_RESERVED_BITS)? != 0 { - fail_with_status_if( - strictness != ParseStrictness::Permissive, - Status::ColrReservedNonzero, - )?; - } + // Tolerate an nclx box truncated before full_range_flag; + // treat it as unset (limited range). + let full_range_flag = if bytes.is_empty() { + warn!("read_colr: nclx missing full_range_flag, assuming limited range"); + fail_with_status_if(strictness == ParseStrictness::Strict, Status::ColrBadSize)?; + false + } else { + let mut bit_reader = BitReader::new(&bytes); + let full_range_flag = bit_reader.read_bool()?; + if bit_reader.remaining() != NUM_RESERVED_BITS.into() { + error!( + "read_colr expected {} reserved bits, found {}", + NUM_RESERVED_BITS, + bit_reader.remaining() + ); + return Status::ColrBadSize.into(); + } + if bit_reader.read_u8(NUM_RESERVED_BITS)? != 0 { + fail_with_status_if( + strictness != ParseStrictness::Permissive, + Status::ColrReservedNonzero, + )?; + } + full_range_flag + }; Ok(ParsedColourInformation::Supported(ColourInformation::Nclx( NclxColourInformation { diff --git a/mp4parse/src/tests.rs b/mp4parse/src/tests.rs index 087ba956..4098b2d6 100644 --- a/mp4parse/src/tests.rs +++ b/mp4parse/src/tests.rs @@ -1434,3 +1434,60 @@ fn read_clli() { assert_eq!(clli.max_content_light_level, 1000); assert_eq!(clli.max_pic_average_light_level, 400); } + +#[test] +fn read_colr_nclx() { + let mut stream = make_box(BoxSize::Auto, b"colr", |s| { + s.append_bytes(b"nclx").B16(1).B16(1).B16(1).B8(0x80) + }); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + assert_eq!(stream.head.name, super::BoxType::ColourInformationBox); + match super::read_colr(&mut stream, ParseStrictness::Strict).unwrap() { + super::ParsedColourInformation::Supported(super::ColourInformation::Nclx(nclx)) => { + assert_eq!(nclx.colour_primaries, 1); + assert_eq!(nclx.transfer_characteristics, 1); + assert_eq!(nclx.matrix_coefficients, 1); + assert!(nclx.full_range_flag); + } + _ => panic!("expected nclx colour information"), + } +} + +#[test] +fn read_colr_nclx_missing_full_range_flag() { + // An 18-byte nclx colr box omitting the trailing full_range_flag/reserved + // byte, as found in the wild (bug 2047239). The layout matches the QTFF + // 'nclc' colour type. + let make_stream = || { + make_box(BoxSize::Auto, b"colr", |s| { + s.append_bytes(b"nclx").B16(1).B16(1).B16(1) + }) + }; + + for strictness in [ParseStrictness::Permissive, ParseStrictness::Normal] { + let mut stream = make_stream(); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + let colr = super::read_colr(&mut stream, strictness) + .expect("nclx missing full_range_flag should parse outside Strict"); + match colr { + super::ParsedColourInformation::Supported(super::ColourInformation::Nclx(nclx)) => { + assert!( + !nclx.full_range_flag, + "missing full_range_flag should default to limited range" + ); + } + _ => panic!("expected nclx colour information"), + } + } + + let mut stream = make_stream(); + let mut iter = super::BoxIter::new(&mut stream); + let mut stream = iter.next_box().unwrap().unwrap(); + match super::read_colr(&mut stream, ParseStrictness::Strict) { + Err(Error::InvalidData(s)) => assert_eq!(s, Status::ColrBadSize), + Ok(_) => panic!("nclx missing full_range_flag should be rejected under Strict"), + Err(e) => panic!("unexpected error {:?}", e), + } +}