mux: thread halt into live AACS key-map resolution; cover Session arm
Round-2 follow-ups to 6d6e60f (inline base-map resolve on the live
single-pass Session/Live mux arms).
Fix 1 (halt threading) — the inline resolve chain sampled ciphertext off
the LIVE drive with no cancel token, so an operator /api/stop during key
resolution was not honored (the FMTS probe can issue hundreds of reads,
each able to stall to the 60s SCSI recovery timeout — violating the
"don't hammer a struggling live drive" rule). Add an optional
`halt: Option<&Halt>` to `resolve_mux_key_map`, `resolve_fmts_key_map`,
`resolve_inline_base_map`, and `Disc::resolve_content_key_map`, and poll
it at each loop boundary (FMTS anchor + per-index probe loops, multi-CPS
extent loop) — returning Err(Halted) promptly. Live/Session arms pass the
driver's halt; sweep/patch pass their own token (via Halt::from_arc);
file-backed probe/ISO callers pass None. Tested with a pre-cancelled halt
(Err Halted, no extent sampling) and a None-halt no-abort case;
mutation-verified (dropping the extent-loop check → Ok, not Err).
Fix 2 (Session-arm coverage) — the MuxInput::Session arm ran the same
resolve→install→decrypt sequence as Live but had NO end-to-end test
(DiscSession only exposed open(), which needs live hardware). Add a
#[cfg(test)] DiscSession::from_parts_for_test (injected reader + scanned
disc, no Drive), an end-to-end AACS decrypt test through the Session arm
(mutation-verified: dropping with_key_map → mux aborts), and a
missing-reader clean-error (not panic) test.
Fix 3 (cleanups) — io_error_code: remove the unreachable typed-Error
downcast branch (From<Error> for io::Error stringifies; no path builds an
io::Error holding a typed Error), keeping the stringify parse is_halt /
is_skippable_title_stub rely on. Add a resolve_keys_for test covering the
largest-title sampling branch. Document the patch wedge-exit coverage gap
(TODO) in passn_handler_ab.rs.
This commit is contained in:
@@ -370,6 +370,32 @@ impl DiscSession {
|
||||
pub fn take_reader(&mut self) -> Option<Box<dyn SectorSource>> {
|
||||
self.reader.take()
|
||||
}
|
||||
|
||||
/// Test-only constructor: build a session over an INJECTED reader + already-
|
||||
/// scanned disc WITHOUT opening a live [`Drive`]. `DiscSession::open` needs
|
||||
/// real hardware, so this is the only way to exercise the
|
||||
/// [`MuxInput::Session`](crate::mux::MuxInput::Session) mux arm (take_reader →
|
||||
/// resolve_inline_base_map → DiscStream → with_key_map) and
|
||||
/// [`Self::resolve_keys`]'s title-sampling branch against a synthetic reader.
|
||||
///
|
||||
/// The drive slot stays `None` (a `MuxInput::Session` mux never touches it —
|
||||
/// it reads through the staged `reader`); `device` carries a sentinel path so
|
||||
/// the driver's missing-reader error still has a name.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn from_parts_for_test(
|
||||
disc: Disc,
|
||||
reader: Option<Box<dyn SectorSource>>,
|
||||
key_fetch: Option<KeyFetch>,
|
||||
) -> DiscSession {
|
||||
DiscSession {
|
||||
drive: None,
|
||||
device: "test://session".to_string(),
|
||||
spec: KeySpec::default(),
|
||||
disc: Some(disc),
|
||||
reader,
|
||||
key_fetch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan an ISO image's structure from a file path, returning the scanned
|
||||
@@ -616,6 +642,71 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A counting reader over zeros — records the highest LBA sampled so the test
|
||||
/// can prove the LARGEST title's extent (not the small one) was read.
|
||||
struct SamplingReader {
|
||||
reads: u32,
|
||||
max_lba: u32,
|
||||
}
|
||||
impl SectorSource for SamplingReader {
|
||||
fn capacity_sectors(&self) -> u32 {
|
||||
100_000
|
||||
}
|
||||
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8], _: bool) -> Result<usize> {
|
||||
self.reads += 1;
|
||||
self.max_lba = self.max_lba.max(lba);
|
||||
let want = count as usize * 2048;
|
||||
buf[..want].fill(0);
|
||||
Ok(want)
|
||||
}
|
||||
}
|
||||
|
||||
/// `resolve_keys_for` samples the LARGEST title's ciphertext through the
|
||||
/// reader when a source is configured (the `session.rs:90` sampling branch).
|
||||
/// The other tests use a title-less disc (no sampling read), so this branch
|
||||
/// was uncovered. With two titles and a non-empty source, the sampling read
|
||||
/// fires against the LARGER title's extent.
|
||||
#[test]
|
||||
fn resolve_keys_for_samples_largest_title_through_reader() {
|
||||
use crate::disc::{DiscTitle, Extent};
|
||||
let mut disc = aacs_disc();
|
||||
let mut small = DiscTitle::empty();
|
||||
small.size_bytes = 1_000;
|
||||
small.extents = vec![Extent {
|
||||
start_lba: 100,
|
||||
sector_count: 300,
|
||||
}];
|
||||
let mut large = DiscTitle::empty();
|
||||
large.size_bytes = 9_000_000;
|
||||
large.extents = vec![Extent {
|
||||
start_lba: 9_000,
|
||||
sector_count: 300,
|
||||
}];
|
||||
disc.titles = vec![small, large];
|
||||
let mut reader = SamplingReader {
|
||||
reads: 0,
|
||||
max_lba: 0,
|
||||
};
|
||||
|
||||
// Non-empty source ⇒ the sampling read is NOT skipped.
|
||||
let resolved = resolve_keys_for(&mut reader, &mut disc, factory_of(|| HasUnitKey([1; 16])));
|
||||
|
||||
assert!(
|
||||
reader.reads > 0,
|
||||
"the largest title was sampled via the reader"
|
||||
);
|
||||
assert!(
|
||||
reader.max_lba >= 9_000,
|
||||
"sampling read the LARGER title's extent (lba>=9000), not the small one \
|
||||
(max_lba={})",
|
||||
reader.max_lba
|
||||
);
|
||||
assert!(
|
||||
resolved.key_fetch.is_some(),
|
||||
"an AACS disc still retains a read-time fetch"
|
||||
);
|
||||
}
|
||||
|
||||
/// A non-AACS disc (CSS / unencrypted — `inputs()` is `None`): resolution is a
|
||||
/// no-op. Empty trace, NO fetch, disc untouched. This is the out-of-the-box
|
||||
/// CSS/None path that must keep working with no keydb.
|
||||
|
||||
Reference in New Issue
Block a user