mux: FMTS read-plan on the inline live-drive path too (DiscStream::with_key_map)

The read_plan fix landed on the file-backed highway (build_iso_pipeline)
but the inline DiscStream (live single-pass) still passed the alternate
device-group half to the demux — a known FMTS bug on rip_mode="single".

DiscStream::with_key_map installs the proactive map and rewrites the
extent walk to the read plan, so the live single-pass path reads ONLY
our-phase units, exactly like the highway. DecryptingSectorSource gains
set_key_map for the already-constructed decorator. Non-forensic maps
return the extents unchanged, so every non-FMTS disc is byte-identical.

Test: with_key_map_reads_only_our_phase_units — a forensic Even segment
drops exactly its alternate units from the extent walk. Precommit green
on 1.86.
This commit is contained in:
Matthew Jackson
2026-07-18 13:27:48 -07:00
parent 05fb632d7c
commit a688e2c642
2 changed files with 67 additions and 0 deletions
+59
View File
@@ -351,6 +351,27 @@ impl DiscStream {
self
}
/// Install a proactive [`AacsKeyMap`](crate::decrypt::AacsKeyMap) on the inline
/// live-drive path — the counterpart to what
/// [`build_iso_pipeline`](crate::mux::resolve::build_iso_pipeline) does for the
/// file-backed highway. The map is the title's read plan: it decides which unit
/// each LBA is and, for an FMTS forensic segment, which phase is ours. The
/// extent walk is rewritten to the read plan so **only our-phase units are read
/// off the drive** (the alternate device-group units are never fetched,
/// decrypted, or muxed), and the map is installed so each unit decrypts with its
/// mapped key. A non-forensic map returns the extents unchanged, so a plain
/// single/multi-CPS disc reads exactly as before.
pub fn with_key_map(mut self, map: std::sync::Arc<crate::decrypt::AacsKeyMap>) -> Self {
self.extents = map.read_plan(&self.extents, self.unit_align.max(1) as u32);
self.bytes_total_extents = self
.extents
.iter()
.map(|e| e.sector_count as u64 * 2048)
.sum();
self.reader.set_key_map(map);
self
}
fn is_halted(&self) -> bool {
self.halt
.as_ref()
@@ -1118,6 +1139,44 @@ mod tests {
);
}
/// `with_key_map` on the inline live-drive path applies the same FMTS read plan
/// the file-backed highway uses: within a forensic segment only our-phase units
/// survive the extent walk, so the alternate device-group units are never read.
#[test]
fn with_key_map_reads_only_our_phase_units() {
use crate::decrypt::{AacsKeyMap, DecryptKeys, Phase};
// AACS keys → unit_align = 3, so a unit is 3 sectors and the phase filter
// engages. Key contents are irrelevant to the read plan.
let aacs = DecryptKeys::Aacs {
unit_keys: vec![(0, [0u8; 16]), (1, [1u8; 16])],
read_data_key: None,
format: ContentFormat::BdTs,
};
// 100 units (300 sectors). A 10-unit Even forensic segment at LBA [30,60):
// even units (30,36,42,48,54) are ours; odd (33,39,45,51,57) are dropped.
let map = AacsKeyMap::from_ranges_phased(vec![(30, 60, 1, Phase::Even)], 0);
let stream = DiscStream::new(
Box::new(ZeroReader { capacity: 300 }),
synthetic_title(300),
aacs,
8,
ContentFormat::BdTs,
)
.with_key_map(std::sync::Arc::new(map));
let total: u32 = stream.extents.iter().map(|e| e.sector_count).sum();
assert_eq!(
total,
300 - 5 * 3,
"exactly the 5 alternate-phase units (15 sectors) are dropped from the read walk"
);
assert!(
stream.extents.len() > 1,
"the forensic segment split the single extent into our-phase-only runs"
);
// The progress denominator tracks the reduced read set.
assert_eq!(stream.bytes_total_extents, total as u64 * 2048);
}
/// Recording `SectorSource`: logs every `(lba, count)` request and
/// returns `Err` whenever the requested range covers `bad_sector`.
/// Successful reads return zeroed sectors (which are NOT
+8
View File
@@ -175,6 +175,14 @@ impl<S: SectorSource> DecryptingSectorSource<S> {
self
}
/// `&mut` counterpart of [`with_key_map`](Self::with_key_map): install the
/// proactive map on an already-constructed source (the inline live-drive
/// [`DiscStream`](crate::mux::DiscStream) builds the decorator first, then
/// installs the map via its own `with_key_map`).
pub fn set_key_map(&mut self, map: Arc<crate::decrypt::AacsKeyMap>) {
self.key_map = Some(map);
}
/// Restrict decrypt to the disc's encrypted-content extents
/// (sorted/merged `(start_lba, sector_count)` — see
/// [`Disc::encrypted_content_ranges`](crate::Disc::encrypted_content_ranges)).