diff --git a/src/clpi.rs b/src/clpi.rs index b2765a2..9cd0296 100644 --- a/src/clpi.rs +++ b/src/clpi.rs @@ -6,26 +6,13 @@ //! //! Reference: https://github.com/lw/BluRay/wiki/CLPI -use crate::consts::{BD_SOURCE_PACKET_BYTES, SECTOR_BYTES_U64}; -use crate::disc::Extent; use crate::error::{Error, Result}; /// Parsed CLPI clip info. #[derive(Debug)] pub(crate) struct ClipInfo { - /// CLPI version string. Parsed for completeness; not yet consumed. - #[allow(dead_code)] - pub version: String, /// Total source packets in the m2ts (each 192 bytes) pub source_packet_count: u32, - /// Coarse EP entries for the primary video stream. Populated for the - /// EP-map → sector-extent lookup (`get_extents`), which is exercised by - /// tests and reserved for the timestamp-range read path. - #[allow(dead_code)] - pub ep_coarse: Vec, - /// Fine EP entries for the primary video stream (see `ep_coarse`). - #[allow(dead_code)] - pub ep_fine: Vec, /// Per-stream metadata from the ProgramInfo section (BD spec). /// Cross-validates the MPLS STN view — see `labels/clpi_audit.rs`. /// Empty when program_info is missing or malformed. @@ -63,141 +50,6 @@ pub(crate) struct ClpiStream { pub video_rate: u8, } -/// Coarse EP-map entry. Fields feed the EP-map resolution used by -/// `get_extents` (test-exercised; reserved for the timestamp-range path). -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub(crate) struct EpCoarse { - pub ref_to_fine_id: u32, - pub pts_coarse: u32, - pub spn_coarse: u32, -} - -/// Fine EP-map entry (see `EpCoarse`). -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub(crate) struct EpFine { - pub pts_fine: u32, - pub spn_fine: u32, -} - -// EP-map → sector-extent resolution. Exercised by the unit tests and -// reserved for the timestamp-range read path; no production caller yet. -#[allow(dead_code)] -impl ClipInfo { - /// Reconstruct full PTS from coarse + fine entry. - /// - /// The BD spec PTS is 33-bit: `pts_coarse` is 14 bits (max 16383) and - /// `16383 << 19` exceeds `u32::MAX`, so the result must be `u64` to - /// avoid overflow (panic in debug, silent wrap in release). - pub fn full_pts(coarse: &EpCoarse, fine: &EpFine) -> u64 { - ((coarse.pts_coarse as u64) << 19) + ((fine.pts_fine as u64) << 8) - } - - /// Reconstruct full SPN from coarse + fine entry. - pub fn full_spn(coarse: &EpCoarse, fine: &EpFine) -> u32 { - // The two operands occupy non-overlapping bit ranges (coarse holds - // the high bits, fine the low 17), so OR expresses intent and is - // robust to a hand-constructed EpFine. - debug_assert!(fine.spn_fine <= 0x1_FFFF); - (coarse.spn_coarse & 0xFFFE_0000) | fine.spn_fine - } - - /// Get all EP entries as (PTS, SPN) pairs, fully resolved. - /// - /// PTS resets at each coarse-group boundary on disc, so the raw - /// concatenation is not globally monotonic. The returned vector is - /// sorted by PTS so callers (e.g. [`get_extents`]) can binary-search it. - /// - /// [`get_extents`]: ClipInfo::get_extents - pub fn resolved_ep_map(&self) -> Vec<(u64, u32)> { - let mut entries = Vec::with_capacity(self.ep_fine.len()); - - for (ci, coarse) in self.ep_coarse.iter().enumerate() { - let fine_start = coarse.ref_to_fine_id as usize; - let fine_end = if ci + 1 < self.ep_coarse.len() { - self.ep_coarse[ci + 1].ref_to_fine_id as usize - } else { - self.ep_fine.len() - }; - - for fi in fine_start..fine_end.min(self.ep_fine.len()) { - let fine = &self.ep_fine[fi]; - let pts = Self::full_pts(coarse, fine); - let spn = Self::full_spn(coarse, fine); - entries.push((pts, spn)); - } - } - - // get_extents binary-searches by PTS, so the map must be ordered. - // Real discs have globally increasing PTS in coarse order; sort by - // (pts, spn) so a cross-group PTS collision can't leave the search - // landing on the wrong group's SPN. - entries.sort_by_key(|&(pts, spn)| (pts, spn)); - - entries - } - - /// Get sector extents for a given in/out time range. - /// - /// Converts PTS timestamps to SPN ranges, then SPN to LBA - /// using the file's starting LBA on disc. - pub fn get_extents(&self, in_time: u64, out_time: u64) -> Vec { - // resolved_ep_map() returns entries sorted by PTS, so binary search - // is valid here. - let ep_map = self.resolved_ep_map(); - if ep_map.is_empty() { - return Vec::new(); - } - - // Find SPN at or before in_time - let start_spn = match ep_map.binary_search_by_key(&in_time, |(pts, _)| *pts) { - Ok(i) => ep_map[i].1, - Err(0) => ep_map[0].1, - Err(i) => ep_map[i - 1].1, - }; - - // 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, - _ => self - .source_packet_count - .max(ep_map.last().unwrap().1.saturating_add(1)), - }; - - if end_spn <= start_spn { - return Vec::new(); - } - - // SPN → byte offset → sector range. Note: the caller adds the file's - // starting LBA from UDF. The start sector FLOORS (the extent begins in - // whichever sector contains its first byte) and the end sector CEILS - // (the extent must cover through the sector holding its last byte), so - // a sub-sector-aligned range still spans every sector it touches. - let start_byte = start_spn as u64 * BD_SOURCE_PACKET_BYTES as u64; - let end_byte = end_spn as u64 * BD_SOURCE_PACKET_BYTES as u64; - let start_sector = (start_byte / SECTOR_BYTES_U64) as u32; - let end_sector = end_byte.div_ceil(SECTOR_BYTES_U64) as u32; - - vec![Extent { - start_lba: start_sector, // relative to m2ts file start - sector_count: end_sector - start_sector, - }] - } -} - /// Parse a CLPI file from raw bytes. pub fn parse(data: &[u8]) -> Result { if data.len() < 40 { @@ -207,12 +59,10 @@ pub fn parse(data: &[u8]) -> Result { if &data[0..4] != b"HDMV" { return Err(Error::ClpiParse); } - let version = String::from_utf8_lossy(&data[4..8]).to_string(); // Header offsets let _seq_info_start = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize; let prog_info_start = u32::from_be_bytes([data[12], data[13], data[14], data[15]]) as usize; - let cpi_start = u32::from_be_bytes([data[16], data[17], data[18], data[19]]) as usize; // ClipInfo section at offset 40 // source_packet_count at offset 40 + 4(len) + 2(reserved) + 1(stream_type) + 1(app_type) + 4(reserved) + 4(ts_rate) @@ -232,18 +82,8 @@ pub fn parse(data: &[u8]) -> Result { Vec::new() }; - // Parse CPI / EP Map - let (ep_coarse, ep_fine) = if cpi_start > 0 && cpi_start + 8 < data.len() { - parse_cpi(&data[cpi_start..])? - } else { - (Vec::new(), Vec::new()) - }; - Ok(ClipInfo { - version, source_packet_count, - ep_coarse, - ep_fine, streams, }) } @@ -363,155 +203,6 @@ fn parse_program_info(data: &[u8]) -> Vec { out } -/// Parse the CPI section containing the EP map. -fn parse_cpi(data: &[u8]) -> Result<(Vec, Vec)> { - if data.len() < 8 { - return Ok((Vec::new(), Vec::new())); - } - - let cpi_length = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize; - if cpi_length < 4 { - return Ok((Vec::new(), Vec::new())); - } - - // Bound all EP-map reads to this CPI section. The length field counts - // bytes after itself, so the section spans data[..cpi_length + 4]. A - // bogus ep_map_offset within data.len() but past the CPI section would - // otherwise read into an adjacent CLPI section; clamp first. - let data = &data[..(cpi_length + 4).min(data.len())]; - - // CPI type at bits 44-47 (byte 5, lower 4 bits) - // Skip to EP map: offset 4 (after length) + 2 (reserved/type) - if data.len() < 6 { - return Ok((Vec::new(), Vec::new())); - } - let ep_map = &data[6..]; - if ep_map.len() < 4 { - return Ok((Vec::new(), Vec::new())); - } - - // EP map header - // [0] reserved - // [1] number of stream PID entries - let num_streams = ep_map[1] as usize; - if num_streams == 0 { - return Ok((Vec::new(), Vec::new())); - } - - // Stream PID entry headers start at offset 2 - // Each: 2(PID) + 2(reserved+type) + 2(num_coarse) + 4(num_fine) + 4(ep_map_start) = 14 bytes - // We only care about the first stream (primary video) - if ep_map.len() < 16 { - return Ok((Vec::new(), Vec::new())); - } - - // Stream PID entry — bit-packed per the BD CLPI spec: - // stream_PID: 16 bits → ep_map[2..4] - // reserved: 10 bits ┐ - // EP_stream_type: 4 bits │ ep_map[4..14] = 80 bits - // num_EP_coarse: 16 bits │ (10+4+16+18+32 = 80) - // num_EP_fine: 18 bits │ - // EP_map_start_address: 32 bits ┘ - let _stream_pid = u16::from_be_bytes([ep_map[2], ep_map[3]]); - - // Read 10 bytes (80 bits) from ep_map[4..14] for bit extraction - // Use two u64s since we need 80 bits - let hi = u64::from_be_bytes([ - ep_map[4], ep_map[5], ep_map[6], ep_map[7], ep_map[8], ep_map[9], ep_map[10], ep_map[11], - ]); - let lo_bytes = [ep_map[12], ep_map[13]]; - - // Bit 0-9: reserved (10) - // Bit 10-13: EP_stream_type (4) - // Bit 14-29: num_coarse (16) - // Bit 30-47: num_fine (18) - // Bit 48-79: EP_map_start (32) — bits 48-63 in hi, bits 64-79 in lo - let num_coarse = ((hi >> 34) & 0xFFFF) as usize; - let num_fine = ((hi >> 16) & 0x3FFFF) as usize; - let ep_map_offset = (((hi & 0xFFFF) as u32) << 16) | (u16::from_be_bytes(lo_bytes) as u32); - let ep_map_offset = ep_map_offset as usize; - - // EP map for this stream starts at ep_map_offset relative to ep_map start - if ep_map_offset + 4 > ep_map.len() { - return Ok((Vec::new(), Vec::new())); - } - - let stream_ep = &ep_map[ep_map_offset..]; - if stream_ep.len() < 4 { - return Ok((Vec::new(), Vec::new())); - } - - // Fine table start address (relative to this stream EP map) - let fine_start = - u32::from_be_bytes([stream_ep[0], stream_ep[1], stream_ep[2], stream_ep[3]]) as usize; - - // Coarse entries start at offset 4, 8 bytes each - let coarse_data = &stream_ep[4..]; - // Cap the pre-reservation by what the slice can actually hold: - // num_coarse is a 16-bit disc field, so a hostile value would - // otherwise reserve up to ~0.5 MB for an entry table that doesn't exist. - let mut ep_coarse = Vec::with_capacity(num_coarse.min(coarse_data.len() / 8)); - for i in 0..num_coarse { - let off = i * 8; - if off + 8 > coarse_data.len() { - break; - } - - let dword0 = u32::from_be_bytes([ - coarse_data[off], - coarse_data[off + 1], - coarse_data[off + 2], - coarse_data[off + 3], - ]); - let ref_to_fine_id = dword0 >> 14; - let pts_coarse = dword0 & 0x3FFF; - let spn_coarse = u32::from_be_bytes([ - coarse_data[off + 4], - coarse_data[off + 5], - coarse_data[off + 6], - coarse_data[off + 7], - ]); - - ep_coarse.push(EpCoarse { - ref_to_fine_id, - pts_coarse, - spn_coarse, - }); - } - - // Fine entries at fine_start, 4 bytes each - // Cap the pre-reservation: num_fine is an 18-bit disc field (max - // 262143), so reserve only what the slice can actually hold. - let mut ep_fine = if fine_start < stream_ep.len() { - Vec::with_capacity(num_fine.min((stream_ep.len() - fine_start) / 4)) - } else { - Vec::new() - }; - if fine_start < stream_ep.len() { - let fine_data = &stream_ep[fine_start..]; - for i in 0..num_fine { - let off = i * 4; - if off + 4 > fine_data.len() { - break; - } - - let dword = u32::from_be_bytes([ - fine_data[off], - fine_data[off + 1], - fine_data[off + 2], - fine_data[off + 3], - ]); - // Bits: is_angle(1) + i_end_offset(3) + pts_fine(11) + spn_fine(17) - let pts_fine = (dword >> 17) & 0x7FF; - let spn_fine = dword & 0x1FFFF; - - ep_fine.push(EpFine { pts_fine, spn_fine }); - } - } - - Ok((ep_coarse, ep_fine)) -} - #[cfg(test)] mod tests { use super::*; @@ -550,236 +241,6 @@ mod tests { buf } - /// Build a CPI section with one stream's EP map. - /// coarse_entries: Vec<(ref_to_fine_id, pts_coarse, spn_coarse)> - /// fine_entries: Vec<(pts_fine, spn_fine)> - fn build_cpi( - stream_pid: u16, - coarse_entries: &[(u32, u32, u32)], - fine_entries: &[(u32, u32)], - ) -> Vec { - // CPI section layout: - // [0..4] cpi_length (u32 BE) - // [4..6] reserved/type (2 bytes) - // [6..] EP map - // - // EP map layout (relative to byte 6 of CPI): - // [0] reserved - // [1] num_streams (1) - // [2..4] stream_PID (u16 BE) - // [4..14] 80 bits: reserved(10) + EP_stream_type(4) + num_coarse(16) + num_fine(18) + EP_map_start(32) - // [14..] (next stream entry, if any) - // - // Stream EP map (at EP_map_start relative to EP map start): - // [0..4] fine_start (relative to stream EP map start) - // [4..] coarse entries, 8 bytes each - // [fine_start..] fine entries, 4 bytes each - - let num_coarse = coarse_entries.len() as u32; - let num_fine = fine_entries.len() as u32; - - // EP_map_start: offset from ep_map start where the stream EP data begins. - // ep_map has: reserved(1) + num_streams(1) + stream_header(12) = 14 bytes - // So EP_map_start = 14 (first stream data right after the header) - let ep_map_start: u32 = 14; - - // Build the 80-bit stream PID entry (10 bytes: ep_map[4..14]) - // Bits: reserved(10) + EP_stream_type(4) + num_coarse(16) + num_fine(18) + EP_map_start(32) - // Total: 80 bits = 10 bytes - // - // Pack into a u128 for convenience then extract 10 bytes - let ep_stream_type: u32 = 1; // video - let packed: u128 = ((ep_stream_type as u128) << 66) // EP_stream_type: 4 bits - | ((num_coarse as u128) << 50) // num_coarse: 16 bits - | ((num_fine as u128) << 32) // num_fine: 18 bits - | (ep_map_start as u128); // EP_map_start: 32 bits - let packed_bytes = packed.to_be_bytes(); // 16 bytes, we want the last 10 - let stream_header_bits = &packed_bytes[6..16]; - - // Build stream EP data - // fine_start = 4 (header) + num_coarse * 8 - let fine_start: u32 = 4 + num_coarse * 8; - let mut stream_ep = Vec::new(); - stream_ep.extend_from_slice(&fine_start.to_be_bytes()); - - // Coarse entries: 8 bytes each - // dword0 = (ref_to_fine_id << 14) | (pts_coarse & 0x3FFF) - // dword1 = spn_coarse - for &(ref_id, pts_c, spn_c) in coarse_entries { - let dword0 = (ref_id << 14) | (pts_c & 0x3FFF); - stream_ep.extend_from_slice(&dword0.to_be_bytes()); - stream_ep.extend_from_slice(&spn_c.to_be_bytes()); - } - - // Fine entries: 4 bytes each - // dword = (is_angle(1) + i_end_offset(3) + pts_fine(11) + spn_fine(17)) - for &(pts_f, spn_f) in fine_entries { - let dword: u32 = ((pts_f & 0x7FF) << 17) | (spn_f & 0x1FFFF); - stream_ep.extend_from_slice(&dword.to_be_bytes()); - } - - // Assemble EP map - let mut ep_map = Vec::new(); - ep_map.push(0); // reserved - ep_map.push(1); // num_streams = 1 - ep_map.extend_from_slice(&stream_pid.to_be_bytes()); - ep_map.extend_from_slice(stream_header_bits); - ep_map.extend_from_slice(&stream_ep); - - // Assemble CPI section - let mut cpi = Vec::new(); - let cpi_length = (2 + ep_map.len()) as u32; // reserved/type(2) + ep_map - cpi.extend_from_slice(&cpi_length.to_be_bytes()); - cpi.extend_from_slice(&[0u8; 2]); // reserved/type - cpi.extend_from_slice(&ep_map); - - cpi - } - - #[test] - fn parse_valid_clpi() { - let cpi = build_cpi( - 0x1011, - &[(0, 100, 0x00020000)], // 1 coarse - &[(50, 1024)], // 1 fine - ); - let data = build_clpi(500_000, Some(&cpi)); - - let clip = parse(&data).expect("should parse valid CLPI"); - assert_eq!(clip.version, "0200"); - assert_eq!(clip.source_packet_count, 500_000); - assert_eq!(clip.ep_coarse.len(), 1); - assert_eq!(clip.ep_fine.len(), 1); - } - - #[test] - fn parse_ep_map() { - let cpi = build_cpi( - 0x1011, - &[ - (0, 100, 0x00020000), // coarse 0: fine starts at 0, pts_coarse=100, spn_coarse=0x20000 - (2, 200, 0x00040000), // coarse 1: fine starts at 2, pts_coarse=200, spn_coarse=0x40000 - ], - &[ - (50, 1024), // fine 0 - (100, 2048), // fine 1 - (25, 512), // fine 2 - (75, 1536), // fine 3 - ], - ); - let data = build_clpi(1_000_000, Some(&cpi)); - - let clip = parse(&data).expect("should parse EP map"); - assert_eq!(clip.ep_coarse.len(), 2); - assert_eq!(clip.ep_fine.len(), 4); - - // Verify coarse entries - assert_eq!(clip.ep_coarse[0].ref_to_fine_id, 0); - assert_eq!(clip.ep_coarse[0].pts_coarse, 100); - assert_eq!(clip.ep_coarse[0].spn_coarse, 0x00020000); - assert_eq!(clip.ep_coarse[1].ref_to_fine_id, 2); - assert_eq!(clip.ep_coarse[1].pts_coarse, 200); - assert_eq!(clip.ep_coarse[1].spn_coarse, 0x00040000); - - // Verify fine entries - assert_eq!(clip.ep_fine[0].pts_fine, 50); - assert_eq!(clip.ep_fine[0].spn_fine, 1024); - assert_eq!(clip.ep_fine[1].pts_fine, 100); - assert_eq!(clip.ep_fine[1].spn_fine, 2048); - assert_eq!(clip.ep_fine[2].pts_fine, 25); - assert_eq!(clip.ep_fine[2].spn_fine, 512); - assert_eq!(clip.ep_fine[3].pts_fine, 75); - assert_eq!(clip.ep_fine[3].spn_fine, 1536); - - // Verify resolved EP map assigns fine entries to coarse correctly - let resolved = clip.resolved_ep_map(); - assert_eq!(resolved.len(), 4); - // First two fines belong to coarse 0, last two to coarse 1 - } - - #[test] - fn full_pts_calculation() { - let coarse = EpCoarse { - ref_to_fine_id: 0, - pts_coarse: 100, - spn_coarse: 0, - }; - let fine = EpFine { - pts_fine: 50, - spn_fine: 0, - }; - // full_pts = (100 << 19) + (50 << 8) = 52_428_800 + 12_800 = 52_441_600 - let pts = ClipInfo::full_pts(&coarse, &fine); - assert_eq!(pts, (100u64 << 19) + (50u64 << 8)); - assert_eq!(pts, 52_441_600); - } - - #[test] - fn full_pts_no_u32_overflow() { - // pts_coarse is a 14-bit field (max 0x3FFF = 16383); 16383 << 19 - // overflows u32, so full_pts must use u64. - let coarse = EpCoarse { - ref_to_fine_id: 0, - pts_coarse: 0x3FFF, - spn_coarse: 0, - }; - let fine = EpFine { - pts_fine: 0x7FF, - spn_fine: 0, - }; - let pts = ClipInfo::full_pts(&coarse, &fine); - assert_eq!(pts, (0x3FFFu64 << 19) + (0x7FFu64 << 8)); - assert!(pts > u32::MAX as u64); - } - - #[test] - fn resolved_ep_map_sorted_for_binary_search() { - // Two coarse groups whose fine PTS reset across the boundary - // (50,100 then 25,75) produce a non-monotonic raw concatenation. - // resolved_ep_map must sort so get_extents' binary search is valid. - let cpi = build_cpi( - 0x1011, - &[(0, 0, 0x00020000), (2, 0, 0x00040000)], - &[(50, 1024), (100, 2048), (25, 512), (75, 1536)], - ); - let data = build_clpi(1_000_000, Some(&cpi)); - let clip = parse(&data).expect("should parse"); - - let resolved = clip.resolved_ep_map(); - assert_eq!(resolved.len(), 4); - // Strictly sorted by PTS. - for w in resolved.windows(2) { - assert!(w[0].0 <= w[1].0, "ep_map not sorted: {resolved:?}"); - } - } - - #[test] - fn full_spn_calculation() { - let coarse = EpCoarse { - ref_to_fine_id: 0, - pts_coarse: 0, - spn_coarse: 0x00FE0000, - }; - let fine = EpFine { - pts_fine: 0, - spn_fine: 0x1234, - }; - // full_spn = (0x00FE0000 & 0xFFFE0000) + 0x1234 = 0x00FE0000 + 0x1234 = 0x00FE1234 - let spn = ClipInfo::full_spn(&coarse, &fine); - assert_eq!(spn, 0x00FE0000 + 0x1234); - assert_eq!(spn, 0x00FE1234); - - // Test that the low bit of spn_coarse is masked out - let coarse2 = EpCoarse { - ref_to_fine_id: 0, - pts_coarse: 0, - spn_coarse: 0x00FF0000, - }; - let spn2 = ClipInfo::full_spn(&coarse2, &fine); - // 0x00FF0000 & 0xFFFE0000 = 0x00FE0000, so low 17 bits of coarse are zeroed - assert_eq!(spn2, 0x00FE0000 + 0x1234); - } - #[test] fn parse_truncated_clipinfo_no_panic() { // 57/58/59-byte CLPI with valid magic: passes the data.len() < 40 @@ -806,30 +267,6 @@ mod tests { assert!(parse(&data).is_err()); } - #[test] - fn parse_empty_ep_map() { - // cpi_start = 0 means no CPI section - let data = build_clpi(100_000, None); - let clip = parse(&data).expect("should parse with no EP map"); - assert_eq!(clip.source_packet_count, 100_000); - assert!(clip.ep_coarse.is_empty()); - assert!(clip.ep_fine.is_empty()); - - // Also test: CPI section present but with zero streams - let mut cpi = Vec::new(); - let cpi_length: u32 = 6; // reserved/type(2) + ep_map(reserved(1) + num_streams=0(1) + 2 padding) - cpi.extend_from_slice(&cpi_length.to_be_bytes()); - cpi.extend_from_slice(&[0u8; 2]); // reserved/type - cpi.push(0); // reserved - cpi.push(0); // num_streams = 0 - cpi.extend_from_slice(&[0u8; 4]); // padding - - let data2 = build_clpi(100_000, Some(&cpi)); - let clip2 = parse(&data2).expect("should parse with zero-stream EP map"); - assert!(clip2.ep_coarse.is_empty()); - assert!(clip2.ep_fine.is_empty()); - } - // ───────────────────────────────────────────────────────────────────── // Added hardening tests. Grounded in the BD-ROM CLPI spec // (https://github.com/lw/BluRay/wiki/CLPI). @@ -1030,265 +467,6 @@ mod tests { assert!(clip.streams.is_empty()); } - /// pts_coarse field is 14 bits: dword0 = ref_to_fine_id<<14 | pts_coarse. - /// A pts_coarse of 0x3FFF (max) with ref_to_fine_id 5 must decode both - /// without bleed. Verify the >>14 and &0x3FFF split. - #[test] - fn coarse_pts_14bit_split() { - let cpi = build_cpi(0x1011, &[(5, 0x3FFF, 0x12340000)], &[(0, 0)]); - let data = build_clpi(1000, Some(&cpi)); - let clip = parse(&data).expect("should parse"); - assert_eq!(clip.ep_coarse[0].ref_to_fine_id, 5); - assert_eq!(clip.ep_coarse[0].pts_coarse, 0x3FFF); - assert_eq!(clip.ep_coarse[0].spn_coarse, 0x12340000); - } - - /// Fine entry: dword = is_angle(1)+i_end_offset(3)+pts_fine(11)+ - /// spn_fine(17). pts_fine occupies bits 17..28 (>>17 & 0x7FF), spn_fine - /// the low 17 bits (& 0x1FFFF). Set high bits (is_angle/i_end_offset) - /// and verify they do NOT bleed into pts_fine. - #[test] - fn fine_entry_bit_layout_isolates_pts_and_spn() { - // Construct a raw fine dword with is_angle=1, i_end_offset=0b111, - // pts_fine=0x5AA, spn_fine=0x1AAAA, then verify decode. - let is_angle: u32 = 1; - let i_end: u32 = 0b111; - let pts_f: u32 = 0x5AA; // 11-bit - let spn_f: u32 = 0x1AAAA; // 17-bit - let dword: u32 = (is_angle << 31) | (i_end << 28) | (pts_f << 17) | spn_f; - - // Build the CPI by hand with this raw fine dword. - let mut stream_ep = Vec::new(); - let fine_start: u32 = 4; // no coarse entries → fine right after header - stream_ep.extend_from_slice(&fine_start.to_be_bytes()); - stream_ep.extend_from_slice(&dword.to_be_bytes()); - - let num_coarse: u32 = 0; - let num_fine: u32 = 1; - let ep_map_start: u32 = 14; - let ep_stream_type: u32 = 1; - let packed: u128 = ((ep_stream_type as u128) << 66) - | ((num_coarse as u128) << 50) - | ((num_fine as u128) << 32) - | (ep_map_start as u128); - let packed_bytes = packed.to_be_bytes(); - let stream_header_bits = &packed_bytes[6..16]; - - let mut ep_map = Vec::new(); - ep_map.push(0); - ep_map.push(1); - ep_map.extend_from_slice(&0x1011u16.to_be_bytes()); - ep_map.extend_from_slice(stream_header_bits); - ep_map.extend_from_slice(&stream_ep); - - 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_eq!(clip.ep_fine[0].pts_fine, 0x5AA); // high bits stripped - assert_eq!(clip.ep_fine[0].spn_fine, 0x1AAAA); - } - - /// resolved_ep_map assigns fine entries to coarse groups via - /// [ref_to_fine_id .. next coarse's ref_to_fine_id). full_pts combines - /// coarse<<19 + fine<<8 and full_spn ORs masked coarse with fine. - /// Verify the first resolved entry's (pts, spn) for a known fixture. - #[test] - fn resolved_ep_map_combines_coarse_and_fine() { - // coarse 0: ref_to_fine_id=0, pts_coarse=10, spn_coarse=0x00020000 - // fine 0: pts_fine=3, spn_fine=0x100 - let cpi = build_cpi(0x1011, &[(0, 10, 0x00020000)], &[(3, 0x100)]); - let data = build_clpi(1000, Some(&cpi)); - let clip = parse(&data).expect("should parse"); - let resolved = clip.resolved_ep_map(); - assert_eq!(resolved.len(), 1); - let expected_pts = (10u64 << 19) + (3u64 << 8); - let expected_spn = (0x00020000u32 & 0xFFFE_0000) | 0x100; - assert_eq!(resolved[0].0, expected_pts); - assert_eq!(resolved[0].1, expected_spn); - } - - /// get_extents converts an in/out PTS range to a single sector Extent. - /// SPN→byte = spn×192, byte→sector = /2048 (start floored, end ceiled), - /// relative to m2ts file start. Verify the math for a known fixture. - #[test] - fn get_extents_spn_to_sector_math() { - // Two EP points: PTS p0 → SPN 0, PTS p1 → SPN big_spn. - // full_spn ORs (spn_coarse & 0xFFFE0000) with spn_fine, so the SPN - // must be coarse-aligned (low 17 bits clear) to survive intact. - // 0x20000 (131072) is the smallest non-zero coarse-aligned SPN. - let big_spn: u32 = 0x20000; - let cpi = build_cpi(0x1011, &[(0, 0, 0), (1, 100, big_spn)], &[(0, 0), (0, 0)]); - let data = build_clpi(1000, Some(&cpi)); - let clip = parse(&data).expect("should parse"); - - let p0 = 0u64; // PTS of first EP - let p1 = 100u64 << 19; // PTS of second EP - let extents = clip.get_extents(p0, p1); - assert_eq!(extents.len(), 1); - // Mirror production: SPN→byte ×packet, byte→sector with start FLOORed - // and end CEILed (same constants as get_extents). - let start_spn: u64 = 0; - let end_spn = big_spn as u64; - let start_byte = start_spn * BD_SOURCE_PACKET_BYTES as u64; - let end_byte = end_spn * BD_SOURCE_PACKET_BYTES as u64; - let start_sector = (start_byte / SECTOR_BYTES_U64) as u32; - let end_sector = end_byte.div_ceil(SECTOR_BYTES_U64) as u32; - assert_eq!(extents[0].start_lba, start_sector); - assert_eq!(extents[0].sector_count, end_sector - start_sector); - // Concretely: 0x20000 × 192 / 2048 = 12288 sectors. - assert_eq!(extents[0].sector_count, 12288); - } - - /// get_extents returns an empty Vec when the EP map is empty (no CPI), - /// since there is no SPN to resolve. Documented early return. - #[test] - fn get_extents_empty_when_no_ep_map() { - let data = build_clpi(1000, None); - let clip = parse(&data).expect("should parse"); - assert!(clip.get_extents(0, 1_000_000).is_empty()); - } - - /// get_extents returns empty when end_spn <= start_spn (degenerate or - /// inverted range). Source has an explicit `if end_spn <= start_spn` - /// guard. Use in_time == out_time on a single-point map. - #[test] - fn get_extents_empty_on_degenerate_range() { - let cpi = build_cpi(0x1011, &[(0, 50, 0x1000)], &[(0, 0)]); - let data = build_clpi(1000, Some(&cpi)); - let clip = parse(&data).expect("should parse"); - let p = 50u64 << 19; - // in == out → start_spn == end_spn → empty. - assert!(clip.get_extents(p, p).is_empty()); - } - - /// full_spn masks the LOW 17 bits of spn_coarse (& 0xFFFE0000) before - /// OR-ing fine. A spn_coarse with low bits set must have them cleared, - /// then replaced by spn_fine. Independent of parse, exercises the - /// reconstruction directly with a hostile low-bit pattern. - #[test] - fn full_spn_clears_coarse_low_17_bits() { - let coarse = EpCoarse { - ref_to_fine_id: 0, - pts_coarse: 0, - spn_coarse: 0x0006_FFFF, // low 17 bits all set - }; - let fine = EpFine { - pts_fine: 0, - spn_fine: 0x5, - }; - // 0x0006_FFFF & 0xFFFE_0000 = 0x0006_0000; | 0x5 = 0x0006_0005. - assert_eq!(ClipInfo::full_spn(&coarse, &fine), 0x0006_0005); - } - - /// CPI guard: cpi_length < 4 short-circuits to empty maps (the length - /// field counts bytes after itself, and the EP map needs ≥4). A - /// cpi_length of 0/1/2/3 must yield empty EP maps, not panic. - #[test] - fn cpi_length_below_4_yields_empty() { - for bad_len in 0u32..4 { - let mut cpi = Vec::new(); - cpi.extend_from_slice(&bad_len.to_be_bytes()); - cpi.extend_from_slice(&[0u8; 20]); // padding so the slice exists - let data = build_clpi(1000, Some(&cpi)); - let clip = parse(&data).expect("should parse"); - assert!(clip.ep_coarse.is_empty(), "len={bad_len}"); - assert!(clip.ep_fine.is_empty(), "len={bad_len}"); - } - } - - /// ep_map_offset that points past the EP map (`ep_map_offset + 4 > - /// ep_map.len()`) → empty maps (bounds guard), not panic. Patch the - /// EP_map_start field to a huge value. - #[test] - fn ep_map_offset_out_of_bounds_yields_empty() { - let cpi = build_cpi(0x1011, &[(0, 10, 0x20000)], &[(5, 100)]); - let mut data = build_clpi(1000, Some(&cpi)); - // EP_map_start is the low 32 bits of the 80-bit stream header at - // ep_map[4..14]. In the file: header(60) + cpi_length(4) + - // reserved(2) + ep_map reserved(1) + num_streams(1) + pid(2) = 70, - // then 10 header bytes [70..80]; EP_map_start is the last 4 [76..80]. - let off = 60 + 4 + 2 + 1 + 1 + 2 + 6; // = 76 - data[off..off + 4].copy_from_slice(&0xFFFF_FFFFu32.to_be_bytes()); - let clip = parse(&data).expect("should not panic"); - assert!(clip.ep_coarse.is_empty()); - assert!(clip.ep_fine.is_empty()); - } - - /// num_coarse declares more entries than the CPI section holds. The - /// loop must stop at `off + 8 > coarse_data.len()` (break), not read - /// out of bounds. Patch num_coarse to a large value while supplying 1 - /// coarse entry's worth of bytes. - #[test] - fn coarse_count_overshoot_truncates_safely() { - let cpi = build_cpi(0x1011, &[(0, 10, 0x20000)], &[(5, 100)]); - let mut data = build_clpi(1000, Some(&cpi)); - // num_coarse is bits 14..30 of the 80-bit header. Rather than - // bit-surgery, rebuild with a hand-set num_coarse=255 but only 1 - // coarse entry of bytes — done below directly. - let _ = &mut data; - - let num_coarse_decl: u32 = 255; - let num_fine: u32 = 1; - let ep_map_start: u32 = 14; - let ep_stream_type: u32 = 1; - let packed: u128 = ((ep_stream_type as u128) << 66) - | ((num_coarse_decl as u128) << 50) - | ((num_fine as u128) << 32) - | (ep_map_start as u128); - let packed_bytes = packed.to_be_bytes(); - let stream_header_bits = &packed_bytes[6..16]; - - // stream EP data: fine_start points past the 1 coarse entry. - let fine_start: u32 = 4 + 8; // 4-byte header + 1 coarse entry x 8 bytes - let mut stream_ep = Vec::new(); - stream_ep.extend_from_slice(&fine_start.to_be_bytes()); - // exactly ONE coarse entry (8 bytes), though header claims 255. - stream_ep.extend_from_slice(&10u32.to_be_bytes()); - stream_ep.extend_from_slice(&0x20000u32.to_be_bytes()); - // one fine entry (4 bytes) - stream_ep.extend_from_slice(&(((5u32 & 0x7FF) << 17) | 100).to_be_bytes()); - - let mut ep_map = Vec::new(); - ep_map.push(0); - ep_map.push(1); - ep_map.extend_from_slice(&0x1011u16.to_be_bytes()); - ep_map.extend_from_slice(stream_header_bits); - ep_map.extend_from_slice(&stream_ep); - let mut cpi2 = Vec::new(); - cpi2.extend_from_slice(&((2 + ep_map.len()) as u32).to_be_bytes()); - cpi2.extend_from_slice(&[0u8; 2]); - cpi2.extend_from_slice(&ep_map); - let data2 = build_clpi(1000, Some(&cpi2)); - let clip = parse(&data2).expect("should not panic on coarse overshoot"); - // Only the 1 real coarse entry was readable. - assert_eq!(clip.ep_coarse.len(), 1); - assert_eq!(clip.ep_coarse[0].pts_coarse, 10); - } - - /// resolved_ep_map: the LAST coarse group's fine range extends to - /// ep_fine.len() (no "next coarse" bound). Verify all trailing fine - /// entries are assigned to the final coarse group. - #[test] - fn resolved_ep_map_last_group_to_end() { - // coarse 0 ref_to_fine_id=0, coarse 1 ref_to_fine_id=1. - // 3 fine entries: fine 0 → coarse 0; fine 1,2 → coarse 1. - let cpi = build_cpi( - 0x1011, - &[(0, 0, 0), (1, 100, 0)], - &[(0, 10), (0, 20), (0, 30)], - ); - let data = build_clpi(1000, Some(&cpi)); - let clip = parse(&data).expect("should parse"); - let resolved = clip.resolved_ep_map(); - // 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. // @@ -1298,72 +476,6 @@ mod tests { // 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`. // ───────────────────────────────────────────────────────────────────── @@ -1393,40 +505,6 @@ mod tests { ); } - /// 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 // ───────────────────────────────────────────────────────────────────── @@ -1514,261 +592,4 @@ mod tests { // ───────────────────────────────────────────────────────────────────── // 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, - trailing: &[u8], - ) -> Vec { - 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); - } }