Anchor the folder encryption probe on a unit boundary

probe_folder_encryption sampled AACS units starting from the largest extent
anywhere in the disc. AACS units are 3 sectors and a unit boundary is only
guaranteed at the START of a clip, so that anchor is only correct when the
largest extent happens to be a clip first extent.

For any clip over ~2 GiB it is not. The planner caps an allocation descriptor
at MAX_AD_BYTES = 524287 sectors, so every full piece of a split file ties on
sector_count and max_by_key returns the LAST tie — an extent beginning
(k-1)*524287 sectors into the file. 524287 % 3 == 1, so that start misses the
unit boundary for two file sizes in three.

The 6144-byte sample windows then begin mid-source-packet and the byte read as
the CPI flag is content. Both verdicts fail in a costly direction: a decrypted
folder is rejected as encrypted (DirImageEncrypted on something perfectly
rippable — the exact case scan_dir was added to rescue), or real ciphertext
reads as clear and the mux writes it out as video at exit 0. is_unit_aligned
cannot catch it: it measures against the same wrong base.

Now the largest TITLE first extent, which is unit-aligned by construction and
is also the more meaningful sample — the main feature rather than whichever
fragment happened to be biggest.

The existing tests could not reach this: their fixture m2ts is 786,432 bytes,
a single extent, which is always its own first.
This commit is contained in:
Matthew Jackson
2026-08-06 07:37:59 -07:00
parent bc5e3453b4
commit 84e0ba9fa8
+24 -2
View File
@@ -508,11 +508,33 @@ fn probe_folder_encryption(reader: &mut dyn SectorSource, disc: &Disc) -> Result
use crate::consts::SECTOR_BYTES;
const UNIT_SECTORS: u32 = 3;
// Anchor on the largest TITLE's FIRST extent, not on the largest extent
// anywhere.
//
// AACS units are 3 sectors, and a unit boundary is only guaranteed at the
// START of a clip. `max_by_key` over every extent picked a mid-file one for
// any clip big enough to be split: the planner caps an allocation
// descriptor at MAX_AD_BYTES = 524287 sectors, every full piece of a split
// file therefore ties on sector_count, and `max_by_key` returns the LAST
// tie — an extent starting (k-1)*524287 sectors in. 524287 % 3 == 1, so
// that start is off the unit boundary for two file sizes in three.
//
// The sampling then reads 6144-byte windows that begin mid-source-packet,
// so the CPI byte it thinks it is testing is content. Both verdicts are
// wrong in a costly direction: a decrypted folder gets rejected as
// encrypted (DirImageEncrypted on something perfectly rippable), or
// genuine ciphertext reads as clear and the mux writes it out as video at
// exit 0. `is_unit_aligned` cannot catch it, because it measures against
// this same wrong base.
let Some(extent) = disc
.titles
.iter()
.flat_map(|t| t.extents.iter())
.max_by_key(|e| e.sector_count)
.max_by_key(|t| {
t.extents
.iter()
.fold(0u64, |a, e| a.saturating_add(e.sector_count as u64))
})
.and_then(|t| t.extents.first())
else {
// No content to judge. A folder with an AACS directory and no titles
// has nothing to rip either way; leave the structural verdict alone.