Close the MEDIUM mutation gaps across transport, labels and codecs

The remaining triage items after tonight's HIGH fixes: 1,290 lines, almost
all tests. Covers disc/mod.rs's DVD scan path (with real minimal VMG/VTS IFO
fixtures rather than mocks), drive/mod.rs, labels/class_reader.rs and
labels/mod.rs — the two biggest untriaged survivor clusters in the crate —
plus hevc.rs and ps.rs.

One production change, and it is an extraction rather than a behaviour
change: MacScsiTransport::open mapped the shim's negative failure sentinels
to typed errors inline, where nothing could reach it without a real IOKit
FFI call. It is now map_shim_open_error, so the mapping can be pinned. It
matters because collapsing -5 into the DeviceNotFound catch-all turns
"another process holds the drive" into "no such drive", and an operator
chasing the wrong problem is worse than a blunt error.

Gate green on the pinned toolchain including the secrets scanner.
This commit is contained in:
Matthew Jackson
2026-08-01 15:00:01 -07:00
parent f8ed0b99f4
commit ff18d4c3c8
7 changed files with 1290 additions and 33 deletions
+577 -16
View File
@@ -2048,22 +2048,7 @@ impl Disc {
// one shared PGS classifier); the rip path leaves it off — the muxer
// detects forced while muxing, without a second read of the clip.
if opts.probe_forced_subtitles {
// One cache across every title: a disc's playlists overwhelmingly
// reference the same handful of clips (main feature, play-all,
// seamless-branch variants), so without memoisation the same physical
// extents are re-read from the drive once per playlist — 30-150 times
// on a typical Blu-ray.
let mut cache = pgs_forced_probe::ForcedProbeCache::new();
for title in &mut titles {
if title.content_format == ContentFormat::BdTs {
pgs_forced_probe::probe_and_set_forced(
reader,
title,
&mut cache,
opts.halt.as_ref(),
);
}
}
Self::probe_forced_subtitles_for_bdts_titles(reader, &mut titles, opts.halt.as_ref());
}
crate::labels::fill_defaults(&mut titles);
@@ -2184,6 +2169,35 @@ impl Disc {
pub const CANONICAL_TITLE_ORDER_KEYS: &'static [&'static str] =
&["fits-disc", "largest-size", "longest", "richest-audio"];
/// Content-based forced-subtitle detection, restricted to `BdTs` titles —
/// gates on the STREAM CONTAINER, not the disc-tree format, because a
/// disc can carry non-`BdTs` titles even under `BDMV/` scanning. Only
/// `BdTs` titles carry PES-wrapped PGS the shared classifier
/// (`pgs_forced_probe`) understands; running it against an HD-DVD/DVD
/// (`MpegPs`) title's extents would demux the wrong container.
///
/// Pulled out of `scan_with` as its own callable predicate: the decision
/// is otherwise reachable only by driving the full scan pipeline (tree
/// dispatch → title parsing → this loop), so a test could not pin the
/// gate without also authoring an entire synthetic disc image.
fn probe_forced_subtitles_for_bdts_titles(
reader: &mut dyn SectorSource,
titles: &mut [DiscTitle],
halt: Option<&crate::halt::Halt>,
) {
// One cache across every title: a disc's playlists overwhelmingly
// reference the same handful of clips (main feature, play-all,
// seamless-branch variants), so without memoisation the same physical
// extents are re-read from the drive once per playlist — 30-150 times
// on a typical Blu-ray.
let mut cache = pgs_forced_probe::ForcedProbeCache::new();
for title in titles.iter_mut() {
if title.content_format == ContentFormat::BdTs {
pgs_forced_probe::probe_and_set_forced(reader, title, &mut cache, halt);
}
}
}
pub fn canonical_title_order(
a: &DiscTitle,
b: &DiscTitle,
@@ -3272,6 +3286,321 @@ mod tests {
assert_eq!(got[0].sector_count, 100_000);
}
// ── scan_image's CSS-crack gate: DVD-only, never AACS/HD-DVD ────────────
/// Minimal VIDEO_TS.IFO (VMG): one title, VTS 1, title 1. Layout mirrors
/// `disc::dvd`'s own IFO builders (magic@0, TT_SRPT ptr@0xC4).
fn dvd_vmg_bytes() -> Vec<u8> {
let tt_srpt_sector = 1u32;
let mut d = vec![0u8; 2 * 2048];
d[0..12].copy_from_slice(b"DVDVIDEO-VMG");
d[0xC4..0xC8].copy_from_slice(&tt_srpt_sector.to_be_bytes());
let base = tt_srpt_sector as usize * 2048;
d[base..base + 2].copy_from_slice(&1u16.to_be_bytes()); // num_titles = 1
let e = base + 8;
d[e + 2..e + 4].copy_from_slice(&1u16.to_be_bytes()); // chapters
d[e + 6] = 1; // vts
d[e + 7] = 1; // vts_title
d
}
/// Minimal VTS_01_0.IFO: one PGC, one cell `[first, last]`, NTSC/4:3, no
/// audio/subs. Layout mirrors `disc::dvd`'s own IFO builders.
fn dvd_vts_bytes(vob_start: u32, first_sector: u32, last_sector: u32) -> Vec<u8> {
let pgcit_sector = 2u32;
let mut d = vec![0u8; 4 * 2048];
d[0..12].copy_from_slice(b"DVDVIDEO-VTS");
d[0xC4..0xC8].copy_from_slice(&vob_start.to_be_bytes()); // vtstt_vobs
d[0xCC..0xD0].copy_from_slice(&pgcit_sector.to_be_bytes());
d[0x202..0x204].copy_from_slice(&0u16.to_be_bytes()); // num_audio
d[0x254..0x256].copy_from_slice(&0u16.to_be_bytes()); // num_subs
let pg = pgcit_sector as usize * 2048;
d[pg..pg + 2].copy_from_slice(&1u16.to_be_bytes()); // num_pgcs = 1
let pgc_rel: u32 = 0x100;
d[pg + 8 + 4..pg + 8 + 8].copy_from_slice(&pgc_rel.to_be_bytes());
let pgc = pg + pgc_rel as usize;
d[pgc + 0x02] = 1; // nr_of_programs
d[pgc + 0x03] = 1; // nr_of_cells
d[pgc + 0x06] = 0x30; // BCD 30s duration
d[pgc + 0x07] = 0b0100_0000; // 25fps rate bits
let cell_tbl_rel: u16 = 0xF0;
let pgm_map_rel: u16 = 0xEC;
d[pgc + 0xE6..pgc + 0xE8].copy_from_slice(&pgm_map_rel.to_be_bytes());
d[pgc + 0xE8..pgc + 0xEA].copy_from_slice(&cell_tbl_rel.to_be_bytes());
d[pgc + pgm_map_rel as usize] = 1; // program 0 -> cell 1
let cell_base = pgc + cell_tbl_rel as usize;
d[cell_base + 8..cell_base + 12].copy_from_slice(&first_sector.to_be_bytes());
d[cell_base + 20..cell_base + 24].copy_from_slice(&last_sector.to_be_bytes());
d
}
/// A still-scrambled CSS DVD image must come back from `scan_image` with
/// `css.is_some()` and `encrypted == true` — the known-plaintext crack must
/// actually run against a `DiscFormat::Dvd` image with titles.
#[test]
fn scan_image_scrambled_css_dvd_is_cracked_and_marked_encrypted() {
use crate::udf::fixture::*;
let title_key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
let crackable = crackable_css_sector(&title_key).to_vec();
let vmg = dvd_vmg_bytes();
// vob_start = 1000, single cell sector [10, 10] -> 1-sector extent.
let vts = dvd_vts_bytes(1000, 10, 10);
let mut disc = MemDisc::new();
let root = DirSpec {
name: String::new(),
icb_lba: 10,
dir_data_lba: 11,
files: Vec::new(),
subdirs: vec![DirSpec {
name: "VIDEO_TS".into(),
icb_lba: 50,
dir_data_lba: 51,
files: vec![
file_with("VIDEO_TS.IFO", 60, 5000, vmg, true),
file_with("VTS_01_0.IFO", 62, 6000, vts, true),
],
subdirs: vec![],
}],
};
build_udf_skeleton(&mut disc, 10);
lay_dir(&mut disc, &root);
// IFO absolute LBA = PART_START(2000) + data_lba(6000) = 8000; extent
// absolute LBA = 8000 + vob_start(1000) + first_sector(10) = 9010.
disc.put_bytes(9010, &crackable);
let disc_scan = Disc::scan_image(&mut disc, 500_000, &ScanOptions::default())
.expect("scan_image must succeed on this synthetic DVD image");
assert_eq!(disc_scan.format, DiscFormat::Dvd, "sanity: image is DVD");
assert!(!disc_scan.titles.is_empty(), "sanity: a title was parsed");
assert!(
disc_scan.css.is_some(),
"a scrambled CSS DVD image must be cracked, not left in the clear"
);
assert!(
disc_scan.encrypted,
"a cracked CSS DVD must be reported encrypted"
);
}
/// An HD-DVD image is also MPEG-PS, but must NEVER enter the CSS
/// known-plaintext crack — its `.evo` payload can never satisfy the CSS
/// attack, and running it wastes a 50_000-sector scan budget in the real
/// case. Prove the gate actually keeps the crack away: the HD-DVD clip's
/// own extent carries a genuinely Stevenson-crackable CSS sector, so if the
/// gate wrongly let the crack run, `disc.css` would come back `Some`.
#[test]
fn scan_image_hddvd_never_enters_css_crack() {
use crate::udf::fixture::*;
let title_key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
let crackable = crackable_css_sector(&title_key).to_vec();
let mut disc = MemDisc::new();
let root = DirSpec {
name: String::new(),
icb_lba: 10,
dir_data_lba: 11,
files: Vec::new(),
subdirs: vec![DirSpec {
name: "HVDVD_TS".into(),
icb_lba: 20,
dir_data_lba: 21,
files: vec![file_with("FEATURE.EVO", 30, 9000, crackable, true)],
subdirs: vec![],
}],
};
build_udf_skeleton(&mut disc, 10);
lay_dir(&mut disc, &root);
let disc_scan = Disc::scan_image(&mut disc, 500_000, &ScanOptions::default())
.expect("scan_image must succeed on this synthetic HD-DVD image");
assert_eq!(
disc_scan.format,
DiscFormat::HdDvd,
"sanity: image is HD-DVD, not DVD"
);
assert!(!disc_scan.titles.is_empty(), "sanity: a title was parsed");
assert!(
disc_scan.css.is_none(),
"the CSS crack must never run against a non-DVD (HD-DVD/AACS) image, \
even when its content happens to be CSS-crackable"
);
}
// ── scan_with's forced-subtitle probe: BdTs-only gate (finding 10) ──────
/// A `PGS` subtitle title with the given `content_format`, one extent
/// starting at `start_lba`, for the `probe_forced_subtitles_for_bdts_titles`
/// container gate.
fn pgs_title(content_format: ContentFormat, start_lba: u32) -> DiscTitle {
DiscTitle {
content_format,
streams: vec![Stream::Subtitle(SubtitleStream {
pid: 0x1200,
codec: Codec::Pgs,
language: "eng".into(),
forced: false,
qualifier: LabelQualifier::None,
codec_data: None,
})],
extents: vec![Extent {
start_lba,
sector_count: 4,
}],
..DiscTitle::empty()
}
}
/// Records every LBA read — used to prove WHICH title's extents were
/// actually touched, not merely that some read happened.
struct ForcedProbeSpyReader {
lbas: std::cell::RefCell<Vec<u32>>,
}
impl SectorSource for ForcedProbeSpyReader {
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
self.lbas.borrow_mut().push(lba);
let n = (count as usize * 2048).min(buf.len());
buf[..n].fill(0);
Ok(n)
}
}
/// The forced-subtitle probe gates on `content_format == BdTs`, NOT on
/// whether a title happens to declare a PGS stream: an HD-DVD/DVD
/// (`MpegPs`) title's PES container is not what the shared PGS classifier
/// understands, so it must never be probed even if it carries a PGS pid.
/// The BdTs title (at LBA 0) must be probed; the MpegPs title (at LBA
/// 5000) must never be touched.
#[test]
fn probe_forced_subtitles_only_touches_bdts_titles() {
let mut titles = vec![
pgs_title(ContentFormat::BdTs, 0),
pgs_title(ContentFormat::MpegPs, 5000),
];
let mut reader = ForcedProbeSpyReader {
lbas: std::cell::RefCell::new(Vec::new()),
};
Disc::probe_forced_subtitles_for_bdts_titles(&mut reader, &mut titles, None);
let lbas = reader.lbas.borrow();
assert!(
!lbas.is_empty(),
"the BdTs title must be probed (a read must occur)"
);
assert!(
lbas.iter().all(|&l| l < 5000),
"the MpegPs title's extent (LBA 5000) must never be read: {lbas:?}"
);
}
// ── scan_with's capacity_bytes feeds canonical_title_order (finding 10) ─
/// Two HD-DVD `.evo` clips (no VTI/playlist, so each is its own title —
/// see `scan_hddvd_titles`), with the given DECLARED byte sizes. No real
/// content is written; `scan_with`'s capacity-threshold ranking depends
/// only on the ICB-declared size, not the bytes at the extent.
fn hddvd_two_clip_disc(
main_bytes: u32,
other_bytes: u32,
) -> (crate::udf::fixture::MemDisc, udf::UdfFs) {
use crate::udf::fixture::*;
let files = vec![
file("MAIN.EVO", 100, 5_000, main_bytes, true),
file("OTHER.EVO", 101, 50_000, other_bytes, true),
];
let root = DirSpec {
name: String::new(),
icb_lba: 10,
dir_data_lba: 11,
files: Vec::new(),
subdirs: vec![DirSpec {
name: "HVDVD_TS".into(),
icb_lba: 20,
dir_data_lba: 21,
files,
subdirs: vec![],
}],
};
let mut disc = MemDisc::new();
build_udf_skeleton(&mut disc, 10);
lay_dir(&mut disc, &root);
let udf = crate::udf::read_filesystem(&mut disc).expect("fs");
(disc, udf)
}
/// `scan_with`'s `capacity_bytes = capacity as u64 * 2048` feeds
/// `canonical_title_order`'s "bigger than the whole disc = play-all
/// composite" threshold. Chosen so `capacity * 2048` clears both titles
/// (neither is oversize, so the bigger one — OTHER — ranks first), but
/// `capacity + 2048` lands BETWEEN the two sizes: MAIN stays non-oversize
/// while OTHER flips to oversize and gets demoted, changing `titles[0]`.
/// A `*` → `+` mutation is caught by this ranking flip, not merely a
/// wrong numeric threshold value.
#[test]
fn scan_with_capacity_bytes_uses_multiplication_not_addition() {
const CAPACITY_SECTORS: u32 = 3_997_952; // *2048 = 8_187_805_696; +2048 = 4_000_000
let (mut disc, udf) = hddvd_two_clip_disc(3_000_000, 5_000_000);
let scanned = Disc::scan_with(
&mut disc,
CAPACITY_SECTORS,
None,
None,
&ScanOptions::default(),
udf,
)
.expect("scan_with must succeed on this synthetic HD-DVD image");
assert_eq!(
scanned.titles.first().map(|t| t.playlist.as_str()),
Some("OTHER.EVO"),
"with the real *2048 capacity both titles are non-oversize, so the \
bigger clip (OTHER, 5_000_000) must rank first: {:?}",
scanned
.titles
.iter()
.map(|t| &t.playlist)
.collect::<Vec<_>>()
);
}
/// Same mechanism as above, tuned to catch a `*` → `/` mutation instead:
/// `capacity * 2048` clears both titles, but `capacity / 2048` collapses
/// to a threshold far below MAIN's declared size while still leaving
/// MAIN under it, so only OTHER flips to oversize.
#[test]
fn scan_with_capacity_bytes_uses_multiplication_not_division() {
const CAPACITY_SECTORS: u32 = 204_800_000; // *2048 = huge; /2048 = 100_000
let (mut disc, udf) = hddvd_two_clip_disc(1_000, 5_000_000);
let scanned = Disc::scan_with(
&mut disc,
CAPACITY_SECTORS,
None,
None,
&ScanOptions::default(),
udf,
)
.expect("scan_with must succeed on this synthetic HD-DVD image");
assert_eq!(
scanned.titles.first().map(|t| t.playlist.as_str()),
Some("OTHER.EVO"),
"with the real *2048 capacity both titles are non-oversize, so the \
bigger clip (OTHER, 5_000_000) must rank first: {:?}",
scanned
.titles
.iter()
.map(|t| &t.playlist)
.collect::<Vec<_>>()
);
}
// ── Unknown must not fabricate a plausible value (finding 2) ────────────
/// `Resolution::Unknown` has no dimensions, so `pixels()` must report none
@@ -3838,6 +4167,201 @@ mod tests {
);
}
/// `read_mkb_content` reads a bounded 16 MiB starting prefix and must GROW
/// it when the MKB's real record stream runs past that prefix — otherwise
/// the caller silently gets a truncated MKB. This fixture's record stream
/// is deliberately built so the first 16 records land EXACTLY on the 16
/// MiB boundary (so the 16 MiB prefix alone parses as a clean, complete
/// record stream — the case a naive "n < buf.len() means done" check would
/// wrongly accept) and 4 more MiB of records follow, terminated by the
/// explicit `00 000000` end marker. Only a caller that actually grows past
/// 16 MiB recovers the true 20 MiB content length.
///
/// Run inside a watchdog thread: some mutants of the growth step (e.g.
/// `want / 2` instead of `want * 2`) walk `want` to 0 and spin forever
/// re-reading a zero-length prefix, which must fail this test rather than
/// hang the suite.
#[test]
fn read_mkb_content_grows_prefix_past_16mib_when_records_run_longer() {
use crate::udf::fixture::*;
const REC_LEN: usize = 1024 * 1024; // 1 MiB, header included
const N_RECORDS: usize = 20; // 20 MiB of real record stream
const TOTAL: usize = N_RECORDS * REC_LEN;
let mut mkb = Vec::with_capacity(TOTAL + 4);
for _ in 0..N_RECORDS {
mkb.push(0x04); // REC_SUBSET_DIFFERENCE — any non-zero, non-terminator type
let len = REC_LEN as u32;
mkb.push((len >> 16) as u8);
mkb.push((len >> 8) as u8);
mkb.push(len as u8);
mkb.resize(mkb.len() + (REC_LEN - 4), 0xAA);
}
mkb.extend_from_slice(&[0, 0, 0, 0]); // explicit end-of-records marker
assert_eq!(mkb.len(), TOTAL + 4);
let mut disc = MemDisc::new();
let root = DirSpec {
name: String::new(),
icb_lba: 10,
dir_data_lba: 11,
files: Vec::new(),
subdirs: vec![DirSpec {
name: "AACS".into(),
icb_lba: 12,
dir_data_lba: 13,
files: vec![file_with("MKB_RO.inf", 14, 1000, mkb, true)],
subdirs: vec![],
}],
};
build_udf_skeleton(&mut disc, 10);
lay_dir(&mut disc, &root);
let udf = crate::udf::read_filesystem(&mut disc).expect("fs");
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut disc = disc;
let r = Disc::read_mkb_content(&mut disc, &udf).map(|v| v.len());
let _ = tx.send(r);
});
let got = rx
.recv_timeout(std::time::Duration::from_secs(5))
.expect(
"read_mkb_content did not terminate — the prefix-growth loop \
spun forever instead of converging on the record-stream length",
)
.expect("read_mkb_content failed");
assert_eq!(
got, TOTAL,
"must recover the full 20 MiB record stream, not the 16 MiB starting prefix"
);
}
// ── identify(): AACS-directory encrypted gate (finding 6) ──────────────
/// A `ScsiTransport` that serves a synthetic UDF image (built with the
/// shared `udf::fixture` helpers) through real READ CAPACITY(10) /
/// READ(10) commands, so `Disc::identify` is exercised end-to-end through
/// a real `Drive` rather than mocking `identify` itself — proving the
/// AACS-directory check is actually what the caller uses, not just that
/// `UdfFs::find_dir` works in isolation.
struct MemDiscDrive {
mem: crate::udf::fixture::MemDisc,
last_lba: u32,
}
impl crate::scsi::ScsiTransport for MemDiscDrive {
fn execute(
&mut self,
cdb: &[u8],
_dir: crate::scsi::DataDirection,
data: &mut [u8],
_timeout_ms: u32,
) -> Result<crate::scsi::ScsiResult> {
match cdb.first().copied() {
Some(op) if op == crate::scsi::SCSI_READ_CAPACITY => {
data[0..4].copy_from_slice(&self.last_lba.to_be_bytes());
data[4..8].copy_from_slice(&2048u32.to_be_bytes());
Ok(crate::scsi::ScsiResult {
status: 0,
bytes_transferred: 8,
sense: [0u8; 32],
})
}
Some(op) if op == crate::scsi::SCSI_READ_10 => {
let lba = u32::from_be_bytes([cdb[2], cdb[3], cdb[4], cdb[5]]);
let count = u16::from_be_bytes([cdb[7], cdb[8]]);
let n = self.mem.read_sectors(lba, count, data, false)?;
Ok(crate::scsi::ScsiResult {
status: 0,
bytes_transferred: n,
sense: [0u8; 32],
})
}
_ => Ok(crate::scsi::ScsiResult {
status: 0,
bytes_transferred: 0,
sense: [0u8; 32],
}),
}
}
}
/// Build a synthetic disc with (or without) a root `/AACS` directory
/// and/or a nested `/BDMV/AACS` directory, then run the real
/// `Disc::identify` end-to-end through a mocked `Drive`.
fn identify_with_aacs_dirs(has_aacs: bool, has_bdmv_aacs: bool) -> DiscId {
use crate::udf::fixture::*;
let mut subdirs = Vec::new();
if has_aacs {
subdirs.push(DirSpec {
name: "AACS".into(),
icb_lba: 20,
dir_data_lba: 21,
files: Vec::new(),
subdirs: Vec::new(),
});
}
if has_bdmv_aacs {
subdirs.push(DirSpec {
name: "BDMV".into(),
icb_lba: 30,
dir_data_lba: 31,
files: Vec::new(),
subdirs: vec![DirSpec {
name: "AACS".into(),
icb_lba: 32,
dir_data_lba: 33,
files: Vec::new(),
subdirs: Vec::new(),
}],
});
}
let root = DirSpec {
name: String::new(),
icb_lba: 10,
dir_data_lba: 11,
files: Vec::new(),
subdirs,
};
let mut mem = MemDisc::new();
build_udf_skeleton(&mut mem, 10);
lay_dir(&mut mem, &root);
let mut drive = crate::drive::Drive::from_transport_for_test(Box::new(MemDiscDrive {
mem,
last_lba: 99_999,
}));
Disc::identify(&mut drive).expect("identify must succeed on this synthetic image")
}
/// A root `/AACS` directory alone (the near-universal retail BD/UHD
/// shape) must report `encrypted == true`.
#[test]
fn identify_reports_encrypted_for_aacs_dir_alone() {
let id = identify_with_aacs_dirs(true, false);
assert!(id.encrypted, "/AACS alone must report encrypted");
}
/// A nested `/BDMV/AACS` directory alone must ALSO report
/// `encrypted == true` — dropping this side of the `||` (degrading it to
/// `&&`) would report virtually every retail BD as unencrypted, since
/// real discs almost never carry both paths at once.
#[test]
fn identify_reports_encrypted_for_bdmv_aacs_dir_alone() {
let id = identify_with_aacs_dirs(false, true);
assert!(id.encrypted, "/BDMV/AACS alone must report encrypted");
}
/// Neither AACS path present must report `encrypted == false`.
#[test]
fn identify_reports_unencrypted_when_no_aacs_dir_exists() {
let id = identify_with_aacs_dirs(false, false);
assert!(
!id.encrypted,
"no AACS directory at all must report unencrypted"
);
}
/// Title selection is by largest physical size, NOT clip count or duration.
/// Real-disc shape (Fast Five): a 57 GB / 11-clip feature must outrank both a
/// small 1-clip bonus reel and a long-but-tiny decoy "play-all" (91 reused
@@ -5456,6 +5980,43 @@ mod tests {
);
}
/// A major sync whose rate nibble (`format_info` bits 31..28) is `0x3` —
/// not one of the six whitelisted rates `truehd_sample_rate_hz` recognises
/// — must leave the container's sample rate untouched, mirroring the
/// channels guard above ("never write a wrong SamplingFrequency"). Both
/// channel-assignment masks are zeroed so `truehd_channels` returns `None`
/// too, isolating this to the sample-rate guard alone.
#[test]
fn correct_truehd_channels_leaves_sample_rate_when_nibble_unrecognized() {
let pid = 0x1100u16;
let format_info = 0x3u32 << 28; // unrecognised rate nibble; ch8/ch6 masks = 0
let es = thd_major_sync_es(format_info, 0);
let ts = thd_bd_pes(pid, &es);
let mut title = DiscTitle::empty();
title.streams = vec![truehd_audio_stream(
pid,
AudioChannels::Surround51,
SampleRate::S48,
)];
title.extents = vec![Extent {
start_lba: 0,
sector_count: 1,
}];
let mut reader = ThdSpyReader {
calls: std::cell::RefCell::new(Vec::new()),
data: ts,
};
correct_truehd_channels(&mut reader, &mut title);
let Stream::Audio(a) = &title.streams[0] else {
panic!("stream type must be preserved")
};
assert_eq!(
a.sample_rate,
SampleRate::S48,
"an unrecognised major-sync rate nibble must not overwrite the container's sample rate"
);
}
// ── bytes_bad_in_title: empty-input guard ────────────────────────────
// NOTE: `bad_ranges.is_empty() || title.extents.is_empty()` (mod.rs:628) —
+121
View File
@@ -2458,6 +2458,127 @@ mod command_tests {
assert_eq!(d.get_config_feature(0x0000), None);
}
#[test]
fn get_config_feature_encodes_feature_code_be_in_cdb() {
// GET CONFIGURATION (0x46), RT field byte 1 = 0x02 (report the
// named feature), then the 16-bit feature code big-endian in
// bytes 2..4. 0x010D has a nonzero high byte, so a swapped shift
// (>> vs <<) or byte order bug asks the drive for the wrong
// feature entirely.
let RecordingHarness {
drive: mut d,
cdb,
timeouts: _to,
} = recording(TransportOutcome::Ok(0));
let _ = d.get_config_feature(0x010D);
let c = cdb.lock().unwrap();
assert_eq!(
&c[..4],
&[crate::scsi::SCSI_GET_CONFIGURATION, 0x02, 0x01, 0x0D],
"feature code must be big-endian in CDB bytes 2..4"
);
}
// ── spin_cycle / wait_ready: recovery entry points (LOW finding 8) ──────
/// Records EVERY CDB issued, in order — unlike `RecordingTransport`
/// (used above), which only keeps the last one. Needed to assert a
/// multi-command sequence like `spin_cycle`'s STOP-then-START.
struct SequenceTransport {
cdbs: Arc<Mutex<Vec<Vec<u8>>>>,
ok: bool,
}
impl ScsiTransport for SequenceTransport {
fn execute(
&mut self,
cdb: &[u8],
_dir: DataDirection,
_data: &mut [u8],
_timeout_ms: u32,
) -> Result<ScsiResult> {
self.cdbs.lock().unwrap().push(cdb.to_vec());
if self.ok {
Ok(ScsiResult {
status: 0,
bytes_transferred: 0,
sense: [0u8; 32],
})
} else {
Err(Error::ScsiError {
opcode: cdb[0],
status: 2,
sense: None,
})
}
}
}
/// The documented BU40N/Initio wedge recovery: spin the disc down then
/// back up WITHOUT ejecting. Exactly two START STOP UNIT (0x1B) commands,
/// in order — STOP (START=0) then START (START=1) — both with LOEJ=0
/// (byte 4 bit 1 clear): a slot-loading BU40N must never eject during
/// unattended recovery. Real time cost (~15s: the validated 5s spin-down
/// idle + 10s spin-up settle) is accepted here rather than adding an
/// injectable-sleep seam.
#[test]
fn spin_cycle_issues_stop_then_start_without_ejecting() {
let cdbs = Arc::new(Mutex::new(Vec::new()));
let t = SequenceTransport {
cdbs: cdbs.clone(),
ok: true,
};
let mut d = Drive::from_transport_for_test(Box::new(t));
d.spin_cycle()
.expect("spin_cycle must succeed when both SCSI commands succeed");
let seq = cdbs.lock().unwrap();
assert_eq!(
seq.len(),
2,
"spin_cycle must issue exactly two commands: {seq:?}"
);
assert_eq!(seq[0][0], SCSI_START_STOP_UNIT);
assert_eq!(seq[0][4], 0x00, "first command: START=0 (spin down)");
assert_eq!(seq[1][0], SCSI_START_STOP_UNIT);
assert_eq!(seq[1][4], 0x01, "second command: START=1 (spin up)");
for (i, c) in seq.iter().enumerate() {
assert_eq!(
c[4] & 0x02,
0,
"LOEJ bit must be clear on command {i} — spin_cycle must never eject"
);
}
}
/// A drive that never answers TEST UNIT READY successfully must surface
/// `Err(DeviceNotReady)`, not silently report ready. Real time cost
/// accepted (60 x 500ms = ~30s) rather than adding an injectable-sleep
/// seam for the poll backoff.
#[test]
fn wait_ready_returns_err_when_drive_never_becomes_ready() {
struct NeverReady;
impl ScsiTransport for NeverReady {
fn execute(
&mut self,
cdb: &[u8],
_dir: DataDirection,
_data: &mut [u8],
_timeout_ms: u32,
) -> Result<ScsiResult> {
Err(Error::ScsiError {
opcode: cdb[0],
status: 2,
sense: None,
})
}
}
let mut d = Drive::from_transport_for_test(Box::new(NeverReady));
let r = d.wait_ready();
assert!(
matches!(r, Err(Error::DeviceNotReady { .. })),
"a drive that never answers TUR successfully must be DeviceNotReady, got {r:?}"
);
}
// ── report_key / mode_sense / read_buffer empty-vs-some ─────────
#[test]
+177
View File
@@ -1546,4 +1546,181 @@ mod tests {
assert_eq!(cf.member_descriptor(&m), Some("()V"));
assert_ne!(cf.member_descriptor(&m), Some("doStuff"));
}
// -----------------------------------------------------------------
// Reader::u16/u32/u64 boundary + value correctness
//
// Mirrors `slice_boundary_is_inclusive_of_the_final_byte`: an
// exact-fit read must succeed, one byte short must fail. Plus
// positive-value tests so a scrambled byte assembly (not just an
// out-of-bounds read) would be caught.
// -----------------------------------------------------------------
#[test]
fn u16_boundary_is_inclusive_of_the_final_byte() {
let data = [0xAB, 0xCD];
let mut r = Reader::new(&data);
assert_eq!(r.u16("exact fit").expect("2 bytes available"), 0xABCD);
let data = [0xAB];
let mut r = Reader::new(&data);
assert!(matches!(
r.u16("one byte short"),
Err(Error::UnexpectedEof { .. })
));
}
#[test]
fn u16_decodes_big_endian_value() {
let data = [0x01, 0x02];
let mut r = Reader::new(&data);
assert_eq!(r.u16("value").unwrap(), 0x0102);
}
#[test]
fn u32_boundary_is_inclusive_of_the_final_byte() {
let data = [0x00, 0x00, 0x00, 0x2A];
let mut r = Reader::new(&data);
assert_eq!(r.u32("exact fit").expect("4 bytes available"), 42);
let data = [0x00, 0x00, 0x00];
let mut r = Reader::new(&data);
assert!(matches!(
r.u32("one byte short"),
Err(Error::UnexpectedEof { .. })
));
}
#[test]
fn u32_decodes_big_endian_value() {
let data = [0x00, 0x00, 0x05, 0x39]; // 1337
let mut r = Reader::new(&data);
assert_eq!(r.u32("value").unwrap(), 1337);
}
#[test]
fn u64_boundary_is_inclusive_of_the_final_byte() {
// pos == 0, buffer exactly 8 bytes: must succeed.
let data = [0, 0, 0, 0, 0, 0, 0, 0x7B]; // 123
let mut r = Reader::new(&data);
assert_eq!(r.u64("exact fit").expect("8 bytes available"), 123);
// pos == 0, buffer one byte short of 8: must fail cleanly, not
// panic on the internal self.data[self.pos + 7] index.
let data = [0u8; 7];
let mut r = Reader::new(&data);
assert!(matches!(
r.u64("one byte short"),
Err(Error::UnexpectedEof { .. })
));
}
#[test]
fn u64_decodes_big_endian_value() {
let data = [0, 0, 0, 0, 0, 0, 0x05, 0x39]; // 1337
let mut r = Reader::new(&data);
assert_eq!(r.u64("value").unwrap(), 1337);
}
// -----------------------------------------------------------------
// decode_modified_utf8: 3-byte (BMP) decode path
// -----------------------------------------------------------------
#[test]
fn modified_utf8_three_byte_cjk() {
// U+3042 (hiragana あ) in modified UTF-8: 1110xxxx 10xxxxxx 10xxxxxx
// = 0xE3 0x81 0x82.
let s = decode_modified_utf8(&[0xE3, 0x81, 0x82]).unwrap();
assert_eq!(s, "\u{3042}");
}
#[test]
fn modified_utf8_three_byte_rejects_bad_first_continuation() {
// Valid lead byte (0xE3), but the first continuation byte is not
// 10xxxxxx (0x01 instead) — must be rejected, proving the first
// `& 0xC0 != 0x80` check is live.
assert!(decode_modified_utf8(&[0xE3, 0x01, 0x82]).is_err());
}
#[test]
fn modified_utf8_three_byte_rejects_bad_second_continuation() {
// Valid lead + first continuation, but the second continuation
// byte is not 10xxxxxx — proves the second check is independently
// live (not short-circuited by the first).
assert!(decode_modified_utf8(&[0xE3, 0x81, 0x01]).is_err());
}
// -----------------------------------------------------------------
// read_constant_pool: Long/Double two-slot skip, real byte parsing
// -----------------------------------------------------------------
#[test]
fn constant_pool_long_entry_occupies_two_slots_via_real_parse() {
// Real class-file bytes (not the `from_entries` synthetic ctor):
// magic + minor/major + cp_count=4 + tag=5 (Long, 8-byte payload
// at index 1, reserved slot at index 2) + tag=1 (Utf8 at index 3)
// + empty access_flags/this/super/interfaces/fields/methods/attrs.
let mut buf = vec![
0xCA, 0xFE, 0xBA, 0xBE, // magic
0x00, 0x00, // minor
0x00, 0x34, // major
0x00, 0x04, // cp_count = 4 (0=Empty,1=Long,2=Empty tail,3=Utf8)
5, // Long tag
];
buf.extend_from_slice(&0x1122_3344_5566_7788u64.to_be_bytes()); // 8-byte payload
buf.push(1); // Utf8 tag
let name = b"marker";
buf.extend_from_slice(&(name.len() as u16).to_be_bytes());
buf.extend_from_slice(name);
// access_flags, this_class, super_class, interfaces_count
buf.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0]);
// fields_count, methods_count, attributes_count
buf.extend_from_slice(&[0, 0, 0, 0, 0, 0]);
let cf = ClassFile::parse(&buf).expect("well-formed synthetic class file");
assert_eq!(cf.constant_pool.len(), 4);
// The Long occupies indices 1 AND 2 (its reserved tail slot).
// The Utf8 must resolve at index 3 = long_index(1) + 2, NOT +1.
assert_eq!(cf.constant_pool.utf8(3), Some("marker"));
// Index 2 is the reserved tail slot: not a Utf8, must not
// resolve as one (guards against the Utf8 landing one slot early).
assert_eq!(cf.constant_pool.utf8(2), None);
match cf.constant_pool.get(1) {
Some(CpInfo::Long(v)) => assert_eq!(*v, 0x1122_3344_5566_7788u64 as i64),
other => panic!("expected Long at index 1, got {:?}", other),
}
}
// -----------------------------------------------------------------
// instruction_size: tableswitch/lookupswitch with non-degenerate
// low/high/npairs (the existing tests only cover low==high==0 and
// npairs==0, which can't distinguish `-` from `+` in the entry-count
// arithmetic).
// -----------------------------------------------------------------
#[test]
fn instruction_size_tableswitch_non_degenerate_range() {
// low=1, high=4 -> 4 entries (high-low+1 = 4). A `-`->`+` mutation
// on that arithmetic would instead compute high+low+1 = 6.
let mut code = vec![TABLESWITCH];
code.extend_from_slice(&[0, 0, 0]); // padding
code.extend_from_slice(&[0, 0, 0, 0]); // default offset
code.extend_from_slice(&1i32.to_be_bytes()); // low = 1
code.extend_from_slice(&4i32.to_be_bytes()); // high = 4
code.extend_from_slice(&[0; 16]); // 4 jump entries * 4 bytes
// total = 1 (opcode) + 3 (pad) + 12 (default/low/high) + 16 (entries) = 32
assert_eq!(instruction_size(&code, 0), Some(32));
}
#[test]
fn instruction_size_lookupswitch_non_degenerate_npairs() {
// npairs = 3 -> 3 * 8 = 24 bytes of pairs.
let mut code = vec![LOOKUPSWITCH];
code.extend_from_slice(&[0, 0, 0]); // padding
code.extend_from_slice(&[0, 0, 0, 0]); // default
code.extend_from_slice(&3i32.to_be_bytes()); // npairs = 3
code.extend_from_slice(&[0; 24]); // 3 pairs
// total = 1 + 3 + 8 (default/npairs) + 24 = 36
assert_eq!(instruction_size(&code, 0), Some(36));
}
}
+222
View File
@@ -2363,3 +2363,225 @@ mod fill_gaps_sort_tests {
assert_eq!(framework[1].stream_number, 1);
}
}
// ── append_clpi_orphans ─────────────────────────────────────────────────────
#[cfg(test)]
mod clpi_orphan_tests {
use super::*;
use crate::udf::fixture::*;
fn label(t: StreamLabelType, n: u16, lang: &str, codec: &str) -> StreamLabel {
StreamLabel {
stream_number: n,
stream_type: t,
language: lang.into(),
name: String::new(),
purpose: LabelPurpose::Normal,
qualifier: LabelQualifier::None,
codec_hint: codec.into(),
variant: String::new(),
}
}
/// Build a CLPI ProgramInfo section for one program with the given
/// (pid, stream_coding_info) pairs. Layout mirrors
/// `crate::clpi::parse_program_info`'s expectations: length(4) +
/// reserved(1) + num_programs(1), then per-program
/// spn(4)+pmt_pid(2)+num_streams(1)+num_groups(1), then per-stream
/// pid(2)+sci_len(1)+sci.
fn build_program_info(streams: &[(u16, Vec<u8>)]) -> Vec<u8> {
let mut body = Vec::new();
body.push(0); // reserved
body.push(1); // num_programs = 1
body.extend_from_slice(&0u32.to_be_bytes()); // spn_program_sequence_start
body.extend_from_slice(&0u16.to_be_bytes()); // program_map_pid
body.push(streams.len() as u8); // num_streams
body.push(0); // num_groups
for (pid, sci) in streams {
body.extend_from_slice(&pid.to_be_bytes());
body.push(sci.len() as u8);
body.extend_from_slice(sci);
}
let mut out = Vec::new();
out.extend_from_slice(&(body.len() as u32).to_be_bytes());
out.extend_from_slice(&body);
out
}
/// Build a full CLPI byte buffer (HDMV header + ProgramInfo) declaring
/// the given (pid, coding_type, lang) streams. `sci` layout follows
/// `crate::clpi::parse_program_info`'s per-coding-type match arms:
/// PG/IG = coding_type + 3-byte lang; audio (primary or secondary) =
/// coding_type + format/rate byte + 3-byte lang.
fn build_clpi(streams: &[(u16, u8, &str)]) -> Vec<u8> {
use crate::consts::coding_type as c;
let sci_streams: Vec<(u16, Vec<u8>)> = streams
.iter()
.map(|(pid, coding, lang)| {
let lang_bytes = lang.as_bytes();
let sci = match *coding {
c::PG | c::IG => {
let mut v = vec![*coding];
v.extend_from_slice(lang_bytes);
v
}
_ => {
let mut v = vec![*coding, 0x61];
v.extend_from_slice(lang_bytes);
v
}
};
(*pid, sci)
})
.collect();
let pi = build_program_info(&sci_streams);
let mut buf = vec![0u8; 60];
buf[0..4].copy_from_slice(b"HDMV");
buf[4..8].copy_from_slice(b"0200");
let prog_info_start: u32 = 60;
buf[12..16].copy_from_slice(&prog_info_start.to_be_bytes());
buf[56..60].copy_from_slice(&1000u32.to_be_bytes()); // source_packet_count
buf.extend_from_slice(&pi);
buf
}
/// Lay a minimal BDMV/CLIPINF/00001.clpi tree on `disc`, with the CLPI
/// declaring the given synthetic streams, and return the parsed UdfFs.
fn fs_with_clpi(disc: &mut MemDisc, streams: &[(u16, u8, &str)]) -> crate::udf::UdfFs {
let clpi_data = build_clpi(streams);
let clipinf = DirSpec {
name: "CLIPINF".to_string(),
icb_lba: 24,
dir_data_lba: 25,
files: vec![file_with("00001.clpi", 26, 8000, clpi_data, false)],
subdirs: vec![],
};
let bdmv = DirSpec {
name: "BDMV".to_string(),
icb_lba: 20,
dir_data_lba: 21,
files: Vec::new(),
subdirs: vec![clipinf],
};
let root = DirSpec {
name: String::new(),
icb_lba: 10,
dir_data_lba: 11,
files: Vec::new(),
subdirs: vec![bdmv],
};
build_udf_skeleton(disc, 10);
lay_dir(disc, &root);
crate::udf::read_filesystem(disc).expect("fs")
}
/// (a) A PG-coded CLPI orphan becomes a Subtitle label.
#[test]
fn pg_orphan_becomes_subtitle() {
let mut disc = MemDisc::new();
let udf = fs_with_clpi(
&mut disc,
&[(0x1200, crate::consts::coding_type::PG, "eng")],
);
let mut labels: Vec<StreamLabel> = Vec::new();
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
assert_eq!(added, 1);
assert_eq!(labels.len(), 1);
assert_eq!(labels[0].stream_type, StreamLabelType::Subtitle);
assert_eq!(labels[0].stream_number, 1);
}
/// (b) An audio-range-coded orphan (here DTS-HD MA, the top of the
/// `LPCM..=DTS_HD_MA` primary-audio range) becomes an Audio label.
#[test]
fn audio_range_orphan_becomes_audio() {
let mut disc = MemDisc::new();
let udf = fs_with_clpi(
&mut disc,
&[(0x1100, crate::consts::coding_type::DTS_HD_MA, "eng")],
);
let mut labels: Vec<StreamLabel> = Vec::new();
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
assert_eq!(added, 1);
assert_eq!(labels[0].stream_type, StreamLabelType::Audio);
}
/// (b, secondary) AC3_PLUS_SECONDARY is outside the primary
/// `LPCM..=DTS_HD_MA` range and must be classified through the
/// dedicated secondary-audio arm.
#[test]
fn secondary_audio_orphan_becomes_audio() {
let mut disc = MemDisc::new();
let udf = fs_with_clpi(
&mut disc,
&[(
0x1A00,
crate::consts::coding_type::AC3_PLUS_SECONDARY,
"eng",
)],
);
let mut labels: Vec<StreamLabel> = Vec::new();
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
assert_eq!(added, 1);
assert_eq!(labels[0].stream_type, StreamLabelType::Audio);
}
/// (c) IG (0x91, BD-J menu overlay) is not a user-facing subtitle and
/// must be skipped entirely, not appended as anything.
#[test]
fn ig_orphan_is_skipped() {
let mut disc = MemDisc::new();
let udf = fs_with_clpi(
&mut disc,
&[(0x1201, crate::consts::coding_type::IG, "eng")],
);
let mut labels: Vec<StreamLabel> = Vec::new();
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
assert_eq!(added, 0);
assert!(labels.is_empty());
}
/// (d) Numbering continues from `max(existing) + 1` and increments once
/// per new orphan stream, independently of PID order.
#[test]
fn numbering_continues_from_max_existing_and_increments() {
let mut disc = MemDisc::new();
let udf = fs_with_clpi(
&mut disc,
&[
(0x1100, crate::consts::coding_type::TRUEHD, "eng"),
(0x1101, crate::consts::coding_type::AC3, "fra"),
],
);
let mut labels = vec![label(StreamLabelType::Audio, 3, "jpn", "DTS")];
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
assert_eq!(added, 2);
let mut nums: Vec<u16> = labels
.iter()
.filter(|l| l.stream_type == StreamLabelType::Audio && l.language != "jpn")
.map(|l| l.stream_number)
.collect();
nums.sort();
assert_eq!(
nums,
vec![4, 5],
"orphans must number 4 and 5 after the existing max of 3"
);
}
/// (e) A CLPI stream whose (type, language, codec) tuple already exists
/// in `existing` is a duplicate and must be skipped, not double-listed.
#[test]
fn duplicate_type_lang_codec_already_in_existing_is_skipped() {
let mut disc = MemDisc::new();
let udf = fs_with_clpi(
&mut disc,
&[(0x1100, crate::consts::coding_type::TRUEHD, "eng")],
);
let mut labels = vec![label(StreamLabelType::Audio, 1, "eng", "TrueHD")];
let added = append_clpi_orphans(&mut labels, &mut disc, &udf);
assert_eq!(added, 0);
assert_eq!(labels.len(), 1);
}
}
+105
View File
@@ -2272,6 +2272,56 @@ mod tests {
);
}
/// The wrap-vs-backstep test above (`cra_after_33bit_pts_wrap_not_rewritten`)
/// keeps `high_pts` near the very top of the 33-bit range, which is close
/// enough to the `PTS_WRAP_PERIOD / 2` threshold that `high - unwrapped`
/// and a hand-flipped `high + unwrapped` land on the SAME side of the
/// threshold at every step in that sequence — it does not actually
/// distinguish the two. This test uses PTS magnitudes around 3e9 (order
/// 2^32, well below the wrap threshold but large enough that a real
/// stream reaches it in well under an hour), where the subtraction and
/// the addition diverge: ordinary forward progression keeps
/// `high - unwrapped` small (no bogus wrap), but `high + unwrapped`
/// already exceeds `PTS_WRAP_PERIOD / 2` on the very next frame, which
/// pollutes `pts_wrap_offset` by a full period. That pollution then
/// masks the GENUINE two-clip splice that follows: the splice's small
/// reset PTS gets `+= pts_wrap_offset` and lands ABOVE the (also
/// polluted) high-water mark instead of below it, so the backward-step
/// detector never fires and the splice CRA is wrongly left as CRA
/// instead of being rewritten to BLA_W_LP.
#[test]
fn cra_splice_detected_at_large_pts_magnitude_not_masked_by_wrap_logic() {
let mut parser = HevcParser::new();
// Clip 1: two ordinary forward-progressing frames at ~3e9 ticks
// (order 2^32, comfortably below PTS_WRAP_PERIOD/2 = 2^32 exactly,
// and far from the actual 2^33 wrap point).
let clip1_base = 3_000_000_000i64;
parser.parse(&make_pes(cra_au(&[0x01]), Some(clip1_base)));
let dip = parser.parse(&make_pes(cra_au(&[0x02]), Some(clip1_base + 3750)));
assert_eq!(
nal_type_of(&nals_of(&dip[0].data)[0]),
NAL_CRA_NUT,
"ordinary forward progression at large PTS magnitude must not itself \
be mistaken for anything"
);
// Clip 2 splice: PES PTS resets to a small new-clip base — a genuine,
// large (~3e9-tick) backward step that is NOT a 2^33 wrap (the
// backward delta here is far short of PTS_WRAP_PERIOD/2).
let splice = parser.parse(&make_pes(cra_au(&[0x03]), Some(500)));
assert_eq!(
nal_type_of(&nals_of(&splice[0].data)[0]),
NAL_BLA_W_LP,
"a genuine large backward PTS reset at this magnitude must still be \
detected as a clip splice and rewrite the CRA to BLA_W_LP"
);
assert_eq!(
splice[0].pts_ns,
pts_to_ns(500),
"the emitted PTS must be the raw splice-clip PTS, unaffected by the \
internal unwrap bookkeeping"
);
}
/// Test 3: non-CRA NALs are never rewritten even when a boundary IS marked.
/// IDR (19), RASL (8/9), VPS/SPS/PPS, and a trailing slice all pass through
/// unmodified; the IDR clears the pending boundary so no later CRA is wrongly
@@ -3345,6 +3395,61 @@ mod tests {
);
}
/// The hvcC array length is a 16-bit big-endian field written as two
/// separate `push`es: `(len >> 8) as u8` then `len as u8`. Every VPS/SPS/
/// PPS the rest of the suite feeds is well under 256 bytes, so the high
/// byte is always 0 and a `>>` -> `<<` mutation (which also always
/// truncates to 0 for those inputs, since `(len << 8) as u8` masks off
/// exactly the low 8 bits) is unobservable there. A real HEVC SPS with an
/// extended VUI/HRD block can exceed 256 bytes, so use a 300+ byte VPS,
/// SPS and PPS here and decode the 16-bit length fields back to confirm
/// they round-trip to the exact NAL length, not merely a byte that
/// happens to be 0.
#[test]
fn hvcc_array_length_round_trips_above_256_bytes() {
let mut parser = HevcParser::new();
let mut data = Vec::new();
// VPS: 2-byte NAL header + 300 filler bytes -> NAL length 302.
data.extend_from_slice(&[0x00, 0x00, 0x01]);
data.extend_from_slice(&hevc_nal_header(32));
data.extend_from_slice(&vec![0x11u8; 300]);
// SPS: same size.
data.extend_from_slice(&[0x00, 0x00, 0x01]);
data.extend_from_slice(&hevc_nal_header(33));
data.extend_from_slice(&vec![0x11u8; 300]);
// PPS: same size.
data.extend_from_slice(&[0x00, 0x00, 0x01]);
data.extend_from_slice(&hevc_nal_header(34));
data.extend_from_slice(&vec![0x11u8; 300]);
parser.parse(&make_pes(data, Some(0)));
let cp = parser.codec_private().expect("hvcC");
let expected_nal_len = 2 + 300; // NAL header + payload
let mut o = 23; // past the 23-byte fixed header
assert_eq!(cp[o], 0x20 | 32, "VPS array nal_type byte");
let vps_len = u16::from_be_bytes([cp[o + 3], cp[o + 4]]) as usize;
assert_eq!(
vps_len, expected_nal_len,
"VPS length must round-trip above 256 bytes"
);
o += 5 + vps_len;
assert_eq!(cp[o], 0x20 | 33, "SPS array nal_type byte");
let sps_len = u16::from_be_bytes([cp[o + 3], cp[o + 4]]) as usize;
assert_eq!(
sps_len, expected_nal_len,
"SPS length must round-trip above 256 bytes"
);
o += 5 + sps_len;
assert_eq!(cp[o], 0x20 | 34, "PPS array nal_type byte");
let pps_len = u16::from_be_bytes([cp[o + 3], cp[o + 4]]) as usize;
assert_eq!(
pps_len, expected_nal_len,
"PPS length must round-trip above 256 bytes"
);
}
/// HEVC counterpart of `h264_ps_reorder_reconstructs_distinct_display_pts`.
///
/// A DVD/HD-DVD program stream stamps a PTS only on each GOP anchor, so the
+32
View File
@@ -1816,4 +1816,36 @@ mod tests {
);
assert_eq!(parsed.data, es);
}
/// A length-bounded PES (`pes_packet_len != 0`) must be emitted the
/// moment its declared length is EXACTLY satisfied by the buffer
/// (`sc + 6 > len`, then `e = sc + 6 + pes_packet_len; e > len`), not
/// held back waiting for a byte that will never arrive. Feed nothing
/// after the packet and don't flush — if the boundary checks were
/// `>=` instead of `>`, an exact fit would incorrectly be treated as
/// "not enough data yet" and the packet would never be produced.
#[test]
fn length_bounded_pes_exact_fit_is_emitted_not_awaited() {
let mut demuxer = PsDemuxer::new();
let payload = [0x11u8, 0x22, 0x33, 0x44, 0x55];
let mut data = vec![0x00, 0x00, 0x01, 0xC0]; // audio stream id
let pes_packet_len = (3 + payload.len()) as u16; // flags+header_len byte + payload
data.extend_from_slice(&pes_packet_len.to_be_bytes());
data.extend_from_slice(&[0x80, 0x00, 0x00]); // no PTS/DTS, header_data_len = 0
data.extend_from_slice(&payload);
assert_eq!(data.len(), 6 + pes_packet_len as usize, "sanity: exact fit");
let packets = demuxer.feed(&data);
assert_eq!(
packets.len(),
1,
"an exact-fit length-bounded PES must be emitted immediately, \
not held awaiting a byte that will never come"
);
assert_eq!(packets[0].data, payload);
assert!(
demuxer.buffer.is_empty(),
"the exact-fit PES must be fully consumed, leaving nothing buffered"
);
}
}
+56 -17
View File
@@ -76,6 +76,27 @@ fn bsd_name_of(device: &Path) -> Result<&str> {
.unwrap_or(dev_str))
}
/// Map a `shim_open_exclusive` failure sentinel (a negative return code, NOT
/// an `IOReturn`) to the typed [`Error`] variant it represents. Pulled out of
/// [`MacScsiTransport::open`] as its own callable predicate so the mapping —
/// otherwise reachable only through a real IOKit FFI call — can be pinned by
/// a test: collapsing `-4..=-2` or `-5` into the `DeviceNotFound` catch-all
/// would silently turn "another process holds the drive" into "no such
/// drive", or "the IOKit plugin chain failed" into the same.
fn map_shim_open_error(rc: i32, path: String) -> Error {
match rc {
// -2/-3/-4: IOCreatePlugInInterfaceForService /
// QueryInterface MMCDeviceInterface /
// GetSCSITaskDeviceInterface failed.
-4..=-2 => Error::IoKitPluginFailed { path, kr: 0 },
// -5: ObtainExclusiveAccess failed (held by another
// process).
-5 => Error::DeviceLocked { path, kr: 0 },
// -1 and anything else: device not present.
_ => Error::DeviceNotFound { path },
}
}
pub struct MacScsiTransport {
_bsd_name: String,
}
@@ -103,22 +124,7 @@ impl MacScsiTransport {
// Release the single-instance lock taken by the OPEN.swap above;
// a failed open must not leave it held or every later open wedges.
OPEN.store(false, Ordering::Release);
let path = bsd_name.to_string();
// The shim returns distinct negative sentinels per failure
// stage; map them to the typed variants that already exist
// rather than collapsing every failure to DeviceNotFound.
// These sentinels are not IOReturn codes, so kr is left 0.
return Err(match rc {
// -2/-3/-4: IOCreatePlugInInterfaceForService /
// QueryInterface MMCDeviceInterface /
// GetSCSITaskDeviceInterface failed.
-4..=-2 => Error::IoKitPluginFailed { path, kr: 0 },
// -5: ObtainExclusiveAccess failed (held by another
// process).
-5 => Error::DeviceLocked { path, kr: 0 },
// -1 and anything else: device not present.
_ => Error::DeviceNotFound { path },
});
return Err(map_shim_open_error(rc, bsd_name.to_string()));
}
Ok(MacScsiTransport {
@@ -306,7 +312,9 @@ pub(super) fn drive_has_disc(path: &Path) -> Result<bool> {
#[cfg(test)]
mod tests {
use super::{K_MAX_CDB_SIZE, OPEN, bsd_name_of, cstr_to_str, drive_has_disc};
use super::{
K_MAX_CDB_SIZE, OPEN, bsd_name_of, cstr_to_str, drive_has_disc, map_shim_open_error,
};
use crate::error::Error;
use std::path::Path;
use std::sync::atomic::Ordering;
@@ -378,6 +386,37 @@ mod tests {
);
}
/// Every negative sentinel `shim_open_exclusive` can return must map to
/// its own distinct typed error, not collapse into the `DeviceNotFound`
/// catch-all. `-2..=-4` (IOKit plugin chain) and `-5` (exclusive access
/// held elsewhere) are the two that a deleted match arm would silently
/// fold into "no such drive".
#[test]
fn map_shim_open_error_distinguishes_every_sentinel() {
for rc in [-2, -3, -4] {
match map_shim_open_error(rc, "disk4".into()) {
Error::IoKitPluginFailed { path, kr } => {
assert_eq!(path, "disk4");
assert_eq!(kr, 0);
}
other => panic!("rc={rc}: expected IoKitPluginFailed, got {other:?}"),
}
}
match map_shim_open_error(-5, "disk4".into()) {
Error::DeviceLocked { path, kr } => {
assert_eq!(path, "disk4");
assert_eq!(kr, 0);
}
other => panic!("expected DeviceLocked, got {other:?}"),
}
for rc in [-1, -6, i32::MIN] {
match map_shim_open_error(rc, "disk4".into()) {
Error::DeviceNotFound { path } => assert_eq!(path, "disk4"),
other => panic!("rc={rc}: expected DeviceNotFound, got {other:?}"),
}
}
}
/// A CDB longer than K_MAX_CDB_SIZE must be rejected with
/// `Error::InvalidCdbLength` before the shim is ever called. Exercises the
/// real guard `MacScsiTransport::execute` uses, without opening an IOKit