Skip to content

Commit 73423e1

Browse files
committed
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.
1 parent 3d36cb1 commit 73423e1

2 files changed

Lines changed: 82 additions & 16 deletions

File tree

mp4parse/src/lib.rs

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3850,22 +3850,31 @@ fn read_colr<T: Read>(
38503850
let transfer_characteristics = be_u16(src)?.try_into()?;
38513851
let matrix_coefficients = be_u16(src)?.try_into()?;
38523852
let bytes = src.read_into_try_vec()?;
3853-
let mut bit_reader = BitReader::new(&bytes);
3854-
let full_range_flag = bit_reader.read_bool()?;
3855-
if bit_reader.remaining() != NUM_RESERVED_BITS.into() {
3856-
error!(
3857-
"read_colr expected {} reserved bits, found {}",
3858-
NUM_RESERVED_BITS,
3859-
bit_reader.remaining()
3860-
);
3861-
return Status::ColrBadSize.into();
3862-
}
3863-
if bit_reader.read_u8(NUM_RESERVED_BITS)? != 0 {
3864-
fail_with_status_if(
3865-
strictness != ParseStrictness::Permissive,
3866-
Status::ColrReservedNonzero,
3867-
)?;
3868-
}
3853+
// Tolerate an nclx box truncated before full_range_flag;
3854+
// treat it as unset (limited range).
3855+
let full_range_flag = if bytes.is_empty() {
3856+
warn!("read_colr: nclx missing full_range_flag, assuming limited range");
3857+
fail_with_status_if(strictness == ParseStrictness::Strict, Status::ColrBadSize)?;
3858+
false
3859+
} else {
3860+
let mut bit_reader = BitReader::new(&bytes);
3861+
let full_range_flag = bit_reader.read_bool()?;
3862+
if bit_reader.remaining() != NUM_RESERVED_BITS.into() {
3863+
error!(
3864+
"read_colr expected {} reserved bits, found {}",
3865+
NUM_RESERVED_BITS,
3866+
bit_reader.remaining()
3867+
);
3868+
return Status::ColrBadSize.into();
3869+
}
3870+
if bit_reader.read_u8(NUM_RESERVED_BITS)? != 0 {
3871+
fail_with_status_if(
3872+
strictness != ParseStrictness::Permissive,
3873+
Status::ColrReservedNonzero,
3874+
)?;
3875+
}
3876+
full_range_flag
3877+
};
38693878

38703879
Ok(ParsedColourInformation::Supported(ColourInformation::Nclx(
38713880
NclxColourInformation {

mp4parse/src/tests.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,3 +1434,60 @@ fn read_clli() {
14341434
assert_eq!(clli.max_content_light_level, 1000);
14351435
assert_eq!(clli.max_pic_average_light_level, 400);
14361436
}
1437+
1438+
#[test]
1439+
fn read_colr_nclx() {
1440+
let mut stream = make_box(BoxSize::Auto, b"colr", |s| {
1441+
s.append_bytes(b"nclx").B16(1).B16(1).B16(1).B8(0x80)
1442+
});
1443+
let mut iter = super::BoxIter::new(&mut stream);
1444+
let mut stream = iter.next_box().unwrap().unwrap();
1445+
assert_eq!(stream.head.name, super::BoxType::ColourInformationBox);
1446+
match super::read_colr(&mut stream, ParseStrictness::Strict).unwrap() {
1447+
super::ParsedColourInformation::Supported(super::ColourInformation::Nclx(nclx)) => {
1448+
assert_eq!(nclx.colour_primaries, 1);
1449+
assert_eq!(nclx.transfer_characteristics, 1);
1450+
assert_eq!(nclx.matrix_coefficients, 1);
1451+
assert!(nclx.full_range_flag);
1452+
}
1453+
_ => panic!("expected nclx colour information"),
1454+
}
1455+
}
1456+
1457+
#[test]
1458+
fn read_colr_nclx_missing_full_range_flag() {
1459+
// An 18-byte nclx colr box omitting the trailing full_range_flag/reserved
1460+
// byte, as found in the wild (bug 2047239). The layout matches the QTFF
1461+
// 'nclc' colour type.
1462+
let make_stream = || {
1463+
make_box(BoxSize::Auto, b"colr", |s| {
1464+
s.append_bytes(b"nclx").B16(1).B16(1).B16(1)
1465+
})
1466+
};
1467+
1468+
for strictness in [ParseStrictness::Permissive, ParseStrictness::Normal] {
1469+
let mut stream = make_stream();
1470+
let mut iter = super::BoxIter::new(&mut stream);
1471+
let mut stream = iter.next_box().unwrap().unwrap();
1472+
let colr = super::read_colr(&mut stream, strictness)
1473+
.expect("nclx missing full_range_flag should parse outside Strict");
1474+
match colr {
1475+
super::ParsedColourInformation::Supported(super::ColourInformation::Nclx(nclx)) => {
1476+
assert!(
1477+
!nclx.full_range_flag,
1478+
"missing full_range_flag should default to limited range"
1479+
);
1480+
}
1481+
_ => panic!("expected nclx colour information"),
1482+
}
1483+
}
1484+
1485+
let mut stream = make_stream();
1486+
let mut iter = super::BoxIter::new(&mut stream);
1487+
let mut stream = iter.next_box().unwrap().unwrap();
1488+
match super::read_colr(&mut stream, ParseStrictness::Strict) {
1489+
Err(Error::InvalidData(s)) => assert_eq!(s, Status::ColrBadSize),
1490+
Ok(_) => panic!("nclx missing full_range_flag should be rejected under Strict"),
1491+
Err(e) => panic!("unexpected error {:?}", e),
1492+
}
1493+
}

0 commit comments

Comments
 (0)