From a688e2c6420ad9ff55a30b841e3cc5a0b356f933 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:27:48 -0700 Subject: [PATCH] mux: FMTS read-plan on the inline live-drive path too (DiscStream::with_key_map) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/mux/disc.rs | 59 ++++++++++++++++++++++++++++++++++++++++ src/sector/decrypting.rs | 8 ++++++ 2 files changed, 67 insertions(+) diff --git a/src/mux/disc.rs b/src/mux/disc.rs index 65b0426..0752ebe 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -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) -> 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 diff --git a/src/sector/decrypting.rs b/src/sector/decrypting.rs index 7592a25..992de88 100644 --- a/src/sector/decrypting.rs +++ b/src/sector/decrypting.rs @@ -175,6 +175,14 @@ impl DecryptingSectorSource { 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) { + 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)).