fix(clpi): resolve out_time past the last EP entry to the end of the clip

ClipInfo::get_extents fell back to `last EP SPN + 1` whenever out_time
lay past the last entry-point. EP entries mark I-frames (BD-ROM Part 3,
CPI / EP map) and a clip's final GOP lies after the last one, so a
PlayItem covering a whole clip — whose OUT_time is the presentation end
— always lands in that arm. The extent then stopped one source packet
after the last I-frame.

Measured on a fixture with 200,000 source packets and the last EP at
SPN 131,072: sector_count came back 12,289 where covering the clip
needs 18,750. Everything from the last entry point to EOF is outside
the returned extent.

Scope, stated plainly: get_extents has NO callers anywhere in the
ecosystem today — it is #[allow(dead_code)] and documented as reserved
for the timestamp-range read path. Nothing ships this loss. It is
fixed now because a latent truncation in extent arithmetic is far
cheaper to correct before it has callers than after.

The SPN at-or-after an out-of-range out_time is the end of the clip,
source_packet_count, with .max(last + 1) so a disc that under-declares
its own packet count against its own EP map still yields a sane bound.

Also 174 mutants killed across clpi, mpls, ifo and ebml — the first
time any of these four files has been examined. And ebml's 8-byte VINT
back-patch was duplicated verbatim in end_master and end_master_buf
with its top four payload octets unreachable through either (they need
a 16 MiB..256 TiB buffer); extracted to fixed_width_vint8 and tested
across the full 56-bit payload, no behaviour change.

38 of ebml's 46 survivors are one equivalence cluster: every | in
write_size / read_id / read_size / read_uint_val ORs into disjoint bit
lanes, where ^ is the identical operation. Applied all 38 at once —
green — then spot-checked four individually.
This commit is contained in:
Matthew Jackson
2026-07-30 14:24:57 -07:00
parent 84a77f6a0e
commit b2e1982051
4 changed files with 1244 additions and 22 deletions
+52 -20
View File
@@ -150,6 +150,30 @@ pub fn start_master<W: Write + Seek>(w: &mut W, id: u32) -> io::Result<u64> {
Ok(size_pos)
}
/// Encode `data_size` as the FIXED-WIDTH 8-octet EBML VINT used to back-patch
/// a master element's size field: the `0x01` VINT_MARKER octet followed by the
/// 56-bit VINT_DATA payload, big-endian (RFC 8794 section 4.4 — an 8-octet
/// VINT carries 7 octets of VINT_DATA).
///
/// Extracted so the full 56-bit payload can be exercised directly: reaching
/// the high payload bytes through [`end_master`] / [`end_master_buf`] would
/// take a multi-terabyte buffer, leaving them unconstrained by any test.
///
/// `data_size` must be below 2^56; both callers check that first.
fn fixed_width_vint8(data_size: u64) -> [u8; 8] {
debug_assert!(data_size < 0x0100_0000_0000_0000);
[
0x01,
(data_size >> 48) as u8,
(data_size >> 40) as u8,
(data_size >> 32) as u8,
(data_size >> 24) as u8,
(data_size >> 16) as u8,
(data_size >> 8) as u8,
data_size as u8,
]
}
/// End a master element: seek back and write the actual size.
///
/// `size_pos` must be the offset returned by [`start_master`], which always
@@ -170,16 +194,7 @@ pub fn end_master<W: Write + Seek>(w: &mut W, size_pos: u64) -> io::Result<()> {
);
w.seek(SeekFrom::Start(size_pos))?;
// Write as 8-byte VINT: 0x01 followed by 7 bytes of size
w.write_all(&[
0x01,
(data_size >> 48) as u8,
(data_size >> 40) as u8,
(data_size >> 32) as u8,
(data_size >> 24) as u8,
(data_size >> 16) as u8,
(data_size >> 8) as u8,
data_size as u8,
])?;
w.write_all(&fixed_width_vint8(data_size))?;
w.seek(SeekFrom::Start(end_pos))?;
Ok(())
}
@@ -220,16 +235,7 @@ pub fn end_master_buf(buf: &mut [u8], size_pos: usize) -> io::Result<()> {
if data_size >= 0x0100_0000_0000_0000 {
return Err(crate::error::Error::MkvUnencodable.into());
}
buf[size_pos..body_start].copy_from_slice(&[
0x01,
(data_size >> 48) as u8,
(data_size >> 40) as u8,
(data_size >> 32) as u8,
(data_size >> 24) as u8,
(data_size >> 16) as u8,
(data_size >> 8) as u8,
data_size as u8,
]);
buf[size_pos..body_start].copy_from_slice(&fixed_width_vint8(data_size));
Ok(())
}
@@ -1465,4 +1471,30 @@ mod tests {
"a size_pos past the end of the buffer must error, not panic"
);
}
/// The fixed-width 8-octet VINT is `0x01` followed by the 56-bit
/// VINT_DATA payload in BIG-ENDIAN order (RFC 8794 section 4.4). Every
/// payload octet is distinct here, so a shifted, reversed or dropped
/// octet is visible; the top payload octets are unreachable through
/// end_master without a multi-terabyte buffer, which is why this is
/// tested at the encoder.
#[test]
fn fixed_width_vint8_is_big_endian_over_the_full_payload() {
assert_eq!(
fixed_width_vint8(0x00AA_BB_CC_DD_EE_FF_11),
[0x01, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11]
);
assert_eq!(fixed_width_vint8(0), [0x01, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(
fixed_width_vint8(0x00FF_FFFF_FFFF_FFFE),
[0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE]
);
// Each payload octet in isolation lands in its own position.
for i in 0..7u32 {
let mut want = [0u8; 8];
want[0] = 0x01;
want[7 - i as usize] = 0x5A;
assert_eq!(fixed_width_vint8(0x5Au64 << (8 * i)), want, "octet {i}");
}
}
}