CSS: key the DVD crack on the disc, not on the container
`resolve_dvd_title_key` decided whether to run the DVD CSS crack by asking what CONTAINER it was looking at — `ContentFormat::MpegPs`. MPEG program stream is what DVD and HD-DVD have in common, so every HD-DVD title was run through a crack scan for a copy-protection scheme HD-DVD does not use and cannot carry: AACS is its family, and CSS appears nowhere on the disc. Both outcomes of that scan were wrong. The cheap one wasted up to 50,000 sector reads per title. The expensive one returned `CssKeyMissing` — E7023 — refusing a perfectly good HD-DVD with a CSS error, which is what a real CI run produced on the HD-DVD fixture. Reading E7023 there sends whoever triages it looking for a missing DVD key on a disc that never had one, which is how a routing bug spends a day disguised as a key problem. The right axis was already in the codebase and already used: `mux/resolve` asks `disc.format == DiscFormat::Dvd`. This threads the disc format down to the decision and adds `DiscFormat::may_have_css` to name the question. The asymmetry decides the default, so it is worth stating. Running CSS on an HD-DVD costs a wasted scan or a false refusal — visible, recoverable, annoying. NOT running it on a real DVD muxes scrambled sectors as plaintext and exits 0, which is the failure-that-looks-like-success class this project has shipped once already. So `may_have_css` is false ONLY for the families proven CSS-free, and `DiscFormat::Unknown` — a bare reader with no scan behind it — still cracks. An `== Dvd` allow-list would have read as tighter while silently stranding every caller that cannot name its disc. Both directions are pinned: an HD-DVD title must never enter the crack, and an unknown-format title must still enter it. Removing the disc-format clause fails the first and leaves the second and every DVD test green.
This commit is contained in:
+133
-1
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+68
-5
@@ -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<dyn SectorSource>,
|
||||
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<Halt>,
|
||||
) -> std::io::Result<Self> {
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
+30
-1
@@ -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
|
||||
},
|
||||
|
||||
+26
-5
@@ -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<S: SectorSource + Send + 'static>(
|
||||
mut keys: crate::decrypt::DecryptKeys,
|
||||
batch_sectors: u16,
|
||||
format: ContentFormat,
|
||||
disc_format: crate::disc::DiscFormat,
|
||||
raw: bool,
|
||||
halt: Option<crate::halt::Halt>,
|
||||
event_fn: Option<crate::sector::prefetched::EventFn>,
|
||||
@@ -2135,17 +2148,20 @@ pub fn build_iso_pipeline<S: SectorSource + Send + 'static>(
|
||||
) -> io::Result<PipelinedPesStream> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user