use std::collections::{HashMap, VecDeque};
use crate::aacs::{self, ALIGNED_UNIT_LEN};
use crate::consts::SECTOR_BYTES_U64;
use crate::decrypt::DecryptKeys;
use crate::sector::KeyFetch;
pub const POST_READ_VERIFY: bool = true;
const MAX_INFLIGHT_UNITS: usize = 4096;
const MAX_FETCH_CALLS: u32 = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ContainerKind {
#[default]
Ts,
Ps,
}
#[derive(Debug, Clone)]
pub struct ClipLayout {
pub size: u64,
pub extents: Vec<(u32, u32)>,
pub container: ContainerKind,
}
#[derive(Debug, Clone)]
struct ExtentRec {
disc_lba: u32,
sectors: u32,
file_off: u64,
clip: u32,
}
struct Partial {
buf: Box<[u8; ALIGNED_UNIT_LEN]>,
have: u8,
lba: [u32; 3],
}
enum Decryptability {
Decryptable,
Undecryptable,
Unknown,
}
pub struct UnitVerifier {
extents: Vec<ExtentRec>,
full_units: Vec<u32>,
containers: Vec<ContainerKind>,
keys: Vec<[u8; 16]>,
fetch: Option<KeyFetch>,
fetch_calls: u32,
fetch_spent: bool,
partials: HashMap<(u32, u32), Partial>,
lru: VecDeque<(u32, u32)>,
}
impl UnitVerifier {
pub fn new(clips: &[ClipLayout], keys: &DecryptKeys, fetch: Option<KeyFetch>) -> Option<Self> {
if !POST_READ_VERIFY {
return None;
}
let DecryptKeys::Aacs { unit_keys, .. } = keys else {
return None;
};
let held: Vec<[u8; 16]> = unit_keys.iter().map(|(_, k)| *k).collect();
if held.is_empty() && fetch.is_none() {
return None;
}
let mut extents = Vec::new();
let mut full_units = Vec::new();
let mut containers = Vec::new();
for (clip, layout) in clips.iter().enumerate() {
full_units.push((layout.size / ALIGNED_UNIT_LEN as u64) as u32);
containers.push(layout.container);
let mut file_off: u64 = 0;
for &(disc_lba, byte_len) in &layout.extents {
let sectors = (byte_len as u64).div_ceil(SECTOR_BYTES_U64) as u32;
if sectors > 0 {
extents.push(ExtentRec {
disc_lba,
sectors,
file_off,
clip: clip as u32,
});
}
file_off = file_off.saturating_add(byte_len as u64);
}
}
if extents.is_empty() {
return None;
}
extents.sort_by_key(|e| e.disc_lba);
Some(Self {
extents,
full_units,
containers,
keys: held,
fetch,
fetch_calls: 0,
fetch_spent: false,
partials: HashMap::new(),
lru: VecDeque::new(),
})
}
fn accept_for(&self, clip: u32) -> fn(&[u8]) -> bool {
match self.containers[clip as usize] {
ContainerKind::Ts => aacs::unit_is_clean_ts,
ContainerKind::Ps => aacs::unit_is_clean_ps,
}
}
pub fn observe(&mut self, disc_lba: u32, bytes: &[u8]) -> Vec<(u32, u32)> {
let mut bad: Vec<(u32, u32)> = Vec::new();
let sector = crate::consts::SECTOR_BYTES;
let n = bytes.len() / sector;
for s in 0..n {
let lba = disc_lba.saturating_add(s as u32);
let Some((clip, unit, slot)) = self.locate(lba) else {
continue; };
if unit >= self.full_units[clip as usize] {
continue;
}
let off = s * sector;
self.fill(clip, unit, slot, lba, &bytes[off..off + sector]);
if let Some((raw, lbas)) = self.take_if_complete(clip, unit) {
let accept = self.accept_for(clip);
match self.decryptability(&raw, accept) {
Decryptability::Undecryptable => push_ranges(&mut bad, &lbas),
Decryptability::Decryptable | Decryptability::Unknown => {}
}
}
}
bad
}
fn locate(&self, lba: u32) -> Option<(u32, u32, usize)> {
let idx = self.extents.partition_point(|e| e.disc_lba <= lba);
if idx == 0 {
return None;
}
let e = &self.extents[idx - 1];
let delta = lba - e.disc_lba;
if delta >= e.sectors {
return None; }
let file_off = e.file_off + delta as u64 * SECTOR_BYTES_U64;
if file_off % SECTOR_BYTES_U64 != 0 {
return None;
}
let unit = (file_off / ALIGNED_UNIT_LEN as u64) as u32;
let in_unit = (file_off % ALIGNED_UNIT_LEN as u64) as usize;
let slot = in_unit / crate::consts::SECTOR_BYTES;
Some((e.clip, unit, slot))
}
fn fill(&mut self, clip: u32, unit: u32, slot: usize, lba: u32, sector_bytes: &[u8]) {
let key = (clip, unit);
let entry = self.partials.entry(key);
let fresh = matches!(entry, std::collections::hash_map::Entry::Vacant(_));
let p = entry.or_insert_with(|| Partial {
buf: Box::new([0u8; ALIGNED_UNIT_LEN]),
have: 0,
lba: [u32::MAX; 3],
});
let off = slot * crate::consts::SECTOR_BYTES;
p.buf[off..off + crate::consts::SECTOR_BYTES].copy_from_slice(sector_bytes);
p.have |= 1 << slot;
p.lba[slot] = lba;
if fresh {
self.lru.push_back(key);
self.evict_if_needed();
}
}
fn take_if_complete(
&mut self,
clip: u32,
unit: u32,
) -> Option<([u8; ALIGNED_UNIT_LEN], [u32; 3])> {
let key = (clip, unit);
let complete = self
.partials
.get(&key)
.map(|p| p.have == 0b111)
.unwrap_or(false);
if !complete {
return None;
}
let p = self.partials.remove(&key)?;
if let Some(pos) = self.lru.iter().position(|k| *k == key) {
self.lru.remove(pos);
}
Some((*p.buf, p.lba))
}
fn evict_if_needed(&mut self) {
while self.partials.len() > MAX_INFLIGHT_UNITS {
if let Some(key) = self.lru.pop_front() {
self.partials.remove(&key);
} else {
break;
}
}
}
fn decryptability(
&mut self,
raw: &[u8; ALIGNED_UNIT_LEN],
accept: fn(&[u8]) -> bool,
) -> Decryptability {
if !aacs::aacs_unit_encrypted(raw) {
return if accept(raw) {
Decryptability::Decryptable
} else {
Decryptability::Unknown
};
}
if self.try_keys(raw, accept) {
return Decryptability::Decryptable;
}
if !self.fetch_spent && self.fetch_calls < MAX_FETCH_CALLS {
if let Some(cb) = self.fetch.clone() {
self.fetch_calls += 1;
let fresh = cb(&[raw.to_vec()]);
let mut added = false;
for k in fresh {
if !self.keys.contains(&k) {
self.keys.push(k);
added = true;
}
}
if !added {
self.fetch_spent = true; return Decryptability::Unknown;
}
if self.try_keys(raw, accept) {
return Decryptability::Decryptable;
}
return Decryptability::Undecryptable; }
}
Decryptability::Unknown
}
fn try_keys(&self, raw: &[u8; ALIGNED_UNIT_LEN], accept: fn(&[u8]) -> bool) -> bool {
for k in &self.keys {
let mut scratch = *raw;
if aacs::decrypt_unit_checked(&mut scratch, k, accept) {
return true;
}
}
false
}
pub fn reverify_iso<S: crate::sector::SectorSource>(
&mut self,
iso: &mut S,
ranges: &[(u64, u64)],
is_finished: &dyn Fn(u32) -> bool,
) -> Vec<(u32, u32)> {
let mut seen: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new();
let mut bad: Vec<(u32, u32)> = Vec::new();
for &(pos, len) in ranges {
if len == 0 {
continue;
}
let start = (pos / SECTOR_BYTES_U64) as u32;
let end = pos.saturating_add(len).div_ceil(SECTOR_BYTES_U64) as u32;
for lba in start..end {
let Some((clip, unit, _)) = self.locate(lba) else {
continue;
};
if unit >= self.full_units[clip as usize] || !seen.insert((clip, unit)) {
continue;
}
let Some(lbas) = self.unit_disc_sectors(clip, unit) else {
continue;
};
if !lbas.iter().all(|&l| is_finished(l)) {
continue;
}
let mut raw = [0u8; ALIGNED_UNIT_LEN];
let mut readable = true;
for (slot, &slba) in lbas.iter().enumerate() {
let off = slot * crate::consts::SECTOR_BYTES;
if iso
.read_sectors(
slba,
1,
&mut raw[off..off + crate::consts::SECTOR_BYTES],
false,
)
.is_err()
{
readable = false;
break;
}
}
let accept = self.accept_for(clip);
if readable
&& matches!(
self.decryptability(&raw, accept),
Decryptability::Undecryptable
)
{
push_ranges(&mut bad, &lbas);
}
}
}
bad
}
fn unit_disc_sectors(&self, clip: u32, unit: u32) -> Option<[u32; 3]> {
let base = unit as u64 * ALIGNED_UNIT_LEN as u64;
let mut out = [u32::MAX; 3];
for (slot, item) in out.iter_mut().enumerate() {
let foff = base + slot as u64 * SECTOR_BYTES_U64;
let e = self.extents.iter().find(|e| {
e.clip == clip
&& e.file_off <= foff
&& foff < e.file_off + e.sectors as u64 * SECTOR_BYTES_U64
})?;
*item = e.disc_lba + ((foff - e.file_off) / SECTOR_BYTES_U64) as u32;
}
Some(out)
}
}
fn push_ranges(out: &mut Vec<(u32, u32)>, lbas: &[u32; 3]) {
let mut present: Vec<u32> = lbas.iter().copied().filter(|&l| l != u32::MAX).collect();
present.sort_unstable();
for lba in present {
if let Some(last) = out.last_mut() {
if last.0 + last.1 == lba {
last.1 += 1;
continue;
}
}
out.push((lba, 1));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
const TS_SYNC: u8 = 0x47;
fn clear_unit() -> Vec<u8> {
let mut u = vec![0u8; ALIGNED_UNIT_LEN];
let mut off = 4;
while off < ALIGNED_UNIT_LEN {
u[off] = TS_SYNC;
off += 192;
}
u
}
fn clear_ps_unit() -> Vec<u8> {
let mut u = vec![0u8; ALIGNED_UNIT_LEN];
for o in [0usize, 2048, 4096] {
u[o..o + 4].copy_from_slice(&[0x00, 0x00, 0x01, 0xBA]);
}
u
}
fn encrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) {
use aes::Aes128;
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
unit[0] |= 0xC0; let header: [u8; 16] = unit[..16].try_into().unwrap();
let derived = crate::aacs::decrypt::aes_ecb_encrypt(unit_key, &header);
let mut k = [0u8; 16];
for i in 0..16 {
k[i] = derived[i] ^ header[i];
}
let cipher = Aes128::new(GenericArray::from_slice(&k));
let mut prev = crate::aacs::decrypt::AACS_IV;
for i in 0..(ALIGNED_UNIT_LEN - 16) / 16 {
let off = 16 + i * 16;
for j in 0..16 {
unit[off + j] ^= prev[j];
}
let mut block = GenericArray::clone_from_slice(&unit[off..off + 16]);
cipher.encrypt_block(&mut block);
unit[off..off + 16].copy_from_slice(&block);
prev.copy_from_slice(&unit[off..off + 16]);
}
}
fn aacs_keys(keys: &[[u8; 16]]) -> DecryptKeys {
DecryptKeys::Aacs {
unit_keys: keys
.iter()
.enumerate()
.map(|(i, k)| (i as u32, *k))
.collect(),
read_data_key: None,
}
}
fn one_clip(lba: u32) -> Vec<ClipLayout> {
vec![ts_clip(
ALIGNED_UNIT_LEN as u64,
vec![(lba, ALIGNED_UNIT_LEN as u32)],
)]
}
fn ts_clip(size: u64, extents: Vec<(u32, u32)>) -> ClipLayout {
ClipLayout {
size,
extents,
container: ContainerKind::Ts,
}
}
#[test]
fn kill_switch_default_on() {
assert!(POST_READ_VERIFY, "shipping default: gate enabled");
}
#[test]
fn new_is_none_for_non_aacs() {
assert!(UnitVerifier::new(&one_clip(100), &DecryptKeys::None, None).is_none());
assert!(
UnitVerifier::new(
&one_clip(100),
&DecryptKeys::Css { title_key: [0; 5] },
None
)
.is_none()
);
}
#[test]
fn new_is_none_with_no_keys_and_no_fetch() {
assert!(UnitVerifier::new(&one_clip(100), &aacs_keys(&[]), None).is_none());
}
#[test]
fn new_is_none_with_no_extents() {
let clips = vec![ts_clip(0, vec![])];
assert!(UnitVerifier::new(&clips, &aacs_keys(&[[1; 16]]), None).is_none());
}
#[test]
fn clear_clean_unit_is_good() {
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[[1; 16]]), None).unwrap();
let bad = v.observe(100, &clear_unit());
assert!(bad.is_empty(), "clean clear unit must not be flagged");
}
#[test]
fn clear_corrupted_unit_is_skipped_not_flagged() {
let mut u = clear_unit();
u[4] = 0x00; let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[[1; 16]]), None).unwrap();
let bad = v.observe(100, &u);
assert!(
bad.is_empty(),
"clear-but-not-clean unit -> skip, never false-bad"
);
}
#[test]
fn encrypted_unit_held_key_decrypts_is_good() {
let key = [0x5a; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &key);
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[key]), None).unwrap();
assert!(v.observe(100, &u).is_empty(), "right key -> good");
}
#[test]
fn encrypted_unit_wrong_key_no_fetch_is_uncertain_not_bad() {
let real = [0x11; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &real);
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x22; 16]]), None).unwrap();
assert!(
v.observe(100, &u).is_empty(),
"missing key without a fetch seam must NOT be a false-bad"
);
}
#[test]
fn encrypted_unit_fetch_supplies_right_key_is_good() {
let real = [0x33; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &real);
let fetch: KeyFetch = Arc::new(move |_samples: &[Vec<u8>]| vec![real]);
let mut v =
UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x44; 16]]), Some(fetch)).unwrap();
assert!(
v.observe(100, &u).is_empty(),
"fetched key recovers -> good"
);
}
#[test]
fn encrypted_unit_fetch_supplies_wrong_keys_is_bad() {
let real = [0x55; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &real);
let fetch: KeyFetch = Arc::new(move |_s: &[Vec<u8>]| vec![[0x99; 16], [0xAA; 16]]);
let mut v =
UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x66; 16]]), Some(fetch)).unwrap();
assert_eq!(
v.observe(100, &u),
vec![(100, 3)],
"wrong fetched keys -> bad"
);
}
#[test]
fn encrypted_unit_fetch_supplies_nothing_is_uncertain() {
let real = [0x77; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &real);
let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| Vec::new());
let mut v =
UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x88; 16]]), Some(fetch)).unwrap();
assert!(
v.observe(100, &u).is_empty(),
"fetch returns nothing new -> uncertain -> skip"
);
}
#[test]
fn fetched_key_is_cached_for_later_units() {
let real = [0xC3; 16];
let calls = Arc::new(std::sync::atomic::AtomicU32::new(0));
let c = calls.clone();
let fetch: KeyFetch = Arc::new(move |_s: &[Vec<u8>]| {
c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
vec![real]
});
let clips = vec![ts_clip(
2 * ALIGNED_UNIT_LEN as u64,
vec![(200, 2 * ALIGNED_UNIT_LEN as u32)],
)];
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[[0x01; 16]]), Some(fetch)).unwrap();
let mut u0 = clear_unit();
encrypt_unit(&mut u0, &real);
let mut u1 = clear_unit();
encrypt_unit(&mut u1, &real);
assert!(v.observe(200, &u0).is_empty());
assert!(v.observe(203, &u1).is_empty());
assert_eq!(
calls.load(std::sync::atomic::Ordering::SeqCst),
1,
"second orphan unit reuses the cached fetched key (one fetch total)"
);
}
#[test]
fn fragmented_unit_assembles_across_distant_extents() {
let real = [0x42; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &real);
let clips = vec![ts_clip(
ALIGNED_UNIT_LEN as u64,
vec![(10, 4096), (5000, 2048)],
)];
let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| vec![[0xEE; 16]]);
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[[0x01; 16]]), Some(fetch)).unwrap();
assert!(
v.observe(10, &u[..4096]).is_empty(),
"incomplete -> no verdict yet"
);
let bad = v.observe(5000, &u[4096..]);
assert_eq!(
bad,
vec![(10, 2), (5000, 1)],
"fragmented bad unit -> both ranges"
);
}
#[test]
fn partial_tail_unit_is_never_verified() {
let key = [0x5a; 16];
let mut u0 = clear_unit();
encrypt_unit(&mut u0, &key);
let clips = vec![ts_clip(
ALIGNED_UNIT_LEN as u64 + 2048,
vec![(100, ALIGNED_UNIT_LEN as u32 + 2048)],
)];
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[key]), None).unwrap();
let mut feed = u0.clone();
feed.extend_from_slice(&[0xABu8; 2048]); assert!(
v.observe(100, &feed).is_empty(),
"tail partial never flagged"
);
}
#[test]
fn incomplete_unit_from_skip_is_never_flagged() {
let real = [0x11; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &real);
let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| vec![[0xEE; 16]]);
let mut v =
UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x01; 16]]), Some(fetch)).unwrap();
assert!(
v.observe(100, &u[..4096]).is_empty(),
"incomplete unit -> no verdict"
);
}
#[test]
fn sectors_split_across_observe_calls_still_complete() {
let key = [0x5a; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &key);
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[key]), None).unwrap();
assert!(v.observe(100, &u[..2048]).is_empty()); assert!(v.observe(101, &u[2048..4096]).is_empty()); assert!(v.observe(102, &u[4096..]).is_empty()); }
#[test]
fn sector_outside_any_clip_is_ignored() {
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[[1; 16]]), None).unwrap();
assert!(v.observe(50, &[0u8; 2048]).is_empty());
assert!(v.observe(200, &[0u8; 2048]).is_empty());
}
struct MockIso {
sectors: std::collections::HashMap<u32, [u8; 2048]>,
err_lba: Option<u32>,
}
impl crate::sector::SectorSource for MockIso {
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> crate::Result<usize> {
for i in 0..count as u32 {
if self.err_lba == Some(lba + i) {
return Err(crate::error::Error::DiscRead {
sector: (lba + i) as u64,
status: None,
sense: None,
});
}
let s = self.sectors.get(&(lba + i)).copied().unwrap_or([0u8; 2048]);
let off = i as usize * 2048;
buf[off..off + 2048].copy_from_slice(&s);
}
Ok(count as usize * 2048)
}
}
fn place_unit(iso: &mut MockIso, lbas: [u32; 3], unit: &[u8]) {
for (slot, &lba) in lbas.iter().enumerate() {
let mut s = [0u8; 2048];
s.copy_from_slice(&unit[slot * 2048..slot * 2048 + 2048]);
iso.sectors.insert(lba, s);
}
}
#[test]
fn reverify_iso_good_unit_returns_empty() {
let key = [0x5a; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &key);
let mut iso = MockIso {
sectors: Default::default(),
err_lba: None,
};
place_unit(&mut iso, [100, 101, 102], &u);
let mut v = UnitVerifier::new(&one_clip(100), &aacs_keys(&[key]), None).unwrap();
let bad = v.reverify_iso(&mut iso, &[(100 * 2048, 3 * 2048)], &|_| true);
assert!(bad.is_empty(), "decryptable unit re-read clean -> not bad");
}
#[test]
fn reverify_iso_bad_unit_returns_its_range() {
let real = [0x11; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &real);
let mut iso = MockIso {
sectors: Default::default(),
err_lba: None,
};
place_unit(&mut iso, [100, 101, 102], &u);
let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| vec![[0xEE; 16]]);
let mut v =
UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x22; 16]]), Some(fetch)).unwrap();
let bad = v.reverify_iso(&mut iso, &[(101 * 2048, 2048)], &|_| true);
assert_eq!(
bad,
vec![(100, 3)],
"undecryptable unit -> full 3-sector range"
);
}
#[test]
fn reverify_iso_fragmented_unit_reads_distant_sectors() {
let key = [0x5a; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &key);
let clips = vec![ts_clip(
ALIGNED_UNIT_LEN as u64,
vec![(10, 4096), (5000, 2048)],
)];
let mut iso = MockIso {
sectors: Default::default(),
err_lba: None,
};
place_unit(&mut iso, [10, 11, 5000], &u);
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[key]), None).unwrap();
let bad = v.reverify_iso(&mut iso, &[(5000 * 2048, 2048)], &|_| true);
assert!(bad.is_empty(), "fragmented decryptable unit re-read clean");
}
#[test]
fn reverify_iso_unreadable_sector_skips_unit() {
let real = [0x11; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &real);
let mut iso = MockIso {
sectors: Default::default(),
err_lba: Some(102), };
place_unit(&mut iso, [100, 101, 102], &u);
let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| vec![[0xEE; 16]]);
let mut v =
UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x22; 16]]), Some(fetch)).unwrap();
let bad = v.reverify_iso(&mut iso, &[(100 * 2048, 3 * 2048)], &|_| true);
assert!(
bad.is_empty(),
"ISO read error on a sector -> skip (fail-safe)"
);
}
#[test]
fn reverify_iso_skips_unit_with_unread_sector_and_never_fetches() {
let real = [0x11; 16];
let mut u = clear_unit();
encrypt_unit(&mut u, &real);
let mut iso = MockIso {
sectors: Default::default(),
err_lba: None,
};
place_unit(&mut iso, [100, 101, 102], &u);
let fetch: KeyFetch =
Arc::new(|_s: &[Vec<u8>]| panic!("must NOT key-fetch an unread unit"));
let mut v =
UnitVerifier::new(&one_clip(100), &aacs_keys(&[[0x22; 16]]), Some(fetch)).unwrap();
let is_finished = |lba: u32| lba != 102;
let bad = v.reverify_iso(&mut iso, &[(100 * 2048, 3 * 2048)], &is_finished);
assert!(
bad.is_empty(),
"unit with an unread sector is skipped, not flagged or fetched"
);
}
#[test]
fn ps_container_routes_through_pack_check() {
let clips = vec![ClipLayout {
size: ALIGNED_UNIT_LEN as u64,
extents: vec![(100, ALIGNED_UNIT_LEN as u32)],
container: ContainerKind::Ps,
}];
let mut v = UnitVerifier::new(&clips, &aacs_keys(&[[1; 16]]), None).unwrap();
assert!(
v.observe(100, &clear_ps_unit()).is_empty(),
"valid PS unit passes unit_is_clean_ps"
);
let mut enc = clear_ps_unit();
enc[0] |= 0xC0; let fetch: KeyFetch = Arc::new(|_s: &[Vec<u8>]| vec![[0xEE; 16]]);
let mut v2 = UnitVerifier::new(&clips, &aacs_keys(&[[0x22; 16]]), Some(fetch)).unwrap();
assert_eq!(
v2.observe(100, &enc),
vec![(100, 3)],
"encrypted PS unit no key opens -> bad via the PS check"
);
}
#[test]
fn eviction_bounds_inflight_partials() {
let mut v = UnitVerifier::new(
&vec![ts_clip(
(MAX_INFLIGHT_UNITS as u64 + 100) * ALIGNED_UNIT_LEN as u64,
vec![(0, u32::MAX / 2)],
)],
&aacs_keys(&[[1; 16]]),
None,
)
.unwrap();
for unit in 0..(MAX_INFLIGHT_UNITS as u32 + 50) {
let lba = unit * 3;
let _ = v.observe(lba, &[0u8; 2048]);
}
assert!(
v.partials.len() <= MAX_INFLIGHT_UNITS,
"in-flight partials bounded by the cap"
);
}
}