mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
decrypt: keep outer key range tail under nested ranges; ifo: fall back to 1:1 for out-of-table PTT_SRPT PGCN; revert angle-cell time exclusion
This commit is contained in:
+71
-26
@@ -210,6 +210,8 @@ pub struct AacsKeyMap {
|
||||
// (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.
|
||||
ranges: Vec<(u32, u32, usize, Phase)>,
|
||||
// Parallel to `ranges`: the original start each piece's unit parity is measured from.
|
||||
anchors: Vec<u32>,
|
||||
// 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`.
|
||||
@@ -233,24 +235,38 @@ impl AacsKeyMap {
|
||||
/// for base/CPS). Ranges are sorted; an LBA in no range is passed through.
|
||||
pub(crate) fn from_ranges_phased(mut ranges: Vec<(u32, u32, usize, Phase)>) -> Self {
|
||||
ranges.sort_by_key(|&(start, _, _, _)| start);
|
||||
// Enforce disjointness (entry_for relies on it): a range starting inside its
|
||||
// predecessor takes over from there, and the predecessor is cut at that start.
|
||||
// Disjointness (entry_for relies on it): each boundary-to-boundary stretch goes to
|
||||
// the covering range that starts last; `anchors` keeps the ORIGINAL start that
|
||||
// FMTS unit parity is measured from.
|
||||
let mut bounds: Vec<u32> = ranges.iter().flat_map(|r| [r.0, r.1]).collect();
|
||||
bounds.sort_unstable();
|
||||
bounds.dedup();
|
||||
let mut disjoint: Vec<(u32, u32, usize, Phase)> = Vec::with_capacity(ranges.len());
|
||||
for r in ranges {
|
||||
if let Some(prev) = disjoint.last_mut()
|
||||
&& prev.1 > r.0
|
||||
let mut anchors: Vec<u32> = Vec::with_capacity(ranges.len());
|
||||
let mut srcs: Vec<usize> = Vec::with_capacity(ranges.len());
|
||||
let mut overlapped = false;
|
||||
for w in bounds.windows(2) {
|
||||
let (a, b) = (w[0], w[1]);
|
||||
let mut cover = (0..ranges.len()).rev().filter(|&i| {
|
||||
let r = ranges[i];
|
||||
r.0 <= a && b <= r.1 && r.0 < r.1
|
||||
});
|
||||
let Some(win) = cover.next() else { continue };
|
||||
overlapped |= cover.next().is_some();
|
||||
if srcs.last() == Some(&win)
|
||||
&& let Some(last) = disjoint.last_mut()
|
||||
&& last.1 == a
|
||||
{
|
||||
tracing::warn!(
|
||||
start = r.0,
|
||||
prev_start = prev.0,
|
||||
"overlapping AACS key ranges; the later range takes over"
|
||||
);
|
||||
prev.1 = r.0;
|
||||
}
|
||||
disjoint.retain(|&(s, e, _, _)| s < e);
|
||||
if r.0 < r.1 {
|
||||
disjoint.push(r);
|
||||
last.1 = b;
|
||||
continue;
|
||||
}
|
||||
let r = ranges[win];
|
||||
disjoint.push((a, b, r.2, r.3));
|
||||
anchors.push(r.0);
|
||||
srcs.push(win);
|
||||
}
|
||||
if overlapped {
|
||||
tracing::warn!("overlapping AACS key ranges; the later-starting range takes over");
|
||||
}
|
||||
let ranges = disjoint;
|
||||
let mut key_indices: Vec<usize> = ranges.iter().map(|&(_, _, i, _)| i).collect();
|
||||
@@ -258,6 +274,7 @@ impl AacsKeyMap {
|
||||
key_indices.dedup();
|
||||
Self {
|
||||
ranges,
|
||||
anchors,
|
||||
key_indices,
|
||||
}
|
||||
}
|
||||
@@ -267,20 +284,17 @@ impl AacsKeyMap {
|
||||
/// the unit through untouched). O(log n). `range_start_lba` lets the mapped
|
||||
/// decrypt compute a unit's parity WITHIN a forensic segment (`Even`/`Odd`).
|
||||
pub fn entry_for(&self, lba: u32) -> Option<(usize, Phase, u32)> {
|
||||
match self
|
||||
let i = match self
|
||||
.ranges
|
||||
.binary_search_by(|&(start, _, _, _)| start.cmp(&lba))
|
||||
{
|
||||
Ok(i) => {
|
||||
let (start, _, idx, ph) = self.ranges[i];
|
||||
Some((idx, ph, start))
|
||||
}
|
||||
Err(0) => None,
|
||||
Err(i) => {
|
||||
let (start, end, idx, ph) = self.ranges[i - 1];
|
||||
(lba >= start && lba < end).then_some((idx, ph, start))
|
||||
}
|
||||
}
|
||||
Ok(i) => i,
|
||||
Err(0) => return None,
|
||||
Err(i) => i - 1,
|
||||
};
|
||||
let (start, end, idx, ph) = self.ranges[i];
|
||||
(lba >= start && lba < end)
|
||||
.then(|| (idx, ph, self.anchors.get(i).copied().unwrap_or(start)))
|
||||
}
|
||||
|
||||
/// The unit-key index for the aligned unit at `lba`, or `None` when no range
|
||||
@@ -1853,6 +1867,37 @@ mod tests {
|
||||
}
|
||||
|
||||
// Overlapping ranges are made disjoint (later start wins) so entry_for stays sound.
|
||||
#[test]
|
||||
fn nested_key_range_keeps_outer_tail_and_phase_anchor() {
|
||||
let map =
|
||||
AacsKeyMap::from_ranges_phased(vec![(0, 100, 0, Phase::Even), (20, 60, 1, Phase::All)]);
|
||||
assert_eq!(map.key_idx_for(80), Some(0));
|
||||
assert_eq!(map.key_idx_for(40), Some(1));
|
||||
assert_eq!(map.entry_for(80), Some((0, Phase::Even, 0)));
|
||||
assert_eq!(map.entry_for(10), Some((0, Phase::Even, 0)));
|
||||
}
|
||||
|
||||
/// Every sector of every input range resolves to some key.
|
||||
#[test]
|
||||
fn every_sector_of_every_input_range_gets_a_key() {
|
||||
let input = vec![
|
||||
(0, 100, 0),
|
||||
(20, 30, 1),
|
||||
(25, 60, 2),
|
||||
(90, 120, 3),
|
||||
(110, 115, 4),
|
||||
];
|
||||
let map = AacsKeyMap::from_ranges(input.clone());
|
||||
for (s, e, _) in input {
|
||||
for lba in s..e {
|
||||
assert!(map.key_idx_for(lba).is_some(), "lba {lba} lost its key");
|
||||
}
|
||||
}
|
||||
assert_eq!(map.key_idx_for(60), Some(0));
|
||||
assert_eq!(map.key_idx_for(116), Some(3));
|
||||
assert_eq!(map.key_idx_for(120), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_key_ranges_are_made_disjoint() {
|
||||
let map = AacsKeyMap::from_ranges(vec![(0, 100, 0), (20, 30, 1), (25, 60, 2), (60, 60, 3)]);
|
||||
|
||||
+20
-48
@@ -812,8 +812,10 @@ pub(crate) fn parse_pgcit(
|
||||
let mut skipped = 0usize;
|
||||
|
||||
for &(chapter_count, vts_title_num) in titles_info {
|
||||
// TTN -> PGC goes through PTT_SRPT; without it assume 1:1 (TTN is 1-based).
|
||||
// TTN -> PGC goes through PTT_SRPT; without it (or when its PGCN is past the
|
||||
// table) assume 1:1 (TTN is 1-based).
|
||||
let pgc_index = ptt_srpt_pgc_index(data, ptt_offset, vts_title_num)
|
||||
.filter(|&i| i < num_pgcs as usize)
|
||||
.unwrap_or(vts_title_num.saturating_sub(1) as usize);
|
||||
if pgc_index >= num_pgcs as usize {
|
||||
skipped += 1;
|
||||
@@ -931,20 +933,9 @@ pub(crate) fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result
|
||||
);
|
||||
}
|
||||
|
||||
// Only one angle plays, so non-first angle cells after the leading (dropped, and
|
||||
// shifted out by the scanner) cells add no time.
|
||||
let lead = leading_secondary_cells(&cells);
|
||||
let plays =
|
||||
|i: usize, cat: u8| i < lead || !CellCategory::decode(cat).is_secondary_block_piece();
|
||||
|
||||
// Recalculate duration from cell times if PGC-level time is zero.
|
||||
let duration_secs = if duration_secs == 0.0 && !cells.is_empty() {
|
||||
cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(i, c)| plays(i, c.category))
|
||||
.map(|(_, c)| c.duration_secs)
|
||||
.sum()
|
||||
cells.iter().map(|c| c.duration_secs).sum()
|
||||
} else {
|
||||
duration_secs
|
||||
};
|
||||
@@ -969,10 +960,6 @@ pub(crate) fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result
|
||||
cell_frames.push((0, 0.0));
|
||||
continue;
|
||||
}
|
||||
if !plays(i, data[co]) {
|
||||
cell_frames.push((0, 0.0));
|
||||
continue;
|
||||
}
|
||||
let t = &data[co + 4..co + 8];
|
||||
match bcd_to_frames(t) {
|
||||
Some((f, r)) => {
|
||||
@@ -2723,6 +2710,22 @@ mod tests {
|
||||
assert_eq!(titles[0].duration_secs, 11.011);
|
||||
}
|
||||
|
||||
/// A PTT_SRPT PGCN past the table falls back to the 1:1 mapping, not a dropped title.
|
||||
#[test]
|
||||
fn pgcit_ptt_pgcn_past_table_falls_back_to_one_to_one() {
|
||||
let pgc0 = build_pgc(bcd_secs(11), &[(0x00, bcd_secs(11), 0, 9)], &[], None);
|
||||
let pgc1 = build_pgc(bcd_secs(22), &[(0x00, bcd_secs(22), 50, 59)], &[], None);
|
||||
let pgcit = build_pgcit(&[pgc0, pgc1]);
|
||||
let pgcit_at = 512usize;
|
||||
let mut data = vec![0u8; pgcit_at];
|
||||
data.extend_from_slice(&pgcit);
|
||||
let ptt_at = data.len();
|
||||
data.extend_from_slice(&build_ptt_srpt(&[9, 9]));
|
||||
let titles = parse_pgcit(&data, pgcit_at, ptt_at, &[(5, 1), (6, 2)]).unwrap();
|
||||
let got: Vec<f64> = titles.iter().map(|t| t.duration_secs).collect();
|
||||
assert_eq!(got, vec![11.011, 22.022]);
|
||||
}
|
||||
|
||||
/// A title number past num_pgcs is dropped, even when an in-buffer 8-byte
|
||||
/// "entry" happens to sit at that index.
|
||||
#[test]
|
||||
@@ -2738,37 +2741,6 @@ mod tests {
|
||||
assert!(titles.is_empty(), "TTN 3 of a 2-PGC table must be omitted");
|
||||
}
|
||||
|
||||
// Cell category bytes for an angle block: block_type=1, block_mode 1/2/3.
|
||||
const ANGLE_FIRST: u8 = 0b0101_0000;
|
||||
const ANGLE_MID: u8 = 0b1001_0000;
|
||||
const ANGLE_LAST: u8 = 0b1101_0000;
|
||||
|
||||
/// Only one angle plays: the other angles' cells add no time to later chapters
|
||||
/// or to the zero-PGC-time duration fallback.
|
||||
#[test]
|
||||
fn pgc_multi_angle_cells_count_once_in_chapter_times_and_duration() {
|
||||
let cells = [
|
||||
(0x00, bcd_secs(10), 0, 9),
|
||||
(ANGLE_FIRST, bcd_secs(20), 10, 19),
|
||||
(ANGLE_MID, bcd_secs(20), 20, 29),
|
||||
(ANGLE_LAST, bcd_secs(20), 30, 39),
|
||||
(0x00, bcd_secs(30), 40, 49),
|
||||
];
|
||||
let pgc = build_pgc(bcd_secs(59), &cells, &[1, 2, 5], None);
|
||||
let title = parse_pgc(&pgc, 0, 3).unwrap();
|
||||
let want = [0.0, 10.01, 30.03];
|
||||
for (g, w) in title.chapter_times.iter().zip(want) {
|
||||
assert!((g - w).abs() < 1e-9, "chapters {:?}", title.chapter_times);
|
||||
}
|
||||
let zero_time = build_pgc([0; 4], &cells, &[1, 2, 5], None);
|
||||
let title = parse_pgc(&zero_time, 0, 3).unwrap();
|
||||
assert!(
|
||||
(title.duration_secs - 60.06).abs() < 1e-6,
|
||||
"duration {}",
|
||||
title.duration_secs
|
||||
);
|
||||
}
|
||||
|
||||
/// Cells with no rate flag fall back to literal seconds; in a mixed-rate PGC the
|
||||
/// first rate seen wins for every frame count.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user