mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
493 lines
26 KiB
Markdown
493 lines
26 KiB
Markdown
# src/disc/mod.rs — internal notes
|
|
|
|
Overflow rationale for internal (non-`pub`) items in `src/disc/mod.rs`,
|
|
relocated here by the comment-guard so the in-file comment can stay short.
|
|
Each section is pointed to by a one-line `// See docs/disc-mod.md — <topic>`
|
|
comment at the corresponding call site.
|
|
|
|
## aacs_dir_present
|
|
|
|
THE ONE definition of "is this disc structurally AACS-encrypted". Both
|
|
`Disc::identify` (fast path, filesystem only) and `Disc::scan_with` (full
|
|
scan) need this, and both used to spell out the same two `find_dir` calls by
|
|
hand. They agreed today; nothing made them agree tomorrow — adding a third
|
|
AACS location, or excluding an empty placeholder directory, to one copy and
|
|
not the other would silently desync the fast identify from the full scan (the
|
|
same disc reported encrypted by one and clear by the other).
|
|
|
|
This is STRUCTURAL, not cryptographic: it says the tree looks like an
|
|
encrypted disc, not that any sector actually is. A folder copied verbatim
|
|
from a decrypted disc keeps its `AACS/` and answers true here (see
|
|
`session::scan_dir`, which corrects for exactly that).
|
|
|
|
## correct_truehd_channels
|
|
|
|
Corrects a title's TrueHD audio-stream metadata by probing the first
|
|
decrypted access units — channel count, real sample rate, and Atmos detection
|
|
in a single major-sync read. The MPLS descriptors declare the BASE layout
|
|
(often 5.1, or a container-guessed rate) even for a 7.1/Atmos TrueHD track;
|
|
the truth is in the MLP major sync. `reader` must yield DECRYPTED sectors
|
|
(the m2ts is AACS-encrypted, so this can only run at mux time, not scan).
|
|
Reads a bounded window of the title's first extent.
|
|
|
|
Corrections, each individually guarded so a malformed field never writes a
|
|
wrong header:
|
|
- **Channels**: from the presentation channel masks (as before).
|
|
- **Sample rate**: from the whitelisted rate nibble; left untouched on an
|
|
unknown rate or no major sync.
|
|
- **Atmos**: when a 4th substream is detected AND the stream still carries
|
|
the basic descriptor label, the label is promoted to the Atmos form;
|
|
richer editorial labels (e.g. an existing "Dolby Atmos") are left intact.
|
|
|
|
## Resolution::pixels
|
|
|
|
`Unknown` deliberately does NOT fabricate a plausible 1920x1080. This is the
|
|
same trap already closed on `AudioChannels::count` (which used to return 6)
|
|
and `SampleRate::hz` (48000.0): a plausible wrong answer is indistinguishable
|
|
from a real one at every call site, so a caller can forget to check the
|
|
variant first — the `json://` sink had already walked into exactly that,
|
|
reporting confident dimensions for a stream whose neighbouring `resolution`
|
|
field said "unknown".
|
|
|
|
It also does not return `(0, 0)`: that shape looks like a usable pair, so the
|
|
MP4 sink stored it and serialised a 0x0 video track — a structurally complete
|
|
file no player can render, written with no error. Before that it returned a
|
|
fabricated 1920x1080, which was wrong but at least playable, so the sink had
|
|
never needed a guard and the absence of one was invisible. A doc comment
|
|
listing which callers are safe cannot hold this: one did not check, and a
|
|
third kept its own duplicate `Unknown` test long after the accessor took the
|
|
job over. `Option` forces every caller to make its OWN decision: Matroska
|
|
omits the optional PixelWidth/PixelHeight elements, the metadata sinks report
|
|
the dimensions as absent, and MP4 must refuse per ISO/IEC 14496-12 (width and
|
|
height are mandatory in both `tkhd` 8.3.2 and VisualSampleEntry 12.1.3).
|
|
|
|
## has_probable_video
|
|
|
|
The exposed `streams` come from the title's FIRST PlayItem only, so a
|
|
feature whose first PlayItem is a non-video bumper reports
|
|
`has_video() == false` and would be wrongly disqualified (audit R6). This
|
|
predicate is deliberately MORE permissive than `has_video` — it only ever
|
|
ADMITS a title the stricter gate would reject — so it cannot reopen the
|
|
decoy hole: a size-inflated streamless decoy is demoted STRUCTURALLY by
|
|
composite detection in `Disc::rank_titles`, not by this gate, while it still
|
|
rejects tiny streamless menu/metadata playlists. The size fallback requires
|
|
at least one clip, so a clip-less pure-metadata decoy (which cannot be a
|
|
real bumper-lead feature — that has the feature clip) is still rejected.
|
|
|
|
## image_crack_extents
|
|
|
|
The extents an image-time CSS crack scans, in the crate's CANONICAL order:
|
|
the main feature's own extents, in natural playback order. Both halves are
|
|
deferred to logic the crate already owns rather than re-derived here:
|
|
|
|
- WHICH title — `scan_with` has already sorted `titles` with
|
|
`Self::canonical_title_order`, so the canonical main feature is the first
|
|
title that actually has extents. No local pick.
|
|
- WHAT ORDER — playback order, i.e. the extent vector untouched, exactly as
|
|
`Self::decrypt_keys_for_title` hands `&title.extents` to
|
|
`css::crack_key_outcome`.
|
|
|
|
**Why this function exists (do not let the copy grow back):** `Disc::scan_image`
|
|
used to re-implement both halves inline — it picked the title with the
|
|
largest total sector count, then re-sorted that title's extents
|
|
LARGEST-CELL-FIRST before handing them to `crack_key_outcome`. Both had
|
|
drifted from the canonical rules:
|
|
|
|
- Largest-cell-first is the 1.5.1 garbage bug. A CSS DVD's biggest cell opens
|
|
with a long CLEAR run, and the crack's 50_000-sector budget is shared
|
|
across the whole extent list, so starting there can exhaust the budget
|
|
without ever reaching a scrambled sector — the crack reports `Unencrypted`
|
|
and the mux emits scrambled MPEG as plaintext. Playback order reaches the
|
|
scrambled feature body after only the small clear front matter that
|
|
precedes it on a real disc (CSS: the title key is recovered from the
|
|
scrambled data itself, so the scan must actually MEET scrambled data).
|
|
- The sector-count pick ignores `canonical_title_order`'s capacity gate, so
|
|
it selects the oversize "play-all" composite (whose declared cells
|
|
double-count data shared with other playlists) instead of the real feature
|
|
— i.e. a DIFFERENT title, and on a multi-VTS disc a different VTS, whose
|
|
CSS title key does not descramble the feature at all.
|
|
|
|
The result: the same disc cracked as an ISO could disagree with the same
|
|
disc cracked from the drive, which is precisely what the duplicate was free
|
|
to do. Keep the derivation here, shared, so it cannot recur.
|
|
|
|
## read_aacs_inputs_from_reader
|
|
|
|
Reads a disc's AACS key-input files from a sector source: returns
|
|
`(Unit_Key_RO.inf, MKB)` raw bytes. Shared body for `Disc::read_aacs_inputs`
|
|
(ISO) and `Disc::read_aacs_inputs_from_drive` (live drive). Prefers MKB_RO,
|
|
falls back to MKB_RW, then TRIMS to the real record length. Both files are
|
|
allocated to a fixed ~128 MiB and zero-padded, so reading either ships up to
|
|
~124 MiB of nothing — trim to the record stream so callers send/store a few
|
|
MB, not 128 MiB.
|
|
|
|
## read_aacs_version
|
|
|
|
AACS major version (`crate::aacs::mkb::AACS_MAJOR_BD` /
|
|
`crate::aacs::mkb::AACS_MAJOR_UHD`) from the content certificate. Drives the
|
|
`Unit_Key_RO.inf` parse stride (48-byte V10 vs 64-byte V20/V21), so the
|
|
out-of-band key-fetch path parses `enc_title_keys` at the right stride (a
|
|
server VUK then derives the correct unit keys).
|
|
|
|
When no content certificate is readable/parseable, defaults to **UHD (V20,
|
|
64-byte stride)** — the conservative choice the pre-1.2.0 fetch path
|
|
hardcoded — and logs it: a wrong stride here folds a server VUK against
|
|
mis-strided title keys (silent wrong unit keys), so a missing cert must not
|
|
quietly pick the V10 stride for a UHD disc.
|
|
|
|
## read_mkb_content
|
|
|
|
Reads the AACS MKB's real record stream — NOT its zero padding. `MKB_RO.inf`
|
|
/ `MKB_RW.inf` are allocated to a fixed ~128 MiB and zero-padded; the actual
|
|
record stream is a few MiB. Reads a bounded prefix, finds the record-stream
|
|
length via `crate::aacs::mkb::mkb_content_len` and returns exactly that,
|
|
growing the prefix if the records run past it. This avoids reading 100+ MiB
|
|
of padding on every scan AND avoids the `read_file` `MAX_FILE_BYTES` cap that
|
|
(since 0.31.0) rejected the padded 128 MiB MKB outright — which made
|
|
`read_aacs_inputs` fail and autorip report "could not read this disc's key
|
|
files" without ever contacting the keyserver.
|
|
|
|
## CANONICAL_TITLE_ORDER_KEYS
|
|
|
|
**Why not just sort by duration descending?** Branching UHDs (and some BD
|
|
authoring) ship a "play-all" virtual playlist that references the same
|
|
source clips multiple times for seamless alternate-angle / alternate-ending
|
|
playback. Those playlists report an inflated `duration_secs` (often 4+
|
|
hours) and an inflated `size_bytes` greater than the disc's physical
|
|
capacity. Example seen in the wild — *The Amateur (2025)* UHD, 58.5 GB
|
|
BD-100 disc:
|
|
|
|
| Title | Playlist | Duration | Size | Clips |
|
|
|-------|------------|----------|---------|-------|
|
|
| 1 | 00020.mpls | 4h 13m | 92.4 GB | 253 |
|
|
| 2 | 00800.mpls | 2h 02m | 57.2 GB | 1 |
|
|
|
|
Title 1's 92.4 GB cannot fit on a 58.5 GB disc unless the same clip data is
|
|
referenced multiple times — proof it's a virtual composite. A duration-only
|
|
sort would put it at `titles[0]`, so `freemkv -t 1`, `disc.titles.first()`,
|
|
and autorip's main-feature picker all grab the 4-hour composite instead of
|
|
the 2-hour movie that actually matches TMDB.
|
|
|
|
**Effect on non-branching discs:** unchanged — the main movie is already the
|
|
longest 1-clip title. **Effect on branching UHDs:** the virtual play-all
|
|
playlist is pushed to the back, the actual movie surfaces at index 0.
|
|
|
|
Defined immediately beside the comparator, so a diagnostic can NAME the
|
|
ordering instead of restating it. The `freemkv::diag` main-feature decision
|
|
row used to carry its own hand-written copy of this list, and it drifted: it
|
|
still advertised a `fewest-clips` key long after the comparator replaced
|
|
clip-count with largest-physical-size, so the `--log-level 3` bug-report log
|
|
explained freemkv's top-level pick with a rule freemkv does not apply. Any
|
|
change to the keys must change this list in the same edit.
|
|
|
|
## probe_forced_subtitles_for_bdts_titles
|
|
|
|
Pulled out of `scan_with` as its own callable predicate: the decision is
|
|
otherwise reachable only by driving the full scan pipeline (tree dispatch ->
|
|
title parsing -> this loop), so a test could not pin the gate without also
|
|
authoring an entire synthetic disc image. Running it against an HD-DVD/DVD
|
|
(`MpegPs`) title's extents would demux the wrong container, since only
|
|
`BdTs` titles carry PES-wrapped PGS the shared classifier
|
|
(`pgs_forced_probe`) understands.
|
|
|
|
## MAIN_FEATURE_ORDER_KEYS
|
|
|
|
- `nav-feature`: the title the disc's own HDMV navigation plays as the
|
|
feature, resolved by running First-Play in the `crate::bdnav` VM the way a
|
|
real player would. This is the authoritative, size-independent pick; it is
|
|
video-gated (a mis-resolve cannot select a streamless playlist) and simply
|
|
absent when the VM abstains (BD-J discs, or non-convergence), in which
|
|
case the lower keys decide.
|
|
- `authoring-feature`: a title the disc's own menu authoring designates as
|
|
the feature (has video, is not itself a composite, and is within a sane
|
|
fraction of the longest title's duration) outranks everything else — a
|
|
size-independent signal an obfuscation decoy cannot forge.
|
|
- `standalone`: a title that is a play-all / wrapper COMPOSITE — its clip
|
|
set properly contains another substantial video title's whole clip set (a
|
|
real UHD title's decoy playlist `00245` = `[bumper][00001 feature
|
|
clip][outro]` case) — is demoted below the standalone title it wraps. This
|
|
is the STRUCTURAL anti-decoy signal; it does not depend on stream counts,
|
|
physical size, or disc capacity, so it survives where those are spoofed or
|
|
unknown.
|
|
- `has-video`: a title with no plausible video content (see
|
|
`DiscTitle::has_probable_video`) is demoted below every title that has
|
|
some — a floor that drops streamless menu/metadata playlists.
|
|
|
|
## rank_titles
|
|
|
|
This catches both the wrapper decoy (`00245 ⊋ 00001`) and the oversize
|
|
play-all — independent of capacity, which the `fits-disc` gate needs and
|
|
which is `0`/unknown on drives that fail READ CAPACITY.
|
|
|
|
### Wrapper-shape gate (issue #45)
|
|
|
|
A proper-subset relation with `Q.size ≥ 0.5·P.size` is NOT by itself proof
|
|
that `P` is a wrapper. A genuine **seamless-branch feature** `P` (body clip
|
|
plus intro/credits/alternate-branch segments) is a *superset* of its
|
|
single-clip body decoy `Q`: `Q` runs almost as long as `P` (e.g. 1h50 vs
|
|
2h03, ~0.89 of the duration) yet is markedly smaller, because `P`'s branch
|
|
segments add bytes without adding much played runtime. The old size-only
|
|
gate mis-flagged such a `P` as a "play-all wrapper" and demoted the real
|
|
2h03 feature below the shorter 1h50 body — a SAFETY bug (wrong title ripped).
|
|
|
|
So the composite flag additionally requires `P` to *have* a wrapper shape,
|
|
via one of two capacity-independent, per-title (`duration_secs`, `size_bytes`)
|
|
signals on the subset `Q`:
|
|
|
|
- **concat / play-all:** `Q.duration < 0.85·P.duration` — `Q` is a much
|
|
shorter cut and `P` pads its runtime with other segments (the true
|
|
play-all runs ~the SUM of its parts, so each part is well under half).
|
|
- **bumper / outro wrapper:** `Q.size ≥ 0.90·P.size` — `Q` already holds
|
|
almost all of `P`'s bytes and `P` merely prepends/appends a short
|
|
non-feature lead/tail (the `00245 ⊋ 00001` decoy: `Q` is ~0.96 of both the
|
|
duration and the size, so the size limb keeps it demoted).
|
|
|
|
Otherwise — `Q` runs almost as long as `P` (≥0.85) yet is well under 0.90 of
|
|
its size — `P` is the fuller branch set and is kept. `0.85` sits above a real
|
|
concat's per-part ratio (~0.5) and below a seamless body's (~0.89); `0.90`
|
|
sits above a bumper wrapper's near-1.0 size share and below a branch body's.
|
|
|
|
**Known residual:** a wrapper that pads runtime only modestly AND withholds
|
|
most of its bytes from the body clip is ambiguous by shape alone. The corpus
|
|
title-selection gate is the backstop across the test corpus, and
|
|
higher-precedence nav/authoring signals override the composite key.
|
|
|
|
## aligned_unit_keys_validate
|
|
|
|
Conservative: a sample that is not AACS-scrambled proves nothing, and with
|
|
no scrambled sample at all there is nothing to disprove against, so it
|
|
returns `true` (accept). It returns `false` when ANY scrambled sample cannot
|
|
be restored to clear MPEG-TS by ANY unit key in the set. That covers two
|
|
distinct failure shapes: (1) a wholly wrong key (a keydb VK that does not
|
|
match this disc) — no sample decrypts; and (2) a *partially* applicable key
|
|
set on a multi-CPS-unit disc — the resolved keys cover CPS unit 0 but not
|
|
CPS unit 1. Accepting on the first sample that decrypts (the old behaviour)
|
|
would commit such a set, after which CPS-unit-1 sectors pass through as raw
|
|
encrypted bytes into the ISO/MKV with no error surfaced anywhere. Requiring
|
|
every scrambled sample to decrypt rejects the incomplete set so the caller
|
|
falls through to the next candidate. Reuses the ecosystem's single
|
|
`is_clean` content-clarity predicate and the full (bus + AACS) unit decrypt,
|
|
so it agrees with the actual mux decrypt.
|
|
|
|
## ensure_decryptable
|
|
|
|
The verdict, in order:
|
|
- `raw == true` → `Ok(())`. `--raw` intentionally skips decryption and needs
|
|
no key (the caller wants an encrypted image).
|
|
- `self.css_error.is_some()` → `Err(Error::CssNoDiscKey)`. The scan saw
|
|
scrambled CSS sectors but recovered no title key (`self.css` is `None` yet
|
|
the content IS encrypted). Treating `css.is_none()` as "unencrypted" would
|
|
mux scrambled MPEG as plaintext garbage. A DISC-LEVEL verdict
|
|
(`error::is_disc_level_no_key`) — the main feature's crack failed, so
|
|
every title fails the same way and the rip loop must stop rather than
|
|
skip each title in turn.
|
|
- AACS-encrypted (`self.aacs.is_some()`) with no usable key
|
|
(`decrypt_keys()` is `None`) → `Err(Error::NoDiscKey { .. })`, naming the
|
|
disc by hash.
|
|
- CSS-encrypted (`self.css.is_some()`) with no usable key →
|
|
`Err(Error::CssKeyMissing)`. (The disc-wide `decrypt_keys()` yields
|
|
`Css{..}` whenever `css.is_some()`, so this is defensive; the live
|
|
multi-VTS case is gated by `Self::ensure_decryptable_keys`.)
|
|
- otherwise → `Ok(())`. A genuinely unencrypted disc has `None` keys
|
|
legitimately, and a CSS disc whose keyless crack succeeded has a key.
|
|
|
|
## decrypt_keys_for_title
|
|
|
|
For a DVD the CSS title key MUST be recovered before descrambling: a
|
|
scrambled sector without a recoverable crib cannot self-crack, and CSS
|
|
leaves the pack/PES header clear, so a sector left un-descrambled would mux
|
|
as a structurally-valid but corrupt PES packet with no loss reported. Two
|
|
ways to get it:
|
|
|
|
- **Fast path** — the scan already cracked a key whose LBA span
|
|
(`crate::css::CssState::crack_span`) covers this title's VTS: reuse it. No
|
|
re-read, and on a live drive no second CSS bus-auth round-trip. CSS title
|
|
keys are per-VTS, so an overlapping span is the same key.
|
|
- **Crack** — when up-front detection missed (`self.css == None`) or the
|
|
title lives in a different VTS: crack the key from this title's OWN
|
|
extents in a SINGLE scan, in natural PLAYBACK ORDER (never
|
|
largest-cell-first, which starved the crack in a big cell's clear prefix —
|
|
the 1.5.1 bug). Playback order reaches the scrambled feature body after
|
|
only the small clear front matter (logo / rating card) that precedes it.
|
|
One scan = one CSS-locked early-bail, so a locked title is not
|
|
re-hammered per cell against a live drive (hard rule #2); its 50k-sector
|
|
budget is the same accepted bound the disc-wide scan uses.
|
|
|
|
Outcomes (`batch_sectors` sizes the crack's batched reads):
|
|
- `(Css{title_key}, false)` — descramble with the recovered/reused key.
|
|
- `(None, true)` — a genuinely-clear title needs no key; the gate passes it.
|
|
- `(None, false)` — scrambled but no key recoverable → hard failure via
|
|
`Self::ensure_title_decryptable`, never a silent garbage mux.
|
|
|
|
## inject_unit_keys
|
|
|
|
Injects pre-resolved AACS unit keys into a scanned disc — the deferred-mux /
|
|
resume path. The keys come from the mapfile's `# freemkv-uk:` header
|
|
(persisted at sweep time when the disc was keyed), so the mux decrypts
|
|
directly with NO key-service round-trip. Populates `self.aacs.unit_keys` so
|
|
`Self::decrypt_keys` returns them and marks the source `ExternalUk`.
|
|
|
|
If the scan built no AACS state (`self.aacs == None`) — which happens when
|
|
the keydb was absent at scan time (`scan_aacs_no_keydb` ->
|
|
`aacs_error = KeydbLoad`) — this synthesizes a minimal `ExternalUk` state
|
|
for an encrypted AACS disc. A Unit Key is the FINAL per-title decryption
|
|
key; the keydb is only needed to *derive* it, and that derivation already
|
|
happened at sweep (the UK is in the mapfile). So a UK alone is sufficient to
|
|
decrypt the on-disk ISO — AACS 2.0 bus decryption was applied by the drive
|
|
at read time, so `read_data_key` is unused for file-backed mux. Without
|
|
this, a keyed disc swept without a keydb would recover its UK yet still
|
|
report E8005 (no usable `decrypt_keys`) at remux. No-op for an unencrypted
|
|
or CSS (DVD) disc.
|
|
|
|
## scan_with_passes_the_halt_flag_to_the_bluray_enumerator
|
|
|
|
Every BD and DVD cancellation test in this crate calls
|
|
`scan_bluray_titles` / `scan_dvd_titles` DIRECTLY, so the three-line wiring
|
|
in `scan_with` that connects the operator's Stop to them was covered by
|
|
nothing at all: replacing `opts.halt.as_ref()` with `None` on either branch
|
|
left the whole suite green while a Stop during a BD or DVD scan silently did
|
|
nothing. Only the HD-DVD branch was pinned.
|
|
|
|
The fixture is deliberately the smallest disc that takes the BD branch — a
|
|
bare `/BDMV` with no PLAYLIST — because the point under test is the
|
|
ARGUMENT, not the enumerator's own (separately tested) halt polling. Nothing
|
|
between `scan_with`'s entry and the enumerator reads the flag (the disc is
|
|
unencrypted, so the AACS path is skipped), so a green here can only mean the
|
|
flag arrived. Mutation: `Self::scan_bluray_titles(reader, &udf_fs, None)?`
|
|
fails here.
|
|
|
|
## halted_reads_do_not_report_the_hddvd_scan_as_successful
|
|
|
|
A Stop on a LIVE DRIVE never touches `ScanOptions::halt`: `Drive` has its
|
|
own flag and `checked_exec` fails every SCSI command with `Error::Halted`
|
|
once it is set. The HD-DVD enumerator must not swallow that into a
|
|
successful scan. Measured before this was fixed: the scan returned `Ok`
|
|
with both titles present and ZERO streams on each — a cancelled scan
|
|
wearing the shape of a disc whose clips carry no video or audio. Downstream
|
|
that is a title list to cache, display and rip from.
|
|
|
|
## scan_with_capacity_bytes tests
|
|
|
|
`scan_with`'s `capacity_bytes = capacity as u64 * 2048` feeds
|
|
`canonical_title_order`'s "bigger than the whole disc = play-all composite"
|
|
threshold.
|
|
|
|
`..._not_addition`: chosen so `capacity * 2048` clears both titles (neither
|
|
is oversize, so the bigger one — OTHER — ranks first), but
|
|
`capacity + 2048` lands BETWEEN the two sizes: MAIN stays non-oversize while
|
|
OTHER flips to oversize and gets demoted, changing `titles[0]`. A `*` -> `+`
|
|
mutation is caught by this ranking flip, not merely a wrong numeric
|
|
threshold value.
|
|
|
|
`..._not_division`: same mechanism, tuned to catch a `*` -> `/` mutation
|
|
instead: `capacity * 2048` clears both titles, but `capacity / 2048`
|
|
collapses to a threshold far below MAIN's declared size while still leaving
|
|
MAIN under it, so only OTHER flips to oversize.
|
|
|
|
## read_mkb_content_grows_prefix_past_16mib_when_records_run_longer
|
|
|
|
This fixture's record stream is deliberately built so the first 16 records
|
|
land EXACTLY on the 16 MiB boundary (so the 16 MiB prefix alone parses as a
|
|
clean, complete record stream — the case a naive "n < buf.len() means done"
|
|
check would wrongly accept) and 4 more MiB of records follow, terminated by
|
|
the explicit `00 000000` end marker. Only a caller that actually grows past
|
|
16 MiB recovers the true 20 MiB content length.
|
|
|
|
## main_feature_order_demotes_wrapper_composite_decoy
|
|
|
|
The decoy `00245.mpls` is a WRAPPER COMPOSITE — `[00339 2s bumper][00001 the
|
|
whole feature clip][00336 outro]` — so its first-PlayItem STN reports v=0,
|
|
and it is the LONGEST and LARGEST title (8350s / 78.4 GB vs the feature's
|
|
8038s / 76.5 GB). Muxing its zero frames produced E6008 forever. It must be
|
|
demoted STRUCTURALLY (its clip set properly contains the feature's), not by
|
|
the fragile first-PlayItem stream count — this is what makes the fix robust
|
|
to the video-bearing-composite variant (see
|
|
`main_feature_order_demotes_video_composite`).
|
|
|
|
## key_source_failure_is_not_reported_as_a_missing_disc_key
|
|
|
|
Drives the real chain end to end — `KeySource` -> `resolve_and_apply_traced`
|
|
-> `Disc::aacs_error` -> `ensure_decryptable` — twice over the SAME disc,
|
|
and asserts the two operator-visible verdicts differ:
|
|
|
|
- source returns `Err(KeyServiceUnavailable)` (the HTTP-502 outage) → E7028
|
|
"the key service could not answer; retry later",
|
|
- source returns `Ok(vec![])` (the service answered, no entry) → E7022 "no
|
|
key source has a decryption key for this disc".
|
|
|
|
The credential and rate-limit verdicts are asserted alongside, since each is
|
|
a different operator action (fix the token / back off).
|
|
|
|
## css_disc_wide_no_key_is_disc_level_while_per_title_stays_skippable
|
|
|
|
The two CSS no-key conditions are NOT the same verdict and must classify
|
|
oppositely through the public predicates:
|
|
|
|
- **disc-wide** — `css_error` is set: the MAIN feature's crack failed, so
|
|
every title of this disc fails identically. Must be
|
|
`crate::error::is_disc_level_no_key` (the rip loop fail-fasts) and must
|
|
NOT be `crate::error::is_skippable_title_stub`. While both conditions
|
|
shared `E_CSS_KEY_MISSING`, an uncrackable CSS disc iterated all N titles
|
|
logging "title skipped" and exited 0 — a total failure reported as
|
|
success.
|
|
- **per-title** — one title's own re-crack failed on a multi-VTS disc
|
|
(`title_is_clear == false`, no key): skipping it and finishing the rest is
|
|
correct policy, so it must STAY skippable and must NOT be disc-level.
|
|
|
|
Pinned in both directions so a future change cannot silently flip either.
|
|
|
|
## correct_truehd_channels_full_correction_and_atmos_promotion
|
|
|
|
Kills the `was_basic ==`, the channels/rate `!=`/`&&` guards' "already
|
|
correct" branch, the `!matches!` per-stream skip, the `is_atmos ==
|
|
Some(true)` branch, and the whole-function no-op mutant.
|
|
|
|
## byte_offset_in_title_accumulates_across_extents
|
|
|
|
Kills: `lba >= start && lba < end` flipped to `||` (the first extent's
|
|
disjunction would trivially match almost any lba and return the wrong,
|
|
too-early offset); `cumulative +=` flipped to `*=` (cumulative is seeded at
|
|
0, so `*=` freezes it at 0 forever); and `sector_count * SECTOR_BYTES_U64`
|
|
flipped to `+` or `/` (wrong per-extent byte length folded into cumulative).
|
|
|
|
## chapter_at_offset_concrete_arithmetic
|
|
|
|
Kills: the `/` (time-fraction) flipped to `%` or `*`; the `*` (duration
|
|
scale) flipped to `+`; the `<=` chapter-scan comparison flipped to `>`; the
|
|
final `chapter_idx + 1` flipped to `-` or `*`; and every whole-function
|
|
fixed-tuple replacement (none produce `(2, 60.0)`).
|
|
|
|
## locate_ranges_positive_bps_duration_and_at_risk
|
|
|
|
Kills `bps > 0.0` flipped to `==`/`<` (both would take the `else 0.0`
|
|
branch here, wrongly reporting 0); `(*size as f64) / bps` flipped to `%` or
|
|
`*`; and the trailing `* MILLIS_PER_SEC` flipped to `+` or `/`.
|
|
|
|
## decrypt_keys_for_title_css_span_reuse_is_half_open
|
|
|
|
A CSS title key is per-VTS: reusing the scan's cracked key for a title that
|
|
lives OUTSIDE the cracked span descrambles that title with the wrong key,
|
|
and the mux emits garbage at exit 0. `crack_span` is documented as the
|
|
half-open LBA span `[start, end)`, so overlap is `extent.start < span.end &&
|
|
span.start < extent.end` — both comparisons STRICT. A title that merely
|
|
ABUTS the span (ends exactly where it begins, or begins exactly where it
|
|
ends) shares no sector with it and must NOT reuse the key.
|
|
|
|
The reader serves only clear (all-zero) sectors, so a title that falls
|
|
through to its own crack is reported unencrypted — distinguishable from the
|
|
reused-key answer both in the key and in the is-clear flag.
|
|
|
|
## merge_content_key_ranges
|
|
|
|
Merges per-title AACS key ranges into the sorted, disjoint set the
|
|
whole-disc map needs (`crate::decrypt::AacsKeyMap::entry_for` requires
|
|
disjoint ranges). Titles that share a clip resolve the SAME physical span
|
|
(same LBAs -> same CPS unit -> same key). When a later range overlaps a kept
|
|
one that carries the SAME key index and phase, the two are UNIONED (end
|
|
extended to the max) — this covers both the exact-duplicate (shared clip)
|
|
case and any partial overlap without ever dropping coverage, so no encrypted
|
|
LBA is left in no range (which would pass through as ciphertext). A real disc
|
|
never produces two DIFFERENT keys for one LBA; if that malformed case ever
|
|
appeared, the later range is dropped to keep the set disjoint rather than
|
|
extend one key over another key's LBAs.
|