mux: add MuxInput::Live for the inline live-drive single-pass path
STEP 4c-ii (layering refactor): give `mux_stream` a live-drive source so autorip's live single-pass mux stops hand-rolling `DiscStream::new`. `MuxInput::Live` is the live analogue of `MuxInput::Iso`: it wraps the raw `Box<dyn SectorSource>` in the INLINE `DiscStream` (the same constructor the `Session` arm uses — NOT `build_iso_pipeline`, the prefetch highway), so a consumer's adaptive batch-retry in `DiscStream::fill_extents` still fires on a bad live-drive sector. Unlike `Session`, `Live` carries an optional pre-resolved forensic `AacsKeyMap` that is applied via `DiscStream::with_key_map` before any read — required for single-pass FMTS correctness (read only our-phase units; decrypt the forensic segment with the mapped key). The consumer resolves keys as its own policy and hands the banked material in; the driver re-resolves nothing. Unit test drives `MuxInput::Live` with a recording `SectorSource` and a phased forensic key map, asserting the alternate-phase (odd) units are never fetched — proof the arm builds the inline `DiscStream` and applies the key map. Mutation-verified: dropping the `with_key_map` call reads the dropped odd-phase LBAs and fails the assertion.
This commit is contained in:
+177
-1
@@ -35,7 +35,7 @@ use crate::event::{BatchSizeReason, Event, EventKind};
|
|||||||
use crate::halt::Halt;
|
use crate::halt::Halt;
|
||||||
use crate::io::pipeline::{Flow, Pipeline, Sink, WRITE_PIPELINE_DEPTH};
|
use crate::io::pipeline::{Flow, Pipeline, Sink, WRITE_PIPELINE_DEPTH};
|
||||||
use crate::pes::{CountingStream, PesFrame, Stream};
|
use crate::pes::{CountingStream, PesFrame, Stream};
|
||||||
use crate::sector::{FileSectorSource, KeyFetch};
|
use crate::sector::{FileSectorSource, KeyFetch, SectorSource};
|
||||||
use crate::session::DiscSession;
|
use crate::session::DiscSession;
|
||||||
|
|
||||||
use super::resolve::{InputOptions, StreamUrl, build_iso_pipeline, input, output, parse_url};
|
use super::resolve::{InputOptions, StreamUrl, build_iso_pipeline, input, output, parse_url};
|
||||||
@@ -79,6 +79,34 @@ pub enum MuxInput<'a> {
|
|||||||
/// Optional read-time key fetch closure (banked by `resolve_keys`).
|
/// Optional read-time key fetch closure (banked by `resolve_keys`).
|
||||||
key_fetch: Option<KeyFetch>,
|
key_fetch: Option<KeyFetch>,
|
||||||
},
|
},
|
||||||
|
/// Live single-pass mux off a raw disc [`SectorSource`] (autorip's
|
||||||
|
/// live-drive path). The driver wraps the reader in a
|
||||||
|
/// [`DiscStream`](crate::mux::DiscStream) — the **INLINE** stream, NOT the
|
||||||
|
/// prefetch highway — so the consumer's adaptive batch-retry in
|
||||||
|
/// `DiscStream::fill_extents` still fires on a bad sector (the highway would
|
||||||
|
/// bypass it). The live analogue of [`MuxInput::Iso`]: the consumer resolves
|
||||||
|
/// keys as its own app-layer policy and hands the already-banked material in;
|
||||||
|
/// the driver does no internal re-resolution.
|
||||||
|
Live {
|
||||||
|
/// The raw sector source (physical drive). Boxed `dyn` — the driver owns
|
||||||
|
/// it and moves it into `DiscStream::new` (whose reader param is exactly
|
||||||
|
/// `Box<dyn SectorSource>`), so no concrete drive type leaks in here.
|
||||||
|
reader: Box<dyn SectorSource>,
|
||||||
|
/// The scanned title to mux.
|
||||||
|
title: DiscTitle,
|
||||||
|
/// Container format (TS vs PS demux selection).
|
||||||
|
format: crate::disc::ContentFormat,
|
||||||
|
/// Decryption keys the consumer already banked (`DecryptKeys::None` for
|
||||||
|
/// raw/clear). The driver consumes them as-is — never re-resolves.
|
||||||
|
keys: DecryptKeys,
|
||||||
|
/// Optional pre-resolved forensic AACS key map, applied VERBATIM via
|
||||||
|
/// [`DiscStream::with_key_map`](crate::mux::DiscStream::with_key_map)
|
||||||
|
/// BEFORE any read. `None` for every non-FMTS disc. Single-pass FMTS
|
||||||
|
/// correctness depends on this reaching the inline reader: `with_key_map`
|
||||||
|
/// rewrites the extent walk to our-phase units only and installs the map
|
||||||
|
/// so each unit decrypts with its mapped key.
|
||||||
|
key_map: Option<Arc<AacsKeyMap>>,
|
||||||
|
},
|
||||||
/// Any URL-addressed source (`iso://`, `mkv://`, `m2ts://`, `network://`,
|
/// Any URL-addressed source (`iso://`, `mkv://`, `m2ts://`, `network://`,
|
||||||
/// stdio) opened via [`input`].
|
/// stdio) opened via [`input`].
|
||||||
Url {
|
Url {
|
||||||
@@ -266,6 +294,44 @@ pub fn mux_stream(
|
|||||||
stream.on_event(reader_event_fn(events.clone()));
|
stream.on_event(reader_event_fn(events.clone()));
|
||||||
(Box::new(stream), Some(playlist))
|
(Box::new(stream), Some(playlist))
|
||||||
}
|
}
|
||||||
|
MuxInput::Live {
|
||||||
|
reader,
|
||||||
|
title,
|
||||||
|
format,
|
||||||
|
keys,
|
||||||
|
key_map,
|
||||||
|
} => {
|
||||||
|
// INLINE `DiscStream` — the same constructor the `Session` arm uses,
|
||||||
|
// NOT `build_iso_pipeline` (the prefetch highway). The consumer's
|
||||||
|
// adaptive batch-retry lives in `DiscStream::fill_extents`, which the
|
||||||
|
// highway would bypass; the live single-pass path must keep it.
|
||||||
|
let mut stream = crate::mux::DiscStream::new(
|
||||||
|
reader,
|
||||||
|
title,
|
||||||
|
keys,
|
||||||
|
opts.batch_sectors,
|
||||||
|
format,
|
||||||
|
opts.raw,
|
||||||
|
Some(halt.clone()),
|
||||||
|
)?;
|
||||||
|
if opts.raw {
|
||||||
|
stream.set_raw();
|
||||||
|
}
|
||||||
|
// Apply the forensic FMTS key map BEFORE reads begin — rewrites the
|
||||||
|
// extent walk to our-phase units only and installs the map so each
|
||||||
|
// unit decrypts with its mapped key. `None` leaves the walk unchanged
|
||||||
|
// (identical to a plain single/multi-CPS disc). Single-pass FMTS
|
||||||
|
// correctness depends on this: dropping it reads the alternate
|
||||||
|
// device-group units and mis-decrypts the forensic segment.
|
||||||
|
if let Some(map) = key_map {
|
||||||
|
stream = stream.with_key_map(map);
|
||||||
|
}
|
||||||
|
stream.skip_errors = opts.skip_errors;
|
||||||
|
// Same reader-side event vocabulary as the `Session` arm
|
||||||
|
// (`SectorSkipped` / `BatchSizeChanged` / `BytesRead`).
|
||||||
|
stream.on_event(reader_event_fn(events.clone()));
|
||||||
|
(Box::new(stream), None)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
drive_mux(
|
drive_mux(
|
||||||
@@ -933,4 +999,114 @@ mod tests {
|
|||||||
"at least one on_read_progress call observed"
|
"at least one on_read_progress call observed"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recording `SectorSource` for the live-path tests: logs every read's
|
||||||
|
/// `(lba, count)` and returns zeroed sectors (never flagged scrambled, so a
|
||||||
|
/// synthetic key map's decrypt is a pass-through — no real crypto). The
|
||||||
|
/// recorded LBAs are the observable proof of WHICH extent walk the inline
|
||||||
|
/// `DiscStream` executed.
|
||||||
|
struct RecordingReader {
|
||||||
|
capacity: u32,
|
||||||
|
log: std::sync::Arc<std::sync::Mutex<Vec<(u32, u16)>>>,
|
||||||
|
}
|
||||||
|
impl crate::sector::SectorSource for RecordingReader {
|
||||||
|
fn read_sectors(
|
||||||
|
&mut self,
|
||||||
|
lba: u32,
|
||||||
|
count: u16,
|
||||||
|
buf: &mut [u8],
|
||||||
|
_recovery: bool,
|
||||||
|
) -> crate::error::Result<usize> {
|
||||||
|
self.log.lock().unwrap().push((lba, count));
|
||||||
|
let bytes = count as usize * 2048;
|
||||||
|
buf[..bytes].fill(0);
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
fn capacity_sectors(&self) -> u32 {
|
||||||
|
self.capacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `MuxInput::Live` builds the INLINE `DiscStream` (not the highway) AND
|
||||||
|
/// applies the forensic `key_map` before reading. Proof is the recorded LBA
|
||||||
|
/// set: `with_key_map` rewrites the extent walk via `AacsKeyMap::read_plan`
|
||||||
|
/// so the alternate-phase (odd) forensic units are NEVER fetched off the
|
||||||
|
/// source. The zeroed reader can't resolve codec headers, so the mux drains
|
||||||
|
/// to a `NoStreams`/`MkvInvalid` refusal — irrelevant here; the test asserts
|
||||||
|
/// only the read plan the reader actually executed.
|
||||||
|
///
|
||||||
|
/// Mutation: deleting the `stream = stream.with_key_map(map)` line in the
|
||||||
|
/// `Live` arm leaves the extents un-rewritten, so the reader DOES fetch the
|
||||||
|
/// dropped odd-phase LBAs and the "never read" assertion below fails. (A
|
||||||
|
/// second mutation — routing `Live` through `build_iso_pipeline` instead of
|
||||||
|
/// `DiscStream::new` — would not compile against a `Box<dyn SectorSource>`
|
||||||
|
/// and drop the inline `fill_extents` retry entirely.)
|
||||||
|
#[test]
|
||||||
|
fn mux_input_live_uses_inline_discstream_and_applies_key_map() {
|
||||||
|
use crate::decrypt::{AacsKeyMap, Phase};
|
||||||
|
use crate::disc::Extent;
|
||||||
|
|
||||||
|
// 3 sectors == one AACS aligned unit. A 10-unit Even forensic segment at
|
||||||
|
// LBA [1030, 1060): kept even units start at 1030,1036,1042,1048,1054;
|
||||||
|
// the odd units 1033,1039,1045,1051,1057 must never be read.
|
||||||
|
let us = 3u32;
|
||||||
|
let map = std::sync::Arc::new(AacsKeyMap::from_ranges_phased(vec![(
|
||||||
|
1030,
|
||||||
|
1060,
|
||||||
|
5,
|
||||||
|
Phase::Even,
|
||||||
|
)]));
|
||||||
|
|
||||||
|
let mut title = aac_audio_title(0x1100);
|
||||||
|
title.extents = vec![Extent {
|
||||||
|
start_lba: 1000,
|
||||||
|
sector_count: 300,
|
||||||
|
}];
|
||||||
|
|
||||||
|
let log = std::sync::Arc::new(std::sync::Mutex::new(Vec::<(u32, u16)>::new()));
|
||||||
|
let reader = Box::new(RecordingReader {
|
||||||
|
capacity: 4000,
|
||||||
|
log: log.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let opts = MuxOptions {
|
||||||
|
skip_errors: false,
|
||||||
|
// One unit per read so the recorded LBAs land on unit boundaries and
|
||||||
|
// an odd-phase unit can't hide inside a larger coalesced batch.
|
||||||
|
batch_sectors: us as u16,
|
||||||
|
raw: false,
|
||||||
|
};
|
||||||
|
let halt = Halt::new();
|
||||||
|
// Drains to a NoStreams refusal (zeroed data resolves no headers); we
|
||||||
|
// only care about the reads it performed on the way there.
|
||||||
|
let _ = mux_stream(
|
||||||
|
MuxInput::Live {
|
||||||
|
reader,
|
||||||
|
title,
|
||||||
|
format: crate::disc::ContentFormat::BdTs,
|
||||||
|
keys: DecryptKeys::None,
|
||||||
|
key_map: Some(map),
|
||||||
|
},
|
||||||
|
"null://",
|
||||||
|
&opts,
|
||||||
|
&halt,
|
||||||
|
Arc::new(NoopEvents),
|
||||||
|
);
|
||||||
|
|
||||||
|
let reads = log.lock().unwrap();
|
||||||
|
let read_lbas: std::collections::HashSet<u32> = reads.iter().map(|&(lba, _)| lba).collect();
|
||||||
|
// The dropped odd-phase forensic units must never be fetched.
|
||||||
|
for odd in [1033u32, 1039, 1045, 1051, 1057] {
|
||||||
|
assert!(
|
||||||
|
!read_lbas.contains(&odd),
|
||||||
|
"alternate-phase forensic unit LBA {odd} was read — key_map not applied \
|
||||||
|
(with_key_map dropped from the Live arm?)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Sanity: our-phase units on either side ARE read (the walk still ran).
|
||||||
|
assert!(
|
||||||
|
read_lbas.contains(&1030) && read_lbas.contains(&1054),
|
||||||
|
"our-phase forensic units must still be read (kept LBAs 1030/1054)"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user