diff --git a/src/css/mod.rs b/src/css/mod.rs index f4922fc..b3ebe07 100644 --- a/src/css/mod.rs +++ b/src/css/mod.rs @@ -146,7 +146,14 @@ pub fn crack_key_outcome( /// ("reading is reading"). CSS keys are per-VTS and crackable from the scrambled /// data itself, so a `None`/MPEG-PS title cracks its own key here, in playback /// order over `extents`. Everything else is left untouched: -/// - AACS keys (HD-DVD `.evo` is also MPEG-PS but arrives as `Aacs`) — no CSS. +/// - a disc format that cannot carry CSS (`!disc_format.may_have_css()`, i.e. +/// HD-DVD and the BD families) — no CSS exists there to crack. This is the +/// DISC-FORMAT axis and it is separate from `format`, the container: HD-DVD +/// `.evo` is MPEG-PS exactly like DVD `.vob`, so the container alone cannot +/// tell them apart. `DiscFormat::Unknown` counts as "may have CSS" — see +/// [`crate::disc::DiscFormat::may_have_css`] for why the safe default is to +/// crack. +/// - AACS keys (an encrypted HD-DVD `.evo` also arrives as `Aacs`) — no CSS. /// - a title that already carries a key — nothing to resolve. /// - a genuinely clear DVD (no scrambled sector) — stays `None`, a mux no-op. /// @@ -157,12 +164,18 @@ pub fn crack_key_outcome( /// yield its key, so an all-titles rip skips this title and finishes the rest. /// The whole-disc failure is [`crate::error::Error::CssNoDiscKey`], raised by /// `Disc::ensure_decryptable_keys` from the scan's `css_error`. +// Eight reader/extent/key/format/mode params is inherent to a shared step that +// must be callable identically from both read paths; the two format params are +// the whole point of this function's contract (container vs disc family) and +// bundling them into a struct would only move the same fields around. +#[allow(clippy::too_many_arguments)] pub(crate) fn resolve_dvd_title_key( reader: &mut dyn SectorSource, extents: &[Extent], keys: &mut crate::decrypt::DecryptKeys, batch_sectors: u16, format: crate::disc::ContentFormat, + disc_format: crate::disc::DiscFormat, raw: bool, halt: Option<&crate::halt::Halt>, ) -> std::io::Result<()> { @@ -173,8 +186,23 @@ pub(crate) fn resolve_dvd_title_key( if raw { return Ok(()); } + // The crack is gated on TWO axes, and both are load-bearing: + // * `format == MpegPs` — the CONTAINER, i.e. "CSS descrambles 2048-byte + // program-stream sectors, not BD transport packets"; + // * `disc_format.may_have_css()` — the DISC FORMAT, i.e. "this family can + // carry CSS at all". + // Keying on the container ALONE was the defect: `ContentFormat::MpegPs` + // covers HD-DVD `.evo` as well as DVD `.vob` (both arms of the tree + // dispatch in `Disc::scan_with` set it), and HD-DVD is AACS — it has no CSS + // to find. Every HD-DVD title therefore paid a 50_000-sector crack scan + // that could not succeed, and a scan that came back `ScrambledUncracked` + // hard-failed a good disc with `CssKeyMissing` (E7023). `may_have_css` is + // deliberately false ONLY for the families proven CSS-free, so `Unknown` + // still cracks: skipping the crack on a real DVD would mux ciphertext as + // plaintext at exit 0, which is far worse than a wasted scan. if matches!(keys, crate::decrypt::DecryptKeys::None) && format == crate::disc::ContentFormat::MpegPs + && disc_format.may_have_css() { // `halt` threads the caller's cancellation token so /api/stop can // interrupt a long crack scan (the old scan-time crack honored it too). @@ -199,6 +227,19 @@ pub(crate) fn resolve_dvd_title_key( } CrackOutcome::Unencrypted => {} } + } else if matches!(keys, crate::decrypt::DecryptKeys::None) + && format == crate::disc::ContentFormat::MpegPs + { + // The skip is the interesting event, so it must not be silent: an + // MPEG-PS title with no key that does NOT get cracked is precisely the + // shape of the catastrophic bug (scrambled passthrough), so the log + // records WHICH disc format bought the skip. On an HD-DVD this line is + // the proof the 50_000-sector scan was avoided on purpose. + tracing::debug!( + target: "mux", + disc_format = ?disc_format, + "css crack skipped: disc format cannot carry CSS" + ); } Ok(()) } @@ -1455,6 +1496,7 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, + crate::disc::DiscFormat::Dvd, false, None, ) @@ -1485,6 +1527,7 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, + crate::disc::DiscFormat::Dvd, false, None, ) @@ -1521,6 +1564,7 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, + crate::disc::DiscFormat::Dvd, true, // raw None, ) @@ -1557,6 +1601,7 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, + crate::disc::DiscFormat::Dvd, false, None, ) @@ -1571,6 +1616,91 @@ mod tests { ); } + /// HD-DVD: `.evo` is MPEG-PS exactly like DVD `.vob`, so the CONTAINER + /// cannot tell the two apart — the disc-format axis must. HD-DVD is an AACS + /// family and carries no CSS at all, yet the old container-only gate sent + /// every HD-DVD title into a 50_000-sector crack scan and, when the scan + /// came back `ScrambledUncracked`, refused a good disc with `CssKeyMissing` + /// (E7023) — what a real CI run produced on the HD-DVD fixture. + /// + /// Catches the mutation of dropping `disc_format.may_have_css()` from the + /// gate (or listing HD-DVD as CSS-capable): the source here is `lock_all`, + /// so ANY read the crack performs drives `ScrambledUncracked` → the call + /// returns `Err`. The zero-reads assertion is the stronger claim: the scan + /// must not merely survive, it must never start. + #[test] + fn resolve_dvd_title_key_hddvd_never_enters_css_crack() { + let mut src = MockSource::new(0x00); + src.lock_all = true; // would hard-fail E7023 IF the crack ran + let extents = [Extent { + start_lba: 0, + sector_count: 4, + }]; + let mut keys = crate::decrypt::DecryptKeys::None; + resolve_dvd_title_key( + &mut src, + &extents, + &mut keys, + 4, + crate::disc::ContentFormat::MpegPs, + crate::disc::DiscFormat::HdDvd, + false, + None, + ) + .expect("an HD-DVD carries no CSS — it must never be refused for a missing CSS key"); + assert!( + matches!(keys, crate::decrypt::DecryptKeys::None), + "no CSS key may be installed on an AACS-family disc" + ); + assert!( + src.reads.borrow().is_empty(), + "the crack scan must not read a single sector on an HD-DVD" + ); + } + + /// The safety valve, and the reason the gate is a NEGATIVE test rather than + /// `disc_format == DiscFormat::Dvd`: a caller that cannot name the disc + /// (`DiscFormat::Unknown` — e.g. a bare reader with no scan behind it) must + /// STILL reach the crack. The two failure directions are asymmetric — a + /// needless scan is recoverable, while skipping the crack on a real DVD + /// muxes ciphertext as plaintext at exit 0. + /// + /// Catches the mutation of "simplifying" `may_have_css()` into a positive + /// `== DiscFormat::Dvd` allow-list, which would silently strand every + /// unknown-format DVD in scrambled passthrough: the crackable sector here + /// stops being cracked and `keys` stays `None`. + #[test] + fn resolve_dvd_title_key_unknown_disc_format_still_cracks() { + let title_key = [0x42, 0x13, 0x37, 0xBE, 0xEF]; + let seed = [0x11, 0x22, 0x33, 0x44, 0x55]; + let crackable = crackable_sector(&title_key, &seed, 8); + let mut src = MockSource::new(0x00); + src.crackable = Some((1003, crackable)); + let extents = [Extent { + start_lba: 1000, + sector_count: 50, + }]; + let mut keys = crate::decrypt::DecryptKeys::None; + resolve_dvd_title_key( + &mut src, + &extents, + &mut keys, + 4, + crate::disc::ContentFormat::MpegPs, + crate::disc::DiscFormat::Unknown, + false, + None, + ) + .expect("an unknown disc format must still resolve a crackable CSS title"); + match keys { + crate::decrypt::DecryptKeys::Css { title_key: got } => assert_eq!( + got, title_key, + "an unknown-format MPEG-PS title must be cracked, not passed through" + ), + _ => panic!("expected Css key: Unknown must default to CSS-capable, never skip"), + } + } + /// Clear DVD: a `None`-keyed MPEG-PS title with no scrambled sector stays /// `None` (a mux no-op) and returns `Ok` — genuinely-unencrypted DVDs pass. #[test] @@ -1587,6 +1717,7 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, + crate::disc::DiscFormat::Dvd, false, None, ) @@ -1618,6 +1749,7 @@ mod tests { &mut keys, 4, crate::disc::ContentFormat::MpegPs, + crate::disc::DiscFormat::Dvd, false, Some(&halt), ) diff --git a/src/disc/mod.rs b/src/disc/mod.rs index b57e70b..3888a4e 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -112,6 +112,47 @@ pub enum DiscFormat { Unknown, } +impl DiscFormat { + /// Can a disc of this format carry CSS (DVD Content Scramble System)? + /// + /// This is the axis the CSS crack MUST be gated on. It is NOT + /// [`ContentFormat`]: `ContentFormat::MpegPs` is the CONTAINER (MPEG + /// program stream) and the tree dispatch in [`Disc::scan_with`] assigns it + /// to the HD-DVD (`/HVDVD_TS`, `.evo`) arm exactly as it does to the DVD + /// (`/VIDEO_TS`, `.vob`) arm. HD-DVD is an AACS family and carries no CSS + /// whatsoever, so gating on the container sent every HD-DVD title into a + /// 50_000-sector CSS crack scan it could never satisfy — and when that scan + /// reported `ScrambledUncracked`, refused a perfectly good HD-DVD with + /// `Error::CssKeyMissing` (E7023). That is what a real CI run produced on + /// the HD-DVD fixture. `Disc::scan_image`'s eager image crack had already + /// learned this lesson and gates on `DiscFormat::Dvd`; the mux-side gate in + /// [`crate::css::resolve_dvd_title_key`] had not. + /// + /// The test is deliberately NEGATIVE — "everything except the families + /// proven CSS-free" — rather than a positive `== DiscFormat::Dvd`, because + /// the two failure directions are wildly asymmetric: + /// + /// * running CSS on a disc that has none costs a wasted scan (and, at + /// worst, a false refusal): loud, visible, recoverable; + /// * NOT running CSS on a real DVD makes the mux pass SCRAMBLED bytes + /// through as plaintext and exit 0 with garbage — a failure that looks + /// like success. That one already shipped once (~9 MB of ciphertext + /// inside a main-movie m2ts at rc=0). + /// + /// So the safe default is "attempt the crack". [`DiscFormat::Unknown`] — + /// the value a caller that never scanned the disc supplies — therefore + /// answers `true`, and any variant added to this enum in future answers + /// `true` until someone deliberately proves it CSS-free and adds it to the + /// exclusion list. A positive `matches!` list would default the other way, + /// i.e. toward the catastrophic direction. + pub fn may_have_css(self) -> bool { + !matches!( + self, + DiscFormat::HdDvd | DiscFormat::BluRay | DiscFormat::Uhd | DiscFormat::Fmts + ) + } +} + /// Disc playback region. #[derive(Debug, Clone, PartialEq)] pub enum DiscRegion { diff --git a/src/mux/disc.rs b/src/mux/disc.rs index fa2cee2..0206216 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -208,12 +208,25 @@ impl DiscStream { /// Works with physical drives and ISO files — both implement SectorSource. /// The caller opens the source, scans for titles/keys, and passes them in. /// The stream handles demuxing, decryption, and codec parsing internally. + /// + /// `content_format` is the CONTAINER (TS vs PS demuxer). `disc_format` is + /// the DISC FAMILY, and it exists as its own parameter because the two are + /// not interchangeable: DVD and HD-DVD are both `ContentFormat::MpegPs`, + /// yet only DVD can carry CSS. It gates the per-title CSS crack below. A + /// caller that genuinely does not know the disc passes + /// [`crate::disc::DiscFormat::Unknown`], which still attempts the crack — + /// the safe direction (see [`crate::disc::DiscFormat::may_have_css`]). + // Eight params is inherent to a constructor that takes the source, the + // title, the keys, both format axes (container and disc family) and the + // read-mode flags; grouping them would only relocate the same fields. + #[allow(clippy::too_many_arguments)] pub fn new( mut reader: Box, title: DiscTitle, mut decrypt_keys: crate::decrypt::DecryptKeys, batch_sectors: u16, content_format: crate::disc::ContentFormat, + disc_format: crate::disc::DiscFormat, raw: bool, halt: Option, ) -> std::io::Result { @@ -223,17 +236,20 @@ impl DiscStream { // Resolve this title's CSS key from the reader if the caller supplied // none — the SAME shared step the file-backed mux highway // (`build_iso_pipeline`) uses, so single-pass and multi-pass descramble a - // DVD identically. No-op for AACS / already-keyed / genuinely-clear input - // or `raw`; a scrambled-but-uncrackable DVD is a hard `CssKeyMissing`. - // `halt` is passed here (not deferred to `with_halt`) so a Stop during the - // crack scan is honored — the scan runs at construction, before the caller - // can attach a token. + // DVD identically. No-op for a disc format that cannot carry CSS (HD-DVD + // and the BD families — `disc_format`, NOT the MPEG-PS container, which + // DVD and HD-DVD share), for AACS / already-keyed / genuinely-clear + // input, and for `raw`; a scrambled-but-uncrackable DVD is a hard + // `CssKeyMissing`. `halt` is passed here (not deferred to `with_halt`) + // so a Stop during the crack scan is honored — the scan runs at + // construction, before the caller can attach a token. crate::css::resolve_dvd_title_key( &mut *reader, &extents, &mut decrypt_keys, batch_sectors, content_format, + disc_format, raw, halt.as_ref(), )?; @@ -1202,6 +1218,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, // request 8 sectors (16384 B); the source delivers 1 (2048 B) ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -1292,6 +1309,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -1328,6 +1346,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -1440,6 +1459,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -1483,6 +1503,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, crate::disc::ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -1518,6 +1539,7 @@ mod tests { aacs, 8, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -1680,6 +1702,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -1796,6 +1819,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -1859,6 +1883,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -1979,6 +2004,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -2038,6 +2064,7 @@ mod tests { keys, 8, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -2137,6 +2164,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -2181,6 +2209,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, crate::disc::ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) @@ -2238,6 +2267,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::MpegPs, + crate::disc::DiscFormat::Dvd, false, None, ); @@ -2247,6 +2277,36 @@ mod tests { ); } + /// The HD-DVD counterpart of the test above, pinned at the SAME boundary so + /// the disc-format axis is proven to reach the shared CSS step through this + /// constructor and not just inside `css::resolve_dvd_title_key`. + /// + /// Byte-for-byte identical input to `disc_stream_new_dvd_none_scrambled_hard_fails` + /// — same `LockedReader`, same MPEG-PS title, same `None` keys — with only + /// the disc format changed. The DVD case must still be refused (E7023) and + /// the HD-DVD case must construct: an HD-DVD is AACS and has no CSS, so + /// there is no CSS key for it to be missing. Catches the mutation of + /// dropping `disc_format` from `DiscStream::new`'s plumbing (or hardcoding + /// a CSS-capable value there), which is exactly the shape of the shipped + /// defect: E7023 on a perfectly good HD-DVD. + #[test] + fn disc_stream_new_hddvd_none_scrambled_does_not_hard_fail() { + let res = DiscStream::new( + Box::new(LockedReader), + mpegps_title(8), + crate::decrypt::DecryptKeys::None, + 8, + ContentFormat::MpegPs, + crate::disc::DiscFormat::HdDvd, + false, + None, + ); + assert!( + res.is_ok(), + "an HD-DVD must never be refused for a missing CSS key — it carries no CSS" + ); + } + /// `raw` must bypass the CSS crack at the DiscStream boundary too: the same /// scrambled-uncrackable input that hard-fails above must CONSTRUCT in raw /// mode (ciphertext passthrough), never hard-fail. @@ -2258,6 +2318,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::MpegPs, + crate::disc::DiscFormat::Dvd, true, // raw None, ); @@ -2471,6 +2532,7 @@ mod tests { crate::decrypt::DecryptKeys::None, 8, ContentFormat::MpegPs, + crate::disc::DiscFormat::Dvd, false, None, ) @@ -2592,6 +2654,7 @@ mod tests { }, 3, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, ) diff --git a/src/mux/driver.rs b/src/mux/driver.rs index e6d407d..b1951e4 100644 --- a/src/mux/driver.rs +++ b/src/mux/driver.rs @@ -129,6 +129,15 @@ pub enum MuxInput<'a> { title: DiscTitle, /// Container format of the title (TS vs PS demuxer selection). format: crate::disc::ContentFormat, + /// The scanned disc's FAMILY (`disc.format`) — a different axis from + /// `format`, which is only the container. DVD and HD-DVD are both + /// `ContentFormat::MpegPs`, yet only DVD can carry CSS, so this is what + /// gates the per-title CSS crack in [`build_iso_pipeline`]. Pass + /// [`crate::disc::DiscFormat::Unknown`] only when the disc was genuinely + /// never scanned: that value still runs the crack, which is the safe + /// direction (skipping it on a real DVD would mux ciphertext as + /// plaintext at exit 0). + disc_format: crate::disc::DiscFormat, /// Decryption keys for the title (`DecryptKeys::None` for raw/clear). keys: DecryptKeys, /// Optional read-time key fetch closure (banked by `resolve_keys`). @@ -151,6 +160,12 @@ pub enum MuxInput<'a> { title: DiscTitle, /// Container format (TS vs PS demux selection). format: crate::disc::ContentFormat, + /// The scanned disc's FAMILY (`disc.format`), the CSS-eligibility axis + /// — see [`MuxInput::Iso::disc_format`]. Without it the inline + /// `DiscStream` cannot tell an HD-DVD `.evo` from a DVD `.vob` (both + /// are `ContentFormat::MpegPs`) and would run a CSS crack that an + /// AACS-family disc can never satisfy. + disc_format: crate::disc::DiscFormat, /// Decryption keys the consumer already banked (`DecryptKeys::None` for /// raw/clear). The driver consumes them as-is — never re-resolves. keys: DecryptKeys, @@ -351,6 +366,7 @@ pub fn mux_stream( path, title, format, + disc_format, keys, key_fetch, } => { @@ -382,6 +398,7 @@ pub fn mux_stream( keys, opts.batch_sectors, format, + disc_format, opts.raw, Some(halt.clone()), Some(reader_event_fn(events.clone())), @@ -396,7 +413,7 @@ pub fn mux_stream( // Pull everything we need out of the disc as owned values so the // immutable disc borrow is released before the mutable // `take_reader` below. - let (mut title, format, mut keys, playlist, source) = { + let (mut title, format, disc_format, mut keys, playlist, source) = { let disc = session.disc().ok_or_else(|| Error::DeviceNotReady { path: session.device_path().to_string(), })?; @@ -424,9 +441,15 @@ pub fn mux_stream( }; // DVD CSS is per-VTS: resolve the per-title key via the pipeline // (see `session_mux_keys`), never the whole-disc `decrypt_keys()`. + // `disc.content_format` is the container; `disc.format` is + // the disc FAMILY. Both are carried out of the borrow: the + // first picks the demuxer, the second decides whether a CSS + // crack is even meaningful (an HD-DVD is MPEG-PS too, and + // has no CSS). ( title, disc.content_format, + disc.format, session_mux_keys(disc), playlist, source, @@ -468,6 +491,7 @@ pub fn mux_stream( keys, opts.batch_sectors, format, + disc_format, opts.raw, Some(halt.clone()), )?; @@ -488,6 +512,7 @@ pub fn mux_stream( mut reader, title, format, + disc_format, mut keys, key_map, } => { @@ -546,6 +571,7 @@ pub fn mux_stream( keys, opts.batch_sectors, format, + disc_format, opts.raw, Some(halt.clone()), )?; @@ -1631,6 +1657,7 @@ mod tests { path: &iso_path, title, format: crate::disc::ContentFormat::BdTs, + disc_format: crate::disc::DiscFormat::BluRay, keys: DecryptKeys::None, key_fetch: None, }, @@ -1742,6 +1769,7 @@ mod tests { reader, title, format: crate::disc::ContentFormat::BdTs, + disc_format: crate::disc::DiscFormat::BluRay, keys: DecryptKeys::None, key_map: Some(map), }, @@ -1864,6 +1892,7 @@ mod tests { reader, title, format: crate::disc::ContentFormat::BdTs, + disc_format: crate::disc::DiscFormat::BluRay, keys, key_map: None, // plain AACS disc: the driver must resolve the base map }, diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs index 71c7679..33a4a81 100644 --- a/src/mux/resolve.rs +++ b/src/mux/resolve.rs @@ -613,6 +613,12 @@ where } let title = disc.titles[idx].clone(); let format = disc.content_format; + // The CSS-eligibility axis handed to the pipeline below. `content_format` + // above is only the container and cannot carry this decision: HD-DVD `.evo` + // is MPEG-PS exactly like DVD `.vob`, and gating the crack on the container + // is what sent every HD-DVD through a CSS scan it could never satisfy. + // Same value `is_dvd` was derived from further up. + let disc_format = disc.format; // ISO file: 8192-sector batch (16 MiB at 2048 B/sector) — // sequential read from fast storage, no bad sectors. Empirically // optimal; bumping to 16384 sectors (32 MiB) regressed (more cache @@ -644,6 +650,7 @@ where effective_keys, ISO_MUX_BATCH_SECTORS, format, + disc_format, opts.raw, None, None, @@ -2109,6 +2116,11 @@ pub(crate) fn resolve_mux_key_map_cached( /// - `batch_sectors`: read batch size in logical (2048-byte) sectors — a /// throughput/latency tuning knob, not a correctness parameter. /// - `format`: container format (`BdTs` → TS demuxer, `MpegPs` → PS demuxer). +/// - `disc_format`: the disc FAMILY, a separate axis from `format` — DVD and +/// HD-DVD are both `MpegPs`, but only DVD can carry CSS. Gates the per-title +/// CSS crack below. A caller with no scanned disc passes +/// [`crate::disc::DiscFormat::Unknown`], which still cracks (the safe +/// direction — see [`crate::disc::DiscFormat::may_have_css`]). /// - `raw`: ciphertext passthrough. When `true`, the per-title CSS crack /// (`resolve_dvd_title_key`) is skipped entirely — no key is resolved and a /// scrambled title is neither descrambled nor hard-failed. @@ -2128,6 +2140,7 @@ pub fn build_iso_pipeline( mut keys: crate::decrypt::DecryptKeys, batch_sectors: u16, format: ContentFormat, + disc_format: crate::disc::DiscFormat, raw: bool, halt: Option, event_fn: Option, @@ -2135,17 +2148,20 @@ pub fn build_iso_pipeline( ) -> io::Result { let extents = title.extents.clone(); // CSS (DVD) key resolution — the shared per-title step (also used by the - // live-drive single-pass `DiscStream`). A `None`/MPEG-PS title cracks its own - // key from the reader in playback order; AACS `.evo` (also MPEG-PS) arrives as - // `Aacs` and is untouched; a clear DVD stays `None`; `raw` skips it entirely. - // Without this a detection-miss CSS DVD would mux scrambled sectors as corrupt - // video. `halt` lets /api/stop interrupt the crack scan. + // live-drive single-pass `DiscStream`). A `None`/MPEG-PS title on a + // CSS-capable DISC FORMAT cracks its own key from the reader in playback + // order; an HD-DVD (also MPEG-PS, but AACS — no CSS exists to find) is + // skipped on the `disc_format` axis; AACS keys are untouched; a clear DVD + // stays `None`; `raw` skips it entirely. Without this a detection-miss CSS + // DVD would mux scrambled sectors as corrupt video. `halt` lets /api/stop + // interrupt the crack scan. crate::css::resolve_dvd_title_key( &mut reader, &extents, &mut keys, batch_sectors, format, + disc_format, raw, halt.as_ref(), )?; @@ -2991,6 +3007,7 @@ mod tests { DecryptKeys::None, 8192, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, None, @@ -3033,6 +3050,7 @@ mod tests { DecryptKeys::None, 8192, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, None, @@ -3130,6 +3148,7 @@ mod tests { DecryptKeys::None, 8192, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, None, @@ -3169,6 +3188,7 @@ mod tests { DecryptKeys::None, 0, ContentFormat::BdTs, + crate::disc::DiscFormat::BluRay, false, None, None, @@ -3211,6 +3231,7 @@ mod tests { DecryptKeys::None, 8192, ContentFormat::MpegPs, + crate::disc::DiscFormat::Dvd, false, None, None, diff --git a/tests/fvi_pipeline.rs b/tests/fvi_pipeline.rs index 8b5baa0..738553b 100644 --- a/tests/fvi_pipeline.rs +++ b/tests/fvi_pipeline.rs @@ -175,6 +175,10 @@ fn run_to_fvi(image: Vec, title: DiscTitle, path: &std::path::Path) { DecryptKeys::None, 3, // 3-sector (one AACS unit) batches → one source stamp per GOP region ContentFormat::MpegPs, + // A DVD-family fixture: the disc-format axis must keep the CSS crack + // reachable here exactly as it is in production (the image is clear, so + // the crack finds nothing and the mux proceeds). + libfreemkv::DiscFormat::Dvd, false, None, None,