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:
+498
-2
@@ -157,11 +157,24 @@ impl ClipInfo {
|
||||
Err(i) => ep_map[i - 1].1,
|
||||
};
|
||||
|
||||
// Find SPN at or after out_time
|
||||
// Find SPN at or after out_time.
|
||||
//
|
||||
// When out_time is past the last EP entry there is no later entry
|
||||
// point to resolve against: EP entries mark I-frames, and the final
|
||||
// GOP of a clip lies *after* the last one. The SPN at-or-after
|
||||
// out_time is then the end of the clip, i.e. source_packet_count.
|
||||
// Falling back to `last EP + 1` here would truncate the extent at
|
||||
// the last I-frame and silently drop every packet after it — which
|
||||
// is the normal case for a PlayItem covering a whole clip, since
|
||||
// its OUT_time is the presentation end, not the last entry point.
|
||||
// `max` keeps the bound sane if a hostile disc declares a
|
||||
// source_packet_count below its own EP map.
|
||||
let end_spn = match ep_map.binary_search_by_key(&out_time, |(pts, _)| *pts) {
|
||||
Ok(i) => ep_map[i].1,
|
||||
Err(i) if i < ep_map.len() => ep_map[i].1,
|
||||
_ => ep_map.last().unwrap().1.saturating_add(1),
|
||||
_ => self
|
||||
.source_packet_count
|
||||
.max(ep_map.last().unwrap().1.saturating_add(1)),
|
||||
};
|
||||
|
||||
if end_spn <= start_spn {
|
||||
@@ -1275,4 +1288,487 @@ mod tests {
|
||||
// All 3 fine entries resolved (last group picks up fine 1 and 2).
|
||||
assert_eq!(resolved.len(), 3);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// get_extents: PTS→SPN resolution and SPN→sector arithmetic.
|
||||
//
|
||||
// Fixture below uses ONE coarse group with spn_coarse = 0 so that
|
||||
// full_spn((0 & 0xFFFE_0000) | spn_fine) == spn_fine exactly, and
|
||||
// full_pts == pts_fine << 8. That makes every (PTS, SPN) pair in the
|
||||
// resolved map an exact, hand-checkable number.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// EP map with three entries: PTS 2560→SPN 1000, 5120→2000, 7680→3000.
|
||||
/// `source_packet_count` is 200_000 (the clip is much longer than its
|
||||
/// last entry point, as every real clip is).
|
||||
fn three_point_clip() -> ClipInfo {
|
||||
let cpi = build_cpi(0x1011, &[(0, 0, 0)], &[(10, 1000), (20, 2000), (30, 3000)]);
|
||||
let data = build_clpi(200_000, Some(&cpi));
|
||||
parse(&data).expect("should parse")
|
||||
}
|
||||
|
||||
/// An out_time past the LAST EP entry must extend to the end of the
|
||||
/// clip, not stop at the last entry point.
|
||||
///
|
||||
/// EP entries mark I-frames (BD Part 3, CPI / EP map): the final GOP of
|
||||
/// a clip lies after the last EP entry, and a PlayItem's OUT_time is the
|
||||
/// presentation end, so out_time > last EP PTS is the ordinary case for
|
||||
/// a whole-clip play item. Resolving that to `last_spn + 1` would return
|
||||
/// an extent covering ~1 source packet past the last I-frame and drop
|
||||
/// every packet after it. The clip end is `source_packet_count`.
|
||||
#[test]
|
||||
fn get_extents_out_time_past_last_ep_covers_clip_tail() {
|
||||
let clip = three_point_clip();
|
||||
let extents = clip.get_extents(2560, u64::MAX);
|
||||
assert_eq!(extents.len(), 1);
|
||||
// start: SPN 1000 × 192 = 192_000 bytes, floor(/2048) = sector 93.
|
||||
assert_eq!(extents[0].start_lba, 93);
|
||||
// end: SPN 200_000 × 192 = 38_400_000 bytes, ceil(/2048) = 18750.
|
||||
assert_eq!(extents[0].sector_count, 18750 - 93);
|
||||
// The extent must actually reach the last byte of the clip.
|
||||
let last_byte = clip.source_packet_count as u64 * BD_SOURCE_PACKET_BYTES as u64;
|
||||
let end_sector = (extents[0].start_lba + extents[0].sector_count) as u64;
|
||||
assert!(
|
||||
end_sector * SECTOR_BYTES_U64 >= last_byte,
|
||||
"extent stops at sector {end_sector} but the clip runs to byte {last_byte}"
|
||||
);
|
||||
}
|
||||
|
||||
/// An out_time that falls strictly BETWEEN two EP entries resolves to
|
||||
/// the next entry (the SPN at-or-after out_time), not to the clip end.
|
||||
#[test]
|
||||
fn get_extents_out_time_between_entries_uses_next_ep() {
|
||||
let clip = three_point_clip();
|
||||
// 6000 lies between EP PTS 5120 (SPN 2000) and 7680 (SPN 3000).
|
||||
let extents = clip.get_extents(2560, 6000);
|
||||
assert_eq!(extents.len(), 1);
|
||||
assert_eq!(extents[0].start_lba, 93);
|
||||
// end: SPN 3000 × 192 = 576_000 bytes, ceil(/2048) = 282.
|
||||
assert_eq!(extents[0].sector_count, 282 - 93);
|
||||
}
|
||||
|
||||
/// An in_time that falls strictly BETWEEN two EP entries resolves to the
|
||||
/// PRECEDING entry (decoding must start at an entry point at or before
|
||||
/// the requested time), and the SPN→byte→sector arithmetic is
|
||||
/// ×BD_SOURCE_PACKET_BYTES then floor/ceil ÷SECTOR_BYTES_U64.
|
||||
#[test]
|
||||
fn get_extents_in_time_between_entries_uses_previous_ep() {
|
||||
let clip = three_point_clip();
|
||||
// 3000 lies between EP PTS 2560 (SPN 1000) and 5120 (SPN 2000):
|
||||
// the preceding entry point is SPN 1000 → sector floor(192000/2048)
|
||||
// = 93. Picking the FOLLOWING entry (SPN 2000 → sector 187) would
|
||||
// start the extent after the I-frame the decoder needs.
|
||||
let extents = clip.get_extents(3000, 6000);
|
||||
assert_eq!(extents.len(), 1);
|
||||
assert_eq!(extents[0].start_lba, 93);
|
||||
assert_eq!(extents[0].sector_count, 282 - 93);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Section-offset gates in `parse`.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A prog_info_start of 0 means "no ProgramInfo section" — the CLPI
|
||||
/// header bytes at offset 0 must NOT be reinterpreted as a ProgramInfo
|
||||
/// table. The fixture is crafted so that parsing from offset 0 WOULD
|
||||
/// yield a stream (num_programs at [5], a second program header whose
|
||||
/// num_streams byte at [20] is 1, then a well-formed stream record), so
|
||||
/// the empty result can only come from the `prog_info_start > 0` gate.
|
||||
#[test]
|
||||
fn prog_info_start_zero_does_not_parse_header_as_program_info() {
|
||||
let mut data = build_clpi(1000, None);
|
||||
data[5] = 2; // num_programs = 2 if read from offset 0
|
||||
// program 0 header = data[6..14]; its num_streams byte is data[12],
|
||||
// which is prog_info_start's first byte and must stay 0.
|
||||
data[20] = 1; // program 1 (header data[14..22]) declares 1 stream
|
||||
data[22..24].copy_from_slice(&0x1011u16.to_be_bytes()); // pid
|
||||
data[24] = 2; // sci_len
|
||||
data[25] = 0x1B; // coding_type H.264
|
||||
data[26] = 0x61; // video format 6 / rate 1
|
||||
let clip = parse(&data).expect("should parse");
|
||||
assert!(
|
||||
clip.streams.is_empty(),
|
||||
"prog_info_start == 0 must mean absent, got {:?}",
|
||||
clip.streams
|
||||
);
|
||||
}
|
||||
|
||||
/// A cpi_start of 0 means "no CPI section" — the CLPI header bytes must
|
||||
/// not be reinterpreted as an EP map. The fixture sets prog_info_start
|
||||
/// to 0x0004_0000 purely so that, read as the 80-bit stream entry at
|
||||
/// data[10..18], it decodes to num_EP_coarse = 1 and a coarse entry
|
||||
/// would be produced. Only the `cpi_start > 0` gate keeps it empty.
|
||||
#[test]
|
||||
fn cpi_start_zero_does_not_parse_header_as_ep_map() {
|
||||
let mut data = build_clpi(1000, None);
|
||||
data[13] = 0x04; // → num_EP_coarse = 1 when data[10..18] is read as
|
||||
// the stream PID entry
|
||||
let clip = parse(&data).expect("should parse");
|
||||
assert!(
|
||||
clip.ep_coarse.is_empty(),
|
||||
"cpi_start == 0 must mean absent, got {:?}",
|
||||
clip.ep_coarse
|
||||
);
|
||||
assert!(clip.ep_fine.is_empty());
|
||||
}
|
||||
|
||||
/// A section offset that points INSIDE the 60-byte CLPI header (a
|
||||
/// hostile-disc value smaller than the header itself) must be handled
|
||||
/// without panicking; here both offsets are 3 and both sections decode
|
||||
/// to nothing.
|
||||
#[test]
|
||||
fn section_offsets_inside_header_do_not_panic() {
|
||||
let mut data = build_clpi(1000, None);
|
||||
data[12..16].copy_from_slice(&3u32.to_be_bytes()); // prog_info_start
|
||||
data[16..20].copy_from_slice(&3u32.to_be_bytes()); // cpi_start
|
||||
let clip = parse(&data).expect("should not panic");
|
||||
assert!(clip.streams.is_empty());
|
||||
assert!(clip.ep_coarse.is_empty());
|
||||
assert!(clip.ep_fine.is_empty());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// parse_program_info
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Secondary audio (0xA1 AC-3+ secondary, 0xA2 DTS-HD secondary) has the
|
||||
/// same stream_coding_info layout as primary audio: sci[1] carries
|
||||
/// audio_presentation_type in the high nibble and sampling_frequency in
|
||||
/// the low nibble, sci[2..5] the ISO 639-2 language. Both sub-fields and
|
||||
/// the language must be populated.
|
||||
#[test]
|
||||
fn program_info_secondary_audio_fields() {
|
||||
for coding in [c_ac3_plus_secondary(), c_dts_hd_secondary()] {
|
||||
let sci = vec![coding, 0x61, b'd', b'e', b'u'];
|
||||
let pi = build_program_info(&[(0x1A00, sci)]);
|
||||
let data = build_clpi_with_proginfo(100, &pi, None);
|
||||
let clip = parse(&data).expect("should parse");
|
||||
assert_eq!(clip.streams.len(), 1, "coding {coding:#04x}");
|
||||
let s = &clip.streams[0];
|
||||
assert_eq!(s.coding_type, coding);
|
||||
// 0x61: high nibble 6, low nibble 1 — distinct values, so a
|
||||
// swapped/ORed/XORed nibble extraction cannot pass.
|
||||
assert_eq!(s.audio_format, 6, "coding {coding:#04x}");
|
||||
assert_eq!(s.audio_rate, 1, "coding {coding:#04x}");
|
||||
assert_eq!(s.language, "deu", "coding {coding:#04x}");
|
||||
}
|
||||
}
|
||||
|
||||
fn c_ac3_plus_secondary() -> u8 {
|
||||
crate::consts::coding_type::AC3_PLUS_SECONDARY
|
||||
}
|
||||
fn c_dts_hd_secondary() -> u8 {
|
||||
crate::consts::coding_type::DTS_HD_SECONDARY
|
||||
}
|
||||
|
||||
/// A stream_coding_info of exactly 1 byte (coding_type only) is the
|
||||
/// minimum the parser accepts: the stream is recorded with its PID and
|
||||
/// coding_type, and every sub-field that needs more bytes stays empty.
|
||||
/// Notably a PG stream must NOT read sci[1..4] when only sci[0] exists.
|
||||
#[test]
|
||||
fn program_info_sci_len_one_yields_bare_stream() {
|
||||
let pi = build_program_info(&[(0x1200, vec![0x90u8])]);
|
||||
let data = build_clpi_with_proginfo(100, &pi, None);
|
||||
let clip = parse(&data).expect("should not panic");
|
||||
assert_eq!(clip.streams.len(), 1);
|
||||
assert_eq!(clip.streams[0].pid, 0x1200);
|
||||
assert_eq!(clip.streams[0].coding_type, 0x90);
|
||||
assert_eq!(clip.streams[0].language, "");
|
||||
}
|
||||
|
||||
/// Below the 6-byte ProgramInfo header (length(4)+reserved(1)+
|
||||
/// num_programs(1)) there is nothing to read; the length guard must fire
|
||||
/// before `data[5]`.
|
||||
#[test]
|
||||
fn program_info_below_header_size_is_empty() {
|
||||
for len in 0..6usize {
|
||||
assert!(parse_program_info(&vec![0u8; len]).is_empty(), "len={len}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A declared program whose 8-byte header runs past the section end must
|
||||
/// stop before reading num_streams at `data[pos + 6]`.
|
||||
#[test]
|
||||
fn program_info_truncated_program_header_is_empty() {
|
||||
// length(4) + reserved(1) + num_programs=1 (1) + only 4 of the 8
|
||||
// program-header bytes.
|
||||
let mut data = vec![0u8; 6];
|
||||
data[5] = 1;
|
||||
data.extend_from_slice(&[0u8; 4]);
|
||||
assert!(parse_program_info(&data).is_empty());
|
||||
}
|
||||
|
||||
/// A declared stream whose 3-byte header (pid(2)+sci_len(1)) runs past
|
||||
/// the section end must stop before reading the PID.
|
||||
#[test]
|
||||
fn program_info_truncated_stream_header_is_empty() {
|
||||
let mut data = vec![0u8; 6];
|
||||
data[5] = 1; // num_programs
|
||||
data.extend_from_slice(&[0u8; 8]); // program header
|
||||
data[6 + 6] = 1; // num_streams = 1
|
||||
data.extend_from_slice(&[0u8; 2]); // only 2 of the 3 stream bytes
|
||||
assert_eq!(data.len(), 16);
|
||||
assert!(parse_program_info(&data).is_empty());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// parse_cpi — low-level fixtures
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Pack the 80-bit stream PID entry body (ep_map[4..14]):
|
||||
/// reserved(10) + EP_stream_type(4) + num_EP_coarse(16) +
|
||||
/// num_EP_fine(18) + EP_map_start(32).
|
||||
fn pack_stream_header(num_coarse: u32, num_fine: u32, ep_map_start: u32) -> [u8; 10] {
|
||||
let packed: u128 = (1u128 << 66) // EP_stream_type = 1 (video)
|
||||
| ((num_coarse as u128) << 50)
|
||||
| ((num_fine as u128) << 32)
|
||||
| (ep_map_start as u128);
|
||||
let b = packed.to_be_bytes();
|
||||
let mut out = [0u8; 10];
|
||||
out.copy_from_slice(&b[6..16]);
|
||||
out
|
||||
}
|
||||
|
||||
/// Assemble a CPI section around one stream EP map.
|
||||
/// `cpi_length` overrides the declared length field (default: exact).
|
||||
/// `trailing` is appended AFTER the section, to model bytes belonging to
|
||||
/// a neighbouring CLPI section.
|
||||
fn assemble_cpi(
|
||||
header: &[u8; 10],
|
||||
stream_ep: &[u8],
|
||||
cpi_length: Option<u32>,
|
||||
trailing: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let mut ep_map = Vec::new();
|
||||
ep_map.push(0u8); // reserved
|
||||
ep_map.push(1u8); // num_stream_pid_entries
|
||||
ep_map.extend_from_slice(&0x1011u16.to_be_bytes());
|
||||
ep_map.extend_from_slice(header);
|
||||
ep_map.extend_from_slice(stream_ep);
|
||||
let declared = cpi_length.unwrap_or((2 + ep_map.len()) as u32);
|
||||
let mut cpi = Vec::new();
|
||||
cpi.extend_from_slice(&declared.to_be_bytes());
|
||||
cpi.extend_from_slice(&[0u8; 2]); // reserved + CPI_type
|
||||
cpi.extend_from_slice(&ep_map);
|
||||
cpi.extend_from_slice(trailing);
|
||||
cpi
|
||||
}
|
||||
|
||||
/// Below the 8-byte minimum there is no CPI section to read; the guard
|
||||
/// must fire before the 4-byte length field is decoded.
|
||||
#[test]
|
||||
fn parse_cpi_below_8_bytes_is_empty() {
|
||||
for len in 0..8usize {
|
||||
let (coarse, fine) = parse_cpi(&vec![0u8; len]).expect("no error");
|
||||
assert!(coarse.is_empty() && fine.is_empty(), "len={len}");
|
||||
}
|
||||
}
|
||||
|
||||
/// EP-map reads are bounded by the DECLARED cpi_length, not by the rest
|
||||
/// of the file. A CPI section that declares room for one coarse entry
|
||||
/// must yield one entry even when a second entry's worth of bytes
|
||||
/// follows in the adjacent section.
|
||||
#[test]
|
||||
fn cpi_length_clamps_reads_to_the_section() {
|
||||
let mut stream_ep = Vec::new();
|
||||
stream_ep.extend_from_slice(&20u32.to_be_bytes()); // fine_start (past end)
|
||||
// coarse 0: dword0 = ref_to_fine_id 0 | pts_coarse 0x11, spn 0x20000
|
||||
stream_ep.extend_from_slice(&0x11u32.to_be_bytes());
|
||||
stream_ep.extend_from_slice(&0x20000u32.to_be_bytes());
|
||||
// coarse 1 — inside the file, but OUTSIDE the declared section.
|
||||
stream_ep.extend_from_slice(&0x22u32.to_be_bytes());
|
||||
stream_ep.extend_from_slice(&0x40000u32.to_be_bytes());
|
||||
|
||||
// Declared length covers reserved(2) + ep_map header(14) +
|
||||
// fine_start(4) + ONE coarse entry(8) = 28.
|
||||
let hdr = pack_stream_header(2, 0, 14);
|
||||
let cpi = assemble_cpi(&hdr, &stream_ep, Some(28), &[]);
|
||||
let data = build_clpi(1000, Some(&cpi));
|
||||
let clip = parse(&data).expect("should parse");
|
||||
assert_eq!(
|
||||
clip.ep_coarse.len(),
|
||||
1,
|
||||
"second entry is outside cpi_length"
|
||||
);
|
||||
assert_eq!(clip.ep_coarse[0].pts_coarse, 0x11);
|
||||
}
|
||||
|
||||
/// An EP map too short to hold the 16-byte (2 + 14) stream PID entry
|
||||
/// must yield empty maps rather than decoding the 80-bit entry body.
|
||||
#[test]
|
||||
fn ep_map_shorter_than_stream_entry_is_empty() {
|
||||
// Declared cpi_length 14 → section is 18 bytes → ep_map is 12 bytes,
|
||||
// short of the 16 needed, but num_stream_pid_entries is non-zero.
|
||||
let hdr = pack_stream_header(1, 1, 14);
|
||||
let mut stream_ep = Vec::new();
|
||||
stream_ep.extend_from_slice(&4u32.to_be_bytes());
|
||||
stream_ep.extend_from_slice(&0u64.to_be_bytes());
|
||||
let cpi = assemble_cpi(&hdr, &stream_ep, Some(14), &[]);
|
||||
let data = build_clpi(1000, Some(&cpi));
|
||||
let clip = parse(&data).expect("should not panic");
|
||||
assert!(clip.ep_coarse.is_empty());
|
||||
assert!(clip.ep_fine.is_empty());
|
||||
}
|
||||
|
||||
/// The LAST coarse entry may end exactly at the end of the coarse table
|
||||
/// (a clip with no fine entries). The loop bound is `off + 8 > len`, so
|
||||
/// an entry finishing precisely at `len` is still read.
|
||||
#[test]
|
||||
fn last_coarse_entry_ending_at_table_end_is_kept() {
|
||||
let mut stream_ep = Vec::new();
|
||||
stream_ep.extend_from_slice(&20u32.to_be_bytes()); // fine_start == stream_ep.len()
|
||||
stream_ep.extend_from_slice(&0x11u32.to_be_bytes());
|
||||
stream_ep.extend_from_slice(&0x20000u32.to_be_bytes());
|
||||
stream_ep.extend_from_slice(&0x22u32.to_be_bytes());
|
||||
stream_ep.extend_from_slice(&0x40000u32.to_be_bytes());
|
||||
assert_eq!(stream_ep.len(), 20);
|
||||
|
||||
let hdr = pack_stream_header(2, 0, 14);
|
||||
let cpi = assemble_cpi(&hdr, &stream_ep, None, &[]);
|
||||
let data = build_clpi(1000, Some(&cpi));
|
||||
let clip = parse(&data).expect("should parse");
|
||||
assert_eq!(clip.ep_coarse.len(), 2, "last coarse entry was dropped");
|
||||
assert_eq!(clip.ep_coarse[0].pts_coarse, 0x11);
|
||||
assert_eq!(clip.ep_coarse[1].pts_coarse, 0x22);
|
||||
assert_eq!(clip.ep_coarse[1].spn_coarse, 0x40000);
|
||||
}
|
||||
|
||||
/// A fine-table start address past the end of the stream EP map yields
|
||||
/// no fine entries — and must not compute a negative remaining length.
|
||||
#[test]
|
||||
fn fine_start_past_stream_ep_yields_no_fine_entries() {
|
||||
let mut stream_ep = Vec::new();
|
||||
stream_ep.extend_from_slice(&1000u32.to_be_bytes()); // fine_start ≫ len
|
||||
stream_ep.extend_from_slice(&0x11u32.to_be_bytes());
|
||||
stream_ep.extend_from_slice(&0x20000u32.to_be_bytes());
|
||||
let hdr = pack_stream_header(1, 4, 14);
|
||||
let cpi = assemble_cpi(&hdr, &stream_ep, None, &[]);
|
||||
let data = build_clpi(1000, Some(&cpi));
|
||||
let clip = parse(&data).expect("should not panic");
|
||||
assert_eq!(clip.ep_coarse.len(), 1);
|
||||
assert!(clip.ep_fine.is_empty());
|
||||
}
|
||||
|
||||
/// num_EP_fine bounds the fine-entry read. It is an 18-bit field packed
|
||||
/// directly below num_EP_coarse in the 80-bit stream PID entry, so the
|
||||
/// count must be masked out of its neighbours: with num_EP_coarse = 1
|
||||
/// the bits above 18 are set, and a count that picked them up would run
|
||||
/// on and swallow the four dwords of trailing section bytes instead of
|
||||
/// the two entries the header declares.
|
||||
#[test]
|
||||
fn num_fine_is_masked_to_18_bits_and_bounds_the_read() {
|
||||
let mut stream_ep = Vec::new();
|
||||
stream_ep.extend_from_slice(&12u32.to_be_bytes()); // fine_start
|
||||
// one coarse entry (8 bytes) so num_EP_coarse = 1 sets the bits
|
||||
// immediately above the num_EP_fine field
|
||||
stream_ep.extend_from_slice(&0x11u32.to_be_bytes());
|
||||
stream_ep.extend_from_slice(&0x20000u32.to_be_bytes());
|
||||
// FOUR fine dwords present, but only TWO declared.
|
||||
for (pts, spn) in [(7u32, 0x111u32), (9, 0x222), (11, 0x333), (13, 0x444)] {
|
||||
stream_ep.extend_from_slice(&((pts << 17) | spn).to_be_bytes());
|
||||
}
|
||||
let hdr = pack_stream_header(1, 2, 14);
|
||||
let cpi = assemble_cpi(&hdr, &stream_ep, None, &[]);
|
||||
let data = build_clpi(1000, Some(&cpi));
|
||||
let clip = parse(&data).expect("should parse");
|
||||
assert_eq!(clip.ep_coarse.len(), 1);
|
||||
assert_eq!(clip.ep_fine.len(), 2, "read past the declared num_EP_fine");
|
||||
assert_eq!(clip.ep_fine[0].pts_fine, 7);
|
||||
assert_eq!(clip.ep_fine[0].spn_fine, 0x111);
|
||||
assert_eq!(clip.ep_fine[1].pts_fine, 9);
|
||||
assert_eq!(clip.ep_fine[1].spn_fine, 0x222);
|
||||
}
|
||||
|
||||
/// An EP map of exactly 16 bytes is the minimum that holds the 2-byte
|
||||
/// EP-map header plus one 14-byte stream PID entry, so it must be read,
|
||||
/// not rejected. Built byte-by-byte (stream_PID 0, EP_stream_type 0)
|
||||
/// so that EP_map_start = 2 lands the stream EP map on a zero
|
||||
/// fine-table start address and one fine entry is decoded.
|
||||
#[test]
|
||||
fn ep_map_of_exactly_16_bytes_is_read() {
|
||||
// 80-bit stream entry: reserved(10)+EP_stream_type(4)+
|
||||
// num_EP_coarse(16)+num_EP_fine(18)+EP_map_start(32), all zero
|
||||
// except num_EP_fine = 1 and EP_map_start = 2.
|
||||
let packed: u128 = (1u128 << 32) | 2;
|
||||
let b = packed.to_be_bytes();
|
||||
let mut ep_map = vec![0u8, 1u8, 0u8, 0u8]; // reserved, 1 entry, PID 0
|
||||
ep_map.extend_from_slice(&b[6..16]);
|
||||
ep_map.extend_from_slice(&[0u8, 0u8]);
|
||||
assert_eq!(ep_map.len(), 16);
|
||||
|
||||
let mut cpi = Vec::new();
|
||||
cpi.extend_from_slice(&((2 + ep_map.len()) as u32).to_be_bytes());
|
||||
cpi.extend_from_slice(&[0u8; 2]);
|
||||
cpi.extend_from_slice(&ep_map);
|
||||
let data = build_clpi(1000, Some(&cpi));
|
||||
let clip = parse(&data).expect("should parse");
|
||||
assert_eq!(clip.ep_fine.len(), 1);
|
||||
assert!(clip.ep_coarse.is_empty());
|
||||
}
|
||||
|
||||
/// EP_map_start is 32 bits wide and straddles the hi/lo split of the
|
||||
/// 80-bit stream PID entry: its high 16 bits come from `hi`, its low 16
|
||||
/// from the trailing two bytes. A section whose stream EP map starts at
|
||||
/// 0x1_0000 (beyond what the low half alone can express) must be found
|
||||
/// at that offset — the low half here is zero, so dropping the high
|
||||
/// half would resolve the offset to 0.
|
||||
#[test]
|
||||
fn ep_map_start_above_16_bits_is_honoured() {
|
||||
const START: usize = 0x1_0000;
|
||||
let hdr = pack_stream_header(1, 0, START as u32);
|
||||
let mut stream_ep = vec![0u8; START - 14]; // pad so the real map lands at START
|
||||
stream_ep.extend_from_slice(&12u32.to_be_bytes()); // fine_start == len
|
||||
stream_ep.extend_from_slice(&0x0000_2AAAu32.to_be_bytes()); // pts_coarse
|
||||
stream_ep.extend_from_slice(&0x5555_0000u32.to_be_bytes()); // spn_coarse
|
||||
let cpi = assemble_cpi(&hdr, &stream_ep, None, &[]);
|
||||
let data = build_clpi(1000, Some(&cpi));
|
||||
let clip = parse(&data).expect("should parse");
|
||||
assert_eq!(clip.ep_coarse.len(), 1);
|
||||
assert_eq!(clip.ep_coarse[0].pts_coarse, 0x2AAA);
|
||||
assert_eq!(clip.ep_coarse[0].spn_coarse, 0x5555_0000);
|
||||
assert!(clip.ep_fine.is_empty());
|
||||
}
|
||||
|
||||
/// EP_map_start is a 32-bit disc field; a value below 4 points back
|
||||
/// into the stream PID entry table itself. The bounds check must handle
|
||||
/// it without underflowing, and the read must stay inside the CPI
|
||||
/// section. With EP_map_start = 0 the "stream EP map" is the whole EP
|
||||
/// map, so the one declared coarse entry decodes out of the 80-bit
|
||||
/// stream entry body — garbage, but bounded and deterministic.
|
||||
#[test]
|
||||
fn ep_map_start_below_4_does_not_underflow() {
|
||||
let mut stream_ep = Vec::new();
|
||||
stream_ep.extend_from_slice(&4u32.to_be_bytes());
|
||||
stream_ep.extend_from_slice(&[0u8; 8]);
|
||||
let hdr = pack_stream_header(1, 0, 0); // EP_map_start = 0
|
||||
let cpi = assemble_cpi(&hdr, &stream_ep, None, &[]);
|
||||
let data = build_clpi(1000, Some(&cpi));
|
||||
let clip = parse(&data).expect("should not panic");
|
||||
assert_eq!(clip.ep_coarse.len(), 1);
|
||||
// dword0 = the first 4 bytes of the 80-bit stream entry body:
|
||||
// 0x0004_0004 → ref_to_fine_id = 16, pts_coarse = 4.
|
||||
assert_eq!(clip.ep_coarse[0].ref_to_fine_id, 16);
|
||||
assert_eq!(clip.ep_coarse[0].pts_coarse, 4);
|
||||
assert_eq!(clip.ep_coarse[0].spn_coarse, 0);
|
||||
assert!(clip.ep_fine.is_empty());
|
||||
}
|
||||
|
||||
/// A stream EP map occupying exactly the last 4 bytes of the EP map
|
||||
/// (EP_map_start + 4 == ep_map.len()) is in bounds and is read: the
|
||||
/// bound is `>`, not `>=`. Here fine_start is 0, so the fine table
|
||||
/// overlaps the stream EP map's own header word — degenerate, but it
|
||||
/// must stay inside the section and yield exactly one (zero) entry
|
||||
/// rather than panicking or reading past the CPI section.
|
||||
#[test]
|
||||
fn stream_ep_map_at_section_end_is_read() {
|
||||
let stream_ep = [0u8; 4]; // fine_start = 0
|
||||
let hdr = pack_stream_header(0, 1, 14);
|
||||
let cpi = assemble_cpi(&hdr, &stream_ep, None, &[]);
|
||||
let data = build_clpi(1000, Some(&cpi));
|
||||
let clip = parse(&data).expect("should not panic");
|
||||
assert!(clip.ep_coarse.is_empty());
|
||||
assert_eq!(clip.ep_fine.len(), 1);
|
||||
assert_eq!(clip.ep_fine[0].pts_fine, 0);
|
||||
assert_eq!(clip.ep_fine[0].spn_fine, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+413
@@ -1923,4 +1923,417 @@ mod tests {
|
||||
title.chapter_times[0]
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Added: cell-category bit isolation, palette/program-map arithmetic,
|
||||
// and the malformed-language salvage paths.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Each low flag of the cell-category byte comes from its OWN bit and no
|
||||
/// other: bit3 seamless_play, bit2 interleaved, bit1 stc_discontinuity,
|
||||
/// bit0 seamless_angle (DVD-Video cell playback `cell_category`).
|
||||
/// Setting one bit must leave all three siblings clear.
|
||||
#[test]
|
||||
fn cell_category_low_flags_are_bit_isolated() {
|
||||
let c = CellCategory::decode(0x00);
|
||||
assert_eq!(
|
||||
[
|
||||
c.seamless_play,
|
||||
c.interleaved,
|
||||
c.stc_discontinuity,
|
||||
c.seamless_angle
|
||||
],
|
||||
[false, false, false, false],
|
||||
"category 0x00 sets no flag"
|
||||
);
|
||||
for bit in 0..4u8 {
|
||||
let c = CellCategory::decode(1u8 << bit);
|
||||
assert_eq!(
|
||||
[
|
||||
c.seamless_play,
|
||||
c.interleaved,
|
||||
c.stc_discontinuity,
|
||||
c.seamless_angle
|
||||
],
|
||||
[bit == 3, bit == 2, bit == 1, bit == 0],
|
||||
"only bit {bit} may be set"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `is_plain_feature` requires BOTH block_mode and block_type to be zero.
|
||||
/// A cell inside a block is not plain feature content even when the other
|
||||
/// field happens to be zero.
|
||||
#[test]
|
||||
fn is_plain_feature_requires_both_block_fields_zero() {
|
||||
assert!(CellCategory::decode(0x00).is_plain_feature());
|
||||
// block_type = 1, block_mode = 0
|
||||
assert!(!CellCategory::decode(0b0001_0000).is_plain_feature());
|
||||
// block_mode = 1, block_type = 0
|
||||
assert!(!CellCategory::decode(0b0100_0000).is_plain_feature());
|
||||
assert!(!CellCategory::decode(0b0101_0000).is_plain_feature());
|
||||
}
|
||||
|
||||
/// be_u32 reads four CONSECUTIVE big-endian bytes from `offset`.
|
||||
/// All four values are distinct so a repeated or skipped index shows up.
|
||||
#[test]
|
||||
fn be_u32_reads_four_consecutive_be_bytes() {
|
||||
let data = [0xFFu8, 0x01, 0x02, 0x03, 0x04, 0xFF];
|
||||
assert_eq!(be_u32(&data, 1).unwrap(), 0x0102_0304);
|
||||
}
|
||||
|
||||
// ── malformed language codes ─────────────────────────────────────────
|
||||
|
||||
/// The ISO 639 language code in the audio/subtitle attribute block is
|
||||
/// two bytes at +2. Only a pair of lowercase a-z bytes is taken verbatim;
|
||||
/// anything else falls through to the ASCII-alphanumeric salvage, which
|
||||
/// keeps only the usable characters. A byte outside a-z must never end up
|
||||
/// in the language string.
|
||||
#[test]
|
||||
fn language_code_rejects_non_lowercase_bytes() {
|
||||
// (byte0, byte1, expected language)
|
||||
let cases: [(u8, u8, &str); 8] = [
|
||||
(b'e', b'n', "en"), // both in range → verbatim
|
||||
(0x21, b'n', "n"), // '!' is below 'a'
|
||||
(0x7B, b'n', "n"), // '{' is above 'z'
|
||||
(b'e', 0x21, "e"), // second byte below 'a'
|
||||
(b'e', 0x7B, "e"), // second byte above 'z'
|
||||
(b'E', 0x00, "E"), // only the second byte is zero
|
||||
(0x00, b'E', "E"), // only the first byte is zero
|
||||
(0x00, 0x00, ""), // both zero → unset
|
||||
];
|
||||
for (b0, b1, want) in cases {
|
||||
let mut audio = vec![0u8; 8];
|
||||
audio[2] = b0;
|
||||
audio[3] = b1;
|
||||
assert_eq!(
|
||||
parse_audio_attr(&audio, 0).unwrap().language,
|
||||
want,
|
||||
"audio language for ({b0:#04x}, {b1:#04x})"
|
||||
);
|
||||
|
||||
let mut sub = vec![0u8; 6];
|
||||
sub[2] = b0;
|
||||
sub[3] = b1;
|
||||
assert_eq!(
|
||||
parse_subtitle_attr(&sub, 0).unwrap().language,
|
||||
want,
|
||||
"subtitle language for ({b0:#04x}, {b1:#04x})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── PGC fixtures ─────────────────────────────────────────────────────
|
||||
|
||||
/// Build a standalone PGC starting at offset 0.
|
||||
///
|
||||
/// Layout: 0xEA-byte header, then the program map (one byte per program,
|
||||
/// the 1-based first cell number), then the 24-byte cell playback table.
|
||||
/// `cells` are `(category, BCD time, first_sector, last_sector)`.
|
||||
fn build_pgc(
|
||||
pgc_time: [u8; 4],
|
||||
cells: &[(u8, [u8; 4], u32, u32)],
|
||||
programs: &[u8],
|
||||
palette: Option<[[u8; 4]; 16]>,
|
||||
) -> Vec<u8> {
|
||||
let mut d = vec![0u8; 0xEA];
|
||||
d[0x02] = programs.len() as u8;
|
||||
d[0x03] = cells.len() as u8;
|
||||
d[0x04..0x08].copy_from_slice(&pgc_time);
|
||||
if let Some(p) = palette {
|
||||
for (i, c) in p.iter().enumerate() {
|
||||
d[0xA4 + i * 4..0xA4 + i * 4 + 4].copy_from_slice(c);
|
||||
}
|
||||
}
|
||||
let pgm_off = 0xEAusize;
|
||||
let cell_off = pgm_off + programs.len();
|
||||
if !programs.is_empty() {
|
||||
d[0xE6..0xE8].copy_from_slice(&(pgm_off as u16).to_be_bytes());
|
||||
}
|
||||
if !cells.is_empty() {
|
||||
d[0xE8..0xEA].copy_from_slice(&(cell_off as u16).to_be_bytes());
|
||||
}
|
||||
d.extend_from_slice(programs);
|
||||
for &(cat, time, first, last) in cells {
|
||||
let mut c = vec![0u8; 24];
|
||||
c[0] = cat;
|
||||
c[4..8].copy_from_slice(&time);
|
||||
c[8..12].copy_from_slice(&first.to_be_bytes());
|
||||
c[20..24].copy_from_slice(&last.to_be_bytes());
|
||||
d.extend_from_slice(&c);
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// BCD playback time of `secs` seconds (< 60) at the 29.97 fps flag.
|
||||
fn bcd_secs(secs: u8) -> [u8; 4] {
|
||||
assert!(secs < 60);
|
||||
[0, 0, ((secs / 10) << 4) | (secs % 10), 0b11_000000]
|
||||
}
|
||||
|
||||
/// When the PGC-level playback time is zero, the duration is recomputed
|
||||
/// as the SUM of every cell's own BCD time, each read from its own
|
||||
/// 24-byte cell playback record at +4.
|
||||
#[test]
|
||||
fn pgc_zero_duration_recomputed_as_sum_of_cell_times() {
|
||||
let pgc = build_pgc(
|
||||
[0, 0, 0, 0],
|
||||
&[
|
||||
(0x00, bcd_secs(10), 100, 199),
|
||||
(0x00, bcd_secs(20), 200, 299),
|
||||
(0x00, bcd_secs(31), 300, 399),
|
||||
],
|
||||
&[],
|
||||
None,
|
||||
);
|
||||
let title = parse_pgc(&pgc, 0, 3).unwrap();
|
||||
assert_eq!(title.cells.len(), 3);
|
||||
assert_eq!(title.cells[0].duration_secs, 10.0);
|
||||
assert_eq!(title.cells[1].duration_secs, 20.0);
|
||||
assert_eq!(title.cells[2].duration_secs, 31.0);
|
||||
assert_eq!(
|
||||
title.duration_secs, 61.0,
|
||||
"recomputed duration must be the sum of the distinct cell times"
|
||||
);
|
||||
}
|
||||
|
||||
/// Chapter times come from the program map: each program's byte is the
|
||||
/// 1-based number of its first cell, and the chapter time is the sum of
|
||||
/// the durations of every cell BEFORE it. Distinct cell durations mean a
|
||||
/// misread program byte or a mis-strided cell table changes the result.
|
||||
#[test]
|
||||
fn pgc_chapter_times_sum_preceding_cell_durations() {
|
||||
let pgc = build_pgc(
|
||||
bcd_secs(59),
|
||||
&[
|
||||
(0x00, bcd_secs(10), 0, 9),
|
||||
(0x00, bcd_secs(20), 10, 19),
|
||||
(0x00, bcd_secs(31), 20, 29),
|
||||
],
|
||||
&[1, 2, 3],
|
||||
None,
|
||||
);
|
||||
let title = parse_pgc(&pgc, 0, 3).unwrap();
|
||||
assert_eq!(title.chapter_times, vec![0.0, 10.0, 30.0]);
|
||||
}
|
||||
|
||||
/// A program map offset of 0 means there is no program map, so no chapter
|
||||
/// times may be produced — the PGC header must not be read as one. Same
|
||||
/// for a cell playback offset of 0.
|
||||
#[test]
|
||||
fn pgc_absent_program_map_or_cell_table_yields_no_chapter_times() {
|
||||
// programs declared, but pgm_map_offset patched to 0
|
||||
let mut pgc = build_pgc(
|
||||
bcd_secs(30),
|
||||
&[(0x00, bcd_secs(10), 0, 9), (0x00, bcd_secs(20), 10, 19)],
|
||||
&[1, 2],
|
||||
None,
|
||||
);
|
||||
pgc[0xE6..0xE8].copy_from_slice(&0u16.to_be_bytes());
|
||||
let title = parse_pgc(&pgc, 0, 2).unwrap();
|
||||
assert!(
|
||||
title.chapter_times.is_empty(),
|
||||
"no program map → no chapter times, got {:?}",
|
||||
title.chapter_times
|
||||
);
|
||||
|
||||
// program map present, but cell_playback_offset patched to 0
|
||||
let mut pgc = build_pgc(
|
||||
bcd_secs(30),
|
||||
&[(0x00, bcd_secs(10), 0, 9), (0x00, bcd_secs(20), 10, 19)],
|
||||
&[1, 2],
|
||||
None,
|
||||
);
|
||||
pgc[0xE8..0xEA].copy_from_slice(&0u16.to_be_bytes());
|
||||
let title = parse_pgc(&pgc, 0, 2).unwrap();
|
||||
assert!(title.cells.is_empty());
|
||||
assert!(
|
||||
title.chapter_times.is_empty(),
|
||||
"no cell table → no chapter times, got {:?}",
|
||||
title.chapter_times
|
||||
);
|
||||
}
|
||||
|
||||
/// A declared cell count larger than the cell table actually holds must
|
||||
/// not read past the end while collecting durations for the chapter-time
|
||||
/// calculation; the missing cells contribute zero.
|
||||
#[test]
|
||||
fn pgc_cell_count_overshoot_does_not_read_past_end() {
|
||||
let mut pgc = build_pgc(
|
||||
bcd_secs(30),
|
||||
&[(0x00, bcd_secs(10), 0, 9), (0x00, bcd_secs(20), 10, 19)],
|
||||
&[1, 2],
|
||||
None,
|
||||
);
|
||||
pgc[0x03] = 8; // declare 8 cells; only 2 records exist
|
||||
let title = parse_pgc(&pgc, 0, 2).unwrap();
|
||||
assert_eq!(title.cells.len(), 2, "only the readable cells are kept");
|
||||
// Program 2 starts at cell 2, so its time is cell 0's duration.
|
||||
assert_eq!(title.chapter_times, vec![0.0, 10.0]);
|
||||
}
|
||||
|
||||
/// A program map that runs past the end of the data must stop at the
|
||||
/// buffer end rather than reading past it. The fixture's map begins at
|
||||
/// PGC+0xEA and the buffer holds 50 bytes from there, so a declared
|
||||
/// count of 255 programs must yield exactly 50 chapter times.
|
||||
#[test]
|
||||
fn pgc_program_map_past_end_stops() {
|
||||
let mut pgc = build_pgc(
|
||||
bcd_secs(30),
|
||||
&[(0x00, bcd_secs(10), 0, 9), (0x00, bcd_secs(20), 10, 19)],
|
||||
&[1, 2],
|
||||
None,
|
||||
);
|
||||
pgc[0x02] = 255; // declare 255 programs
|
||||
let available = pgc.len() - 0xEA;
|
||||
assert_eq!(available, 50, "fixture: 2 map bytes + 2 cells of 24");
|
||||
let title = parse_pgc(&pgc, 0, 2).unwrap();
|
||||
assert_eq!(
|
||||
title.chapter_times.len(),
|
||||
available,
|
||||
"the program map walk must stop exactly at the buffer end"
|
||||
);
|
||||
assert_eq!(title.chapter_times[0], 0.0);
|
||||
assert_eq!(title.chapter_times[1], 10.0);
|
||||
}
|
||||
|
||||
/// The subtitle palette is 16 entries of 4 bytes at PGC+0xA4, each
|
||||
/// `[padding, Y, Cb, Cr]`. Every byte of every entry is distinct here, so
|
||||
/// a wrong stride, a wrong base or a shifted component shows up.
|
||||
#[test]
|
||||
fn pgc_palette_entries_read_at_correct_stride() {
|
||||
let mut pal = [[0u8; 4]; 16];
|
||||
for (i, c) in pal.iter_mut().enumerate() {
|
||||
let b = i as u8;
|
||||
*c = [0x10 + b, 0x30 + b, 0x50 + b, 0x70 + b];
|
||||
}
|
||||
let pgc = build_pgc(bcd_secs(30), &[], &[], Some(pal));
|
||||
let title = parse_pgc(&pgc, 0, 1).unwrap();
|
||||
let got = title.palette.expect("palette present");
|
||||
assert_eq!(got.len(), 16);
|
||||
for i in 0..16 {
|
||||
let b = i as u8;
|
||||
assert_eq!(
|
||||
got[i],
|
||||
[0x10 + b, 0x30 + b, 0x50 + b, 0x70 + b],
|
||||
"palette entry {i}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A palette is "present" when ANY of Y, Cb or Cr is non-zero in ANY
|
||||
/// entry — a single non-zero chroma component is enough. Only the
|
||||
/// padding byte [0] is ignored.
|
||||
#[test]
|
||||
fn pgc_palette_present_on_any_single_nonzero_component() {
|
||||
for comp in 1..4usize {
|
||||
let mut pal = [[0u8; 4]; 16];
|
||||
pal[7][comp] = 0x40; // exactly one non-zero component, in one entry
|
||||
let pgc = build_pgc(bcd_secs(30), &[], &[], Some(pal));
|
||||
let title = parse_pgc(&pgc, 0, 1).unwrap();
|
||||
assert!(
|
||||
title.palette.is_some(),
|
||||
"component {comp} alone must mark the palette present"
|
||||
);
|
||||
}
|
||||
// Only the padding byte set → still empty.
|
||||
let mut pal = [[0u8; 4]; 16];
|
||||
for c in pal.iter_mut() {
|
||||
c[0] = 0xFF;
|
||||
}
|
||||
let pgc = build_pgc(bcd_secs(30), &[], &[], Some(pal));
|
||||
assert!(parse_pgc(&pgc, 0, 1).unwrap().palette.is_none());
|
||||
}
|
||||
|
||||
// ── PGCIT ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Build a VTS_PGCIT at offset 0: VTS_PGC_Ns(2) + reserved(2) +
|
||||
/// VTS_PGCIT_EA(4), then one 8-byte VTS_PGCI_SRP per PGC
|
||||
/// (VTS_PGC_CAT(4) + VTS_PGCI_SA(4), the PGC's byte offset from the
|
||||
/// VTS_PGCIT start). `pgcs` are appended after the SRP table.
|
||||
fn build_pgcit(pgcs: &[Vec<u8>]) -> Vec<u8> {
|
||||
let mut d = vec![0u8; 8 + pgcs.len() * 8];
|
||||
d[0..2].copy_from_slice(&(pgcs.len() as u16).to_be_bytes());
|
||||
let mut off = d.len();
|
||||
for (i, p) in pgcs.iter().enumerate() {
|
||||
let e = 8 + i * 8;
|
||||
d[e + 4..e + 8].copy_from_slice(&(off as u32).to_be_bytes());
|
||||
off += p.len();
|
||||
}
|
||||
for p in pgcs {
|
||||
d.extend_from_slice(p);
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// Each VTS_PGCI_SRP is 8 bytes and the PGC start address is the second
|
||||
/// word of its own entry, so title N must resolve to PGC N. Two PGCs
|
||||
/// with distinct durations catch an entry read at the wrong stride or
|
||||
/// from the wrong entry.
|
||||
#[test]
|
||||
fn pgcit_entry_stride_selects_the_right_pgc() {
|
||||
let pgc0 = build_pgc(bcd_secs(11), &[(0x00, bcd_secs(11), 0, 9)], &[], None);
|
||||
let pgc1 = build_pgc(bcd_secs(22), &[(0x00, bcd_secs(22), 50, 59)], &[], None);
|
||||
let data = build_pgcit(&[pgc0, pgc1]);
|
||||
|
||||
// vts_title_num is 1-based: title 2 → PGC index 1.
|
||||
let titles = parse_pgcit(&data, 0, &[(5, 2)]).unwrap();
|
||||
assert_eq!(titles.len(), 1);
|
||||
assert_eq!(titles[0].duration_secs, 22.0);
|
||||
assert_eq!(titles[0].cells[0].first_sector, 50);
|
||||
|
||||
let titles = parse_pgcit(&data, 0, &[(5, 1)]).unwrap();
|
||||
assert_eq!(titles.len(), 1);
|
||||
assert_eq!(titles[0].duration_secs, 11.0);
|
||||
assert_eq!(titles[0].cells[0].first_sector, 0);
|
||||
|
||||
// Both titles, in order.
|
||||
let titles = parse_pgcit(&data, 0, &[(5, 1), (7, 2)]).unwrap();
|
||||
assert_eq!(titles.len(), 2);
|
||||
assert_eq!(titles[0].duration_secs, 11.0);
|
||||
assert_eq!(titles[1].duration_secs, 22.0);
|
||||
}
|
||||
|
||||
/// A VTS_PGCIT whose 8-byte header ends exactly at the end of the data is
|
||||
/// a complete header with no SRP entries: an empty title list, not an
|
||||
/// error. Below 8 bytes the header itself is truncated → IfoParse.
|
||||
#[test]
|
||||
fn pgcit_header_boundary() {
|
||||
let data = vec![0u8; 8];
|
||||
let titles = parse_pgcit(&data, 0, &[(5, 1)]).expect("complete header parses");
|
||||
assert!(titles.is_empty());
|
||||
for len in 0..8usize {
|
||||
assert!(
|
||||
parse_pgcit(&vec![0u8; len], 0, &[(5, 1)]).is_err(),
|
||||
"len={len}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An SRP entry that runs past the end of the data is SKIPPED, leaving
|
||||
/// an empty title list — a truncated entry table must not turn into a
|
||||
/// parse error for the whole PGCIT.
|
||||
#[test]
|
||||
fn pgcit_entry_past_end_is_skipped_not_an_error() {
|
||||
// 8-byte header declaring 3 PGCs, then only 12 bytes of table: the
|
||||
// entry for PGC index 2 (offset 24..32) is entirely past the end.
|
||||
let mut data = vec![0u8; 20];
|
||||
data[0..2].copy_from_slice(&3u16.to_be_bytes());
|
||||
let titles = parse_pgcit(&data, 0, &[(5, 3)])
|
||||
.expect("a truncated SRP entry is skipped, not an error");
|
||||
assert!(titles.is_empty());
|
||||
}
|
||||
|
||||
/// A TT_SRP entry is 12 bytes: playback_type(1) + angles(1) +
|
||||
/// number_of_PTTs(2) + parental_mask(2) + VTSN(1) + VTS_TTN(1) +
|
||||
/// VTS_start_sector(4). The chapter count is the 16-bit field at +2, and
|
||||
/// each entry's own value must be carried through. Distinct counts per
|
||||
/// entry catch a read from a neighbouring offset.
|
||||
#[test]
|
||||
fn tt_srpt_chapter_count_read_from_entry_offset_2() {
|
||||
let data = tt_srpt_bytes(3, &[(7, 1, 1), (13, 1, 2), (0x0102, 2, 1)]);
|
||||
let map = parse_tt_srpt(&data, 0).unwrap();
|
||||
assert_eq!(map[&1], vec![(7u16, 1u8), (13, 2)]);
|
||||
assert_eq!(map[&2], vec![(0x0102u16, 1u8)]);
|
||||
}
|
||||
}
|
||||
|
||||
+281
@@ -1416,4 +1416,285 @@ mod tests {
|
||||
data[8..12].copy_from_slice(&40u32.to_be_bytes()); // playlist_start = 40 = len
|
||||
assert!(parse(&data).is_err());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Added: STN-table block alignment and section-boundary hardening.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Build an MPLS from raw PlayItem bodies, with no PlayListMark section
|
||||
/// (mark_start = 0). Lets a test control item_length exactly.
|
||||
fn build_mpls_raw_items(items: &[Vec<u8>]) -> Vec<u8> {
|
||||
let playlist_start: u32 = 40;
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(b"MPLS0200");
|
||||
buf.extend_from_slice(&playlist_start.to_be_bytes());
|
||||
buf.extend_from_slice(&[0u8; 28]); // mark_start = 0, then padding
|
||||
let pl_start = buf.len();
|
||||
buf.extend_from_slice(&[0u8; 4]); // PlayList length placeholder
|
||||
buf.extend_from_slice(&[0u8; 2]); // reserved
|
||||
buf.extend_from_slice(&(items.len() as u16).to_be_bytes());
|
||||
buf.extend_from_slice(&[0u8; 2]); // num_sub_paths
|
||||
for it in items {
|
||||
buf.extend_from_slice(&(it.len() as u16).to_be_bytes());
|
||||
buf.extend_from_slice(it);
|
||||
}
|
||||
let pl_len = (buf.len() - pl_start - 4) as u32;
|
||||
buf[pl_start..pl_start + 4].copy_from_slice(&pl_len.to_be_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// The 20 bytes a PlayItem needs for clip_id(5) + codec_id(4) +
|
||||
/// connection_condition(1) + reserved(2) + IN_time(4) + OUT_time(4).
|
||||
fn play_item_20(clip: &[u8; 5], cc: u8, in_t: u32, out_t: u32) -> Vec<u8> {
|
||||
let mut it = Vec::new();
|
||||
it.extend_from_slice(clip);
|
||||
it.extend_from_slice(b"M2TS");
|
||||
it.push(cc);
|
||||
it.extend_from_slice(&[0u8; 2]);
|
||||
it.extend_from_slice(&in_t.to_be_bytes());
|
||||
it.extend_from_slice(&out_t.to_be_bytes());
|
||||
assert_eq!(it.len(), 20);
|
||||
it
|
||||
}
|
||||
|
||||
/// A PlayItem body of exactly 20 bytes carries every field the parser
|
||||
/// reads (the last is OUT_time at [16..20]), so it must be RECORDED,
|
||||
/// not skipped — and it has no STN table, which starts at byte 32.
|
||||
#[test]
|
||||
fn play_item_of_exactly_20_bytes_is_recorded_without_stn() {
|
||||
let data = build_mpls_raw_items(&[play_item_20(b"00007", 5, 90_000, 180_000)]);
|
||||
let pl = parse(&data).expect("a 20-byte PlayItem must parse");
|
||||
assert_eq!(pl.play_items.len(), 1);
|
||||
assert_eq!(pl.play_items[0].clip_id, "00007");
|
||||
assert_eq!(pl.play_items[0].in_time, 90_000);
|
||||
assert_eq!(pl.play_items[0].out_time, 180_000);
|
||||
assert_eq!(pl.play_items[0].connection_condition, 5);
|
||||
assert!(pl.streams.is_empty(), "no STN table exists below byte 32");
|
||||
}
|
||||
|
||||
/// A 40-byte MPLS whose PlayList section is exactly its 10-byte header
|
||||
/// (length(4)+reserved(2)+num_play_items(2)+num_sub_paths(2)) ending at
|
||||
/// EOF is structurally complete, not truncated: nothing the parser reads
|
||||
/// lies past the buffer, so it must parse to an empty playlist.
|
||||
#[test]
|
||||
fn minimum_size_mpls_with_empty_playlist_header_parses() {
|
||||
let mut data = vec![0u8; 40];
|
||||
data[0..4].copy_from_slice(b"MPLS");
|
||||
data[4..8].copy_from_slice(b"0200");
|
||||
data[8..12].copy_from_slice(&30u32.to_be_bytes()); // playlist_start + 10 == 40
|
||||
// mark_start (12..16) stays 0; num_play_items at data[36..38] is 0.
|
||||
let pl = parse(&data).expect("40-byte MPLS with a complete PlayList header must parse");
|
||||
assert!(pl.play_items.is_empty());
|
||||
assert!(pl.streams.is_empty());
|
||||
assert!(pl.marks.is_empty());
|
||||
}
|
||||
|
||||
/// A mark_start of 0 means "no PlayListMark section". The file header
|
||||
/// bytes at offset 0 must not be decoded as one — data[4..6] is the
|
||||
/// version string "02", which as a big-endian num_marks would be 12338.
|
||||
#[test]
|
||||
fn mark_start_zero_does_not_parse_header_as_marks() {
|
||||
let data = build_mpls_raw_items(&[play_item_20(b"00007", 1, 0, 90_000)]);
|
||||
assert_eq!(
|
||||
&data[12..16],
|
||||
&[0, 0, 0, 0],
|
||||
"fixture must have mark_start 0"
|
||||
);
|
||||
let pl = parse(&data).expect("should parse");
|
||||
assert!(
|
||||
pl.marks.is_empty(),
|
||||
"mark_start == 0 must mean absent, got {} marks",
|
||||
pl.marks.len()
|
||||
);
|
||||
}
|
||||
|
||||
/// Full STN table walk with every category populated and DISTINCT
|
||||
/// counts, so no count byte can be read from a neighbour's offset
|
||||
/// without changing the result.
|
||||
///
|
||||
/// Each secondary block is followed by its reference block(s), which
|
||||
/// per the BD STN table are num_refs(1) + reserved(1) + one byte per
|
||||
/// ref + one padding byte when the ref count is odd. Every ref count
|
||||
/// here is 1 — the value that distinguishes `n % 2` (=1) from `n / 2`
|
||||
/// (=0) — so a wrong skip length misaligns the cursor and every
|
||||
/// following stream decodes from the wrong offset. IG entries are
|
||||
/// consumed to keep the cursor aligned but never retained.
|
||||
#[test]
|
||||
fn full_stn_table_block_alignment() {
|
||||
let mut entries: Vec<Vec<u8>> = Vec::new();
|
||||
entries.push(build_stream_entry_video(0x1011, 0x1B, 6, 1, None));
|
||||
entries.push(build_stream_entry_audio(0x1100, 0x83, 6, 1, b"eng"));
|
||||
entries.push(build_stream_entry_audio(0x1101, 0x86, 3, 1, b"fra"));
|
||||
entries.push(build_stream_entry_pg(0x1200, 0x90, b"eng"));
|
||||
entries.push(build_stream_entry_pg(0x1201, 0x90, b"fra"));
|
||||
entries.push(build_stream_entry_pg(0x1202, 0x90, b"deu"));
|
||||
for i in 0..4u16 {
|
||||
entries.push(build_stream_entry_pg(0x1400 + i, 0x91, b"eng"));
|
||||
}
|
||||
// secondary audio + its secondary-audio ref block (1 ref → 1 pad)
|
||||
let mut sec_audio = build_stream_entry_audio(0x1A00, 0x83, 3, 1, b"spa");
|
||||
sec_audio.extend_from_slice(&[1, 0, 0x55, 0x00]);
|
||||
entries.push(sec_audio);
|
||||
// secondary video + audio-ref block + PiP-PG-ref block
|
||||
let mut sec_video = build_stream_entry_video(0x1B00, 0x1B, 4, 1, None);
|
||||
sec_video.extend_from_slice(&[1, 0, 0x55, 0x00]);
|
||||
sec_video.extend_from_slice(&[1, 0, 0x66, 0x00]);
|
||||
entries.push(sec_video);
|
||||
// PiP PG + its ref block
|
||||
let mut pip_pg = build_stream_entry_pg(0x1C00, 0x90, b"jpn");
|
||||
pip_pg.extend_from_slice(&[1, 0, 0x77, 0x00]);
|
||||
entries.push(pip_pg);
|
||||
// Dolby Vision enhancement layer
|
||||
entries.push(build_stream_entry_video(0x1015, 0x24, 8, 1, Some(0x12)));
|
||||
|
||||
let data = build_mpls(
|
||||
&[(b"00001", 1, 0, 9_000_000)],
|
||||
(1, 2, 3, 4, 1, 1, 1, 1),
|
||||
&entries,
|
||||
);
|
||||
let pl = parse(&data).expect("should parse");
|
||||
|
||||
let got: Vec<(u8, u16, bool)> = pl
|
||||
.streams
|
||||
.iter()
|
||||
.map(|s| (s.stream_type, s.pid, s.secondary))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
got,
|
||||
vec![
|
||||
(1, 0x1011, false), // primary video
|
||||
(2, 0x1100, false), // primary audio ×2
|
||||
(2, 0x1101, false),
|
||||
(3, 0x1200, false), // PG ×3
|
||||
(3, 0x1201, false),
|
||||
(3, 0x1202, false),
|
||||
// the 4 IG entries are consumed and discarded
|
||||
(5, 0x1A00, true), // secondary audio
|
||||
(6, 0x1B00, true), // secondary video
|
||||
(3, 0x1C00, true), // PiP PG
|
||||
(7, 0x1015, true), // Dolby Vision EL
|
||||
]
|
||||
);
|
||||
// Languages prove each entry was decoded at its own offset.
|
||||
assert_eq!(pl.streams[1].language, "eng");
|
||||
assert_eq!(pl.streams[2].language, "fra");
|
||||
assert_eq!(pl.streams[6].language, "spa");
|
||||
assert_eq!(pl.streams[8].language, "jpn");
|
||||
}
|
||||
|
||||
/// A secondary block whose stream entry ends exactly at the end of the
|
||||
/// PlayItem has no reference block at all; the count byte must not be
|
||||
/// read from one-past-the-end. Covers all three secondary blocks that
|
||||
/// carry reference data.
|
||||
#[test]
|
||||
fn secondary_ref_block_at_item_end_is_not_read() {
|
||||
let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None);
|
||||
|
||||
// Secondary audio is the last entry, with no ref bytes following.
|
||||
let sec_audio = build_stream_entry_audio(0x1A00, 0x83, 3, 1, b"eng");
|
||||
let data = build_mpls(
|
||||
&[(b"00001", 1, 0, 9_000_000)],
|
||||
(1, 0, 0, 0, 1, 0, 0, 0),
|
||||
&[video.clone(), sec_audio],
|
||||
);
|
||||
let pl = parse(&data).expect("secondary audio at item end");
|
||||
assert_eq!(pl.streams.len(), 2);
|
||||
assert_eq!(pl.streams[1].pid, 0x1A00);
|
||||
|
||||
// Secondary video is the last entry, with no ref bytes following.
|
||||
let sec_video = build_stream_entry_video(0x1B00, 0x1B, 4, 1, None);
|
||||
let data = build_mpls(
|
||||
&[(b"00001", 1, 0, 9_000_000)],
|
||||
(1, 0, 0, 0, 0, 1, 0, 0),
|
||||
&[video.clone(), sec_video.clone()],
|
||||
);
|
||||
let pl = parse(&data).expect("secondary video at item end");
|
||||
assert_eq!(pl.streams.len(), 2);
|
||||
assert_eq!(pl.streams[1].pid, 0x1B00);
|
||||
|
||||
// Secondary video whose audio-ref block ends exactly at item end, so
|
||||
// the PiP-PG ref count byte would sit one past it.
|
||||
let mut sec_video_arefs = sec_video;
|
||||
sec_video_arefs.extend_from_slice(&[0, 0]); // n_arefs = 0, reserved
|
||||
let data = build_mpls(
|
||||
&[(b"00001", 1, 0, 9_000_000)],
|
||||
(1, 0, 0, 0, 0, 1, 0, 0),
|
||||
&[video.clone(), sec_video_arefs],
|
||||
);
|
||||
let pl = parse(&data).expect("secondary video aref block at item end");
|
||||
assert_eq!(pl.streams.len(), 2);
|
||||
assert_eq!(pl.streams[1].pid, 0x1B00);
|
||||
|
||||
// PiP PG is the last entry, with no ref bytes following.
|
||||
let pip_pg = build_stream_entry_pg(0x1C00, 0x90, b"jpn");
|
||||
let data = build_mpls(
|
||||
&[(b"00001", 1, 0, 9_000_000)],
|
||||
(1, 0, 0, 0, 0, 0, 1, 0),
|
||||
&[video, pip_pg],
|
||||
);
|
||||
let pl = parse(&data).expect("PiP PG at item end");
|
||||
assert_eq!(pl.streams.len(), 2);
|
||||
assert_eq!(pl.streams[1].pid, 0x1C00);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// parse_stream_entry bounds, exercised directly.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Fewer than 2 bytes remain for the stream_entry header
|
||||
/// (length(1) + stream_entry_type(1)) → None, without reading either.
|
||||
#[test]
|
||||
fn stream_entry_header_past_end_is_none() {
|
||||
let item = [0u8; 8];
|
||||
for pos in 7..12usize {
|
||||
assert!(
|
||||
parse_stream_entry(&item, pos, STREAM_CATEGORY_VIDEO).is_none(),
|
||||
"pos={pos}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The stream_attributes header (length(1) + coding_type(1)) lies past
|
||||
/// the end of the PlayItem → None, without reading the length byte.
|
||||
#[test]
|
||||
fn stream_attributes_header_past_end_is_none() {
|
||||
// se_len = 3 → se_end = 4 == item.len(); the sa length byte would be
|
||||
// at item[4] and the coding type at item[5].
|
||||
let item = [3u8, STREAM_ENTRY_PLAYITEM_CLIP, 0x10, 0x11];
|
||||
assert!(parse_stream_entry(&item, 0, STREAM_CATEGORY_VIDEO).is_none());
|
||||
}
|
||||
|
||||
/// A declared stream_attributes length of 0 has no coding_type byte and
|
||||
/// must be rejected — even when the (empty) attribute region is itself
|
||||
/// in bounds.
|
||||
#[test]
|
||||
fn zero_length_attributes_in_bounds_is_none() {
|
||||
// se_len = 3 → se_end = 4; sa_len = item[4] = 0 → sa_end = 5 ≤ 6.
|
||||
let item = [3u8, STREAM_ENTRY_PLAYITEM_CLIP, 0x10, 0x11, 0, 0];
|
||||
assert!(parse_stream_entry(&item, 0, STREAM_CATEGORY_VIDEO).is_none());
|
||||
}
|
||||
|
||||
/// stream_attributes of exactly 1 byte carries only the coding_type.
|
||||
/// That is the minimum the parser accepts, so the entry is returned
|
||||
/// with its PID and coding_type and no format-specific fields — for a
|
||||
/// PG stream the 3-byte language must NOT be read past the attributes.
|
||||
#[test]
|
||||
fn one_byte_stream_attributes_yields_bare_entry() {
|
||||
// se_len = 3 → se_end = 4; sa_len = 1 → sa_end = 6 == item.len().
|
||||
let item = [3u8, STREAM_ENTRY_PLAYITEM_CLIP, 0x10, 0x11, 1, 0x1B];
|
||||
let (entry, next) =
|
||||
parse_stream_entry(&item, 0, STREAM_CATEGORY_VIDEO).expect("1-byte attrs are valid");
|
||||
assert_eq!(entry.pid, 0x1011);
|
||||
assert_eq!(entry.coding_type, 0x1B);
|
||||
assert_eq!(entry.video_format, 0);
|
||||
assert_eq!(entry.video_rate, 0);
|
||||
assert_eq!(next, 6);
|
||||
|
||||
let pg = [3u8, STREAM_ENTRY_PLAYITEM_CLIP, 0x12, 0x00, 1, 0x90];
|
||||
let (entry, _) = parse_stream_entry(&pg, 0, STREAM_CATEGORY_PG_SUBTITLE)
|
||||
.expect("1-byte PG attrs are valid");
|
||||
assert_eq!(entry.pid, 0x1200);
|
||||
assert_eq!(entry.coding_type, 0x90);
|
||||
assert_eq!(entry.language, "");
|
||||
}
|
||||
}
|
||||
|
||||
+52
-20
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user