Cache AacsKeyMap key indices; extract + test whole-disc range merge

- AacsKeyMap now derives its distinct key-index set once at construction
  (from_ranges_phased) instead of re-allocating/sorting it on every
  decrypt batch; key_indices() returns the cached slice.
- Extract the whole-disc content-map range merge out of
  resolve_content_key_map into merge_content_key_ranges and cover it:
  sort/disjoint, shared-clip dedup, overlap drop, adjacent-kept.
This commit is contained in:
Matthew Jackson
2026-07-23 16:00:00 -07:00
parent dc7dfc6041
commit 197489fb7c
2 changed files with 95 additions and 18 deletions
+16 -8
View File
@@ -209,6 +209,10 @@ pub struct AacsKeyMap {
// (start_lba, end_lba, key_idx, phase). An LBA in NO range is passed through // (start_lba, end_lba, key_idx, phase). An LBA in NO range is passed through
// untouched — the map is a positive list of "this key here", nothing more. // untouched — the map is a positive list of "this key here", nothing more.
ranges: Vec<(u32, u32, usize, Phase)>, ranges: Vec<(u32, u32, usize, Phase)>,
// Distinct, sorted key indices the map selects — derived from `ranges` once at
// construction so the per-batch decrypt bounds check does not re-allocate/sort
// it on every read. Kept in sync by building both in `from_ranges_phased`.
key_indices: Vec<usize>,
} }
impl AacsKeyMap { impl AacsKeyMap {
@@ -228,7 +232,13 @@ impl AacsKeyMap {
/// for base/CPS). Ranges are sorted; an LBA in no range is passed through. /// for base/CPS). Ranges are sorted; an LBA in no range is passed through.
pub fn from_ranges_phased(mut ranges: Vec<(u32, u32, usize, Phase)>) -> Self { pub fn from_ranges_phased(mut ranges: Vec<(u32, u32, usize, Phase)>) -> Self {
ranges.sort_by_key(|&(start, _, _, _)| start); ranges.sort_by_key(|&(start, _, _, _)| start);
Self { ranges } let mut key_indices: Vec<usize> = ranges.iter().map(|&(_, _, i, _)| i).collect();
key_indices.sort_unstable();
key_indices.dedup();
Self {
ranges,
key_indices,
}
} }
/// The `(key_idx, phase, range_start_lba)` for the aligned unit at `lba`, or /// The `(key_idx, phase, range_start_lba)` for the aligned unit at `lba`, or
@@ -264,12 +274,10 @@ impl AacsKeyMap {
} }
/// The distinct key indices this map selects — the CPS units / segments the /// The distinct key indices this map selects — the CPS units / segments the
/// title actually reaches. The resolver secures exactly these up front. /// title actually reaches. The resolver secures exactly these up front. Computed
pub fn key_indices(&self) -> Vec<usize> { /// once at construction (see [`from_ranges_phased`](Self::from_ranges_phased)).
let mut v: Vec<usize> = self.ranges.iter().map(|&(_, _, i, _)| i).collect(); pub fn key_indices(&self) -> &[usize] {
v.sort_unstable(); &self.key_indices
v.dedup();
v
} }
/// Build the FMTS **read plan**: the title's aligned units filtered down to /// Build the FMTS **read plan**: the title's aligned units filtered down to
@@ -392,7 +400,7 @@ pub fn decrypt_sectors_mapped(
// Validate every selectable index up front (fail loud) so the per-unit hot // Validate every selectable index up front (fail loud) so the per-unit hot
// loop can index without bounds churn and a resolver gap never silently // loop can index without bounds churn and a resolver gap never silently
// passes ciphertext through as "decrypted". // passes ciphertext through as "decrypted".
for idx in map.key_indices() { for &idx in map.key_indices() {
if unit_keys.get(idx).is_none() { if unit_keys.get(idx).is_none() {
return Err(crate::error::Error::DecryptFailed); return Err(crate::error::Error::DecryptFailed);
} }
+79 -10
View File
@@ -589,6 +589,25 @@ pub(crate) fn correct_truehd_channels(reader: &mut dyn SectorSource, title: &mut
} }
} }
/// Merge 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), so a later range that starts before the previous kept range's
/// end is that duplicate and is dropped. A real disc never produces two DIFFERENT
/// keys for one LBA, so the drop is a dedup, not a conflict resolution.
fn merge_content_key_ranges(
mut ranges: Vec<(u32, u32, usize, crate::decrypt::Phase)>,
) -> Vec<(u32, u32, usize, crate::decrypt::Phase)> {
ranges.sort_by_key(|&(s, _, _, _)| s);
let mut merged: Vec<(u32, u32, usize, crate::decrypt::Phase)> = Vec::new();
for r in ranges {
if merged.last().is_none_or(|&(_, e, _, _)| r.0 >= e) {
merged.push(r);
}
}
merged
}
/// Calculate how many bytes of bad/unreadable data fall within a title's extents. /// Calculate how many bytes of bad/unreadable data fall within a title's extents.
/// `pub(crate)` so autorip can use it for main-movie lost_ms computation. /// `pub(crate)` so autorip can use it for main-movie lost_ms computation.
pub fn bytes_bad_in_title(title: &DiscTitle, bad_ranges: &[(u64, u64)]) -> u64 { pub fn bytes_bad_in_title(title: &DiscTitle, bad_ranges: &[(u64, u64)]) -> u64 {
@@ -2376,16 +2395,9 @@ impl Disc {
crate::mux::resolve_mux_key_map(reader, title, keys, fetch, self.content_format)?; crate::mux::resolve_mux_key_map(reader, title, keys, fetch, self.content_format)?;
ranges.extend_from_slice(map.ranges()); ranges.extend_from_slice(map.ranges());
} }
ranges.sort_by_key(|&(s, _, _, _)| s); Ok(crate::decrypt::AacsKeyMap::from_ranges_phased(
let mut merged: Vec<(u32, u32, usize, crate::decrypt::Phase)> = Vec::new(); merge_content_key_ranges(ranges),
for r in ranges { ))
// Drop a range that overlaps one already kept (a clip shared by two
// titles resolves the same span twice) — entry_for needs disjoint ranges.
if merged.last().is_none_or(|&(_, e, _, _)| r.0 >= e) {
merged.push(r);
}
}
Ok(crate::decrypt::AacsKeyMap::from_ranges_phased(merged))
} }
/// The disc's AACS-encrypted content as a sorted, merged, disjoint set of /// The disc's AACS-encrypted content as a sorted, merged, disjoint set of
@@ -4320,6 +4332,63 @@ mod tests {
assert_eq!(merged_extents(v.iter()), vec![(100, 50)]); assert_eq!(merged_extents(v.iter()), vec![(100, 50)]);
} }
// ── merge_content_key_ranges (whole-disc AACS map assembly) ────────────────
use crate::decrypt::Phase;
/// Ranges from different titles are sorted by start LBA and kept disjoint.
#[test]
fn merge_key_ranges_sorts_and_keeps_disjoint() {
let v = vec![
(500u32, 600u32, 1usize, Phase::All),
(100, 200, 0, Phase::All),
(300, 400, 2, Phase::All),
];
assert_eq!(
merge_content_key_ranges(v),
vec![
(100, 200, 0, Phase::All),
(300, 400, 2, Phase::All),
(500, 600, 1, Phase::All),
]
);
}
/// A clip shared by two titles resolves the SAME span twice; the duplicate is
/// dropped so `entry_for` sees a disjoint set (one key for the span).
#[test]
fn merge_key_ranges_dedups_shared_clip_span() {
let v = vec![
(100u32, 300u32, 0usize, Phase::All),
(100, 300, 0, Phase::All),
];
assert_eq!(merge_content_key_ranges(v), vec![(100, 300, 0, Phase::All)]);
}
/// A later range that merely overlaps a kept one (starts before its end) is
/// dropped — the map stays disjoint rather than admitting an ambiguous LBA.
#[test]
fn merge_key_ranges_drops_overlap() {
let v = vec![
(100u32, 400u32, 0usize, Phase::All),
(200, 500, 0, Phase::All),
];
assert_eq!(merge_content_key_ranges(v), vec![(100, 400, 0, Phase::All)]);
}
/// Adjacent (touching) ranges are BOTH kept — `r.0 >= prev_end` holds when the
/// next starts exactly at the previous end, so no coverage is lost.
#[test]
fn merge_key_ranges_keeps_adjacent() {
let v = vec![
(100u32, 200u32, 0usize, Phase::All),
(200, 300, 1, Phase::All),
];
assert_eq!(
merge_content_key_ranges(v),
vec![(100, 200, 0, Phase::All), (200, 300, 1, Phase::All)]
);
}
/// A Windows-form optical device path (`\\.\CdRom0`, `\\.\D:`) must never /// A Windows-form optical device path (`\\.\CdRom0`, `\\.\D:`) must never
/// fall through to the block default (8192 sectors = 16 MiB, well over the /// fall through to the block default (8192 sectors = 16 MiB, well over the
/// optical 510-sector cap). It has no forward slash, so the Linux-sysfs /// optical 510-sector cap). It has no forward slash, so the Linux-sysfs