Library-wide review-and-fix pass: tightened AACS keydb/handshake/variant handling and trailing-partial-unit policy, corrected MPLS mark offset and added UDF allocation bounds, hardened the mux/codec framing and M2TS paths, guarded SCSI READ CAPACITY short transfers and unified error mapping, added overflow guards on untrusted disc input, and made prefetch shutdown deterministic. Release profile now builds with thin LTO + single codegen unit.
484 lines
19 KiB
Rust
484 lines
19 KiB
Rust
//! AACS content decryption — AES primitives, unit decryption, bus encryption.
|
||
|
||
use aes::Aes128;
|
||
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||
|
||
// ── AACS constants ──────────────────────────────────────────────────────────
|
||
|
||
/// Fixed IV used by AACS for all AES-CBC operations.
|
||
pub(crate) const AACS_IV: [u8; 16] = [
|
||
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
|
||
];
|
||
|
||
/// Size of an AACS aligned unit (3 × 2048-byte sectors).
|
||
pub const ALIGNED_UNIT_LEN: usize = 6144;
|
||
|
||
/// Size of one sector.
|
||
const SECTOR_LEN: usize = 2048;
|
||
|
||
/// Transport stream packet spacing in Blu-ray m2ts (192 bytes = 4 TP_extra + 188 TS).
|
||
const TS_PACKET_LEN: usize = 192;
|
||
|
||
/// TS sync byte.
|
||
const TS_SYNC: u8 = 0x47;
|
||
|
||
// ── AES primitives ──────────────────────────────────────────────────────────
|
||
|
||
/// AES-128-ECB encrypt a single 16-byte block.
|
||
pub(crate) fn aes_ecb_encrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||
let mut block = GenericArray::clone_from_slice(data);
|
||
cipher.encrypt_block(&mut block);
|
||
let mut out = [0u8; 16];
|
||
out.copy_from_slice(&block);
|
||
out
|
||
}
|
||
|
||
/// AES-128-ECB decrypt a single 16-byte block.
|
||
pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||
let mut block = GenericArray::clone_from_slice(data);
|
||
cipher.decrypt_block(&mut block);
|
||
let mut out = [0u8; 16];
|
||
out.copy_from_slice(&block);
|
||
out
|
||
}
|
||
|
||
/// AES-128-CBC decrypt in-place with the fixed AACS IV.
|
||
///
|
||
/// Precondition: `data.len()` is a multiple of 16. Any trailing partial
|
||
/// block is silently ignored; all callers pass aligned regions (6128 and
|
||
/// 2032 bytes), and the assert documents/enforces that contract.
|
||
pub(crate) fn aes_cbc_decrypt(key: &[u8; 16], data: &mut [u8]) {
|
||
debug_assert!(
|
||
data.len() % 16 == 0,
|
||
"aes_cbc_decrypt requires a block-aligned slice"
|
||
);
|
||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||
let num_blocks = data.len() / 16;
|
||
// Process blocks in reverse to avoid clobbering ciphertext needed for XOR
|
||
for i in (0..num_blocks).rev() {
|
||
let offset = i * 16;
|
||
let prev = if i == 0 {
|
||
AACS_IV
|
||
} else {
|
||
let mut p = [0u8; 16];
|
||
p.copy_from_slice(&data[(i - 1) * 16..i * 16]);
|
||
p
|
||
};
|
||
let mut block = GenericArray::clone_from_slice(&data[offset..offset + 16]);
|
||
cipher.decrypt_block(&mut block);
|
||
for j in 0..16 {
|
||
data[offset + j] = block[j] ^ prev[j];
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Content decryption ──────────────────────────────────────────────────────
|
||
|
||
/// True if a 6144-byte aligned unit is AACS-scrambled on disc.
|
||
///
|
||
/// AACS encrypts the unit body, which destroys the MPEG-TS sync bytes (`0x47`)
|
||
/// a clear unit carries at offsets 4, 196, 388, … (one per 192-byte source
|
||
/// packet). So "scrambled" = "the TS syncs are NOT intact". This is
|
||
/// flag-independent: it does NOT read the TP_extra copy-control bits (byte 0)
|
||
/// or the TS scrambling-control bits (byte 7) — AACS sets neither reliably
|
||
/// across discs/players.
|
||
///
|
||
/// This is the single shared definition of "encrypted" for the whole ecosystem
|
||
/// — libfreemkv's decrypt gate, autorip's sample selection, and the online key
|
||
/// service's validation gate all call THIS, so they always agree on what is
|
||
/// encrypted. A correctly-decrypted (or natively-clear) unit reports `false`,
|
||
/// so the decrypt path never double-decrypts and there is no flag to clear.
|
||
pub fn is_aacs_scrambled(unit: &[u8]) -> bool {
|
||
unit.len() >= ALIGNED_UNIT_LEN && !ts_syncs_intact(unit)
|
||
}
|
||
|
||
/// Count the MPEG-TS sync bytes (`0x47`) present at the BD-TS packet stride
|
||
/// (offset 4 and every 192 bytes after — 4-byte TP_extra_header + 188-byte
|
||
/// TS packet). A clear or correctly-decrypted m2ts unit shows ~one per
|
||
/// packet; an encrypted unit, or a non-content unit decrypted under a key
|
||
/// that doesn't apply, shows ~none.
|
||
pub fn ts_sync_count(unit: &[u8]) -> usize {
|
||
let mut count = 0;
|
||
let mut offset = 4;
|
||
while offset < unit.len() {
|
||
if unit[offset] == TS_SYNC {
|
||
count += 1;
|
||
}
|
||
offset += TS_PACKET_LEN;
|
||
}
|
||
count
|
||
}
|
||
|
||
/// Number of BD-TS packets in the unit — the maximum possible sync count.
|
||
pub fn ts_packet_total(unit: &[u8]) -> usize {
|
||
// One sync byte per 192-byte BD-TS packet (at offset 4 of each). The old
|
||
// `(len - 4) / TS_PACKET_LEN + 1` over-counted by one for lengths of the
|
||
// form `4 + k·192`.
|
||
unit.len() / TS_PACKET_LEN
|
||
}
|
||
|
||
fn ts_syncs_intact(unit: &[u8]) -> bool {
|
||
ts_sync_count(unit) > ts_packet_total(unit) / 2
|
||
}
|
||
|
||
/// Verify a decrypted unit looks like clear MPEG-TS (sync bytes intact).
|
||
fn verify_ts(unit: &[u8]) -> bool {
|
||
ts_syncs_intact(unit)
|
||
}
|
||
|
||
/// Decrypt one AACS aligned unit (6144 bytes) in-place.
|
||
/// Returns true if decryption succeeded (verified by TS sync bytes).
|
||
///
|
||
/// Algorithm:
|
||
/// 1. AES-128-ECB encrypt first 16 bytes with unit_key → derived
|
||
/// 2. XOR derived with original 16 bytes → unit_decrypt_key
|
||
/// 3. AES-128-CBC decrypt bytes 16..6143 with unit_decrypt_key and AACS IV
|
||
///
|
||
/// Decryption restores the TS sync bytes, so the unit reads as clear afterward;
|
||
/// there is no flag to clear.
|
||
pub fn decrypt_unit(unit: &mut [u8], unit_key: &[u8; 16]) -> bool {
|
||
if unit.len() < ALIGNED_UNIT_LEN {
|
||
return false;
|
||
}
|
||
if !is_aacs_scrambled(unit) {
|
||
return true; // not encrypted
|
||
}
|
||
|
||
// Save original first 16 bytes (they're plaintext TP_extra_header)
|
||
let mut header = [0u8; 16];
|
||
header.copy_from_slice(&unit[..16]);
|
||
|
||
// Step 1: Encrypt header with unit key to derive per-unit key
|
||
let derived = aes_ecb_encrypt(unit_key, &header);
|
||
|
||
// Step 2: XOR to get the actual decryption key
|
||
let mut decrypt_key = [0u8; 16];
|
||
for i in 0..16 {
|
||
decrypt_key[i] = derived[i] ^ header[i];
|
||
}
|
||
|
||
// Step 3: Decrypt bytes 16..6143 with AES-CBC
|
||
aes_cbc_decrypt(&decrypt_key, &mut unit[16..ALIGNED_UNIT_LEN]);
|
||
|
||
// Decryption restored the TS syncs; verify the unit now looks like clear TS.
|
||
verify_ts(unit)
|
||
}
|
||
|
||
/// Fast, NON-MUTATING unit-key validation for the brute-force key search.
|
||
///
|
||
/// `decrypt_unit` pays a full 6128-byte (383-block) CBC decrypt before
|
||
/// `verify_ts` can reject a wrong key — but in a brute scan ~every candidate is
|
||
/// wrong. In CBC the plaintext of block *i* is `AES_dec(C_i) XOR C_{i-1}`, so
|
||
/// the FIRST restored TS sync byte (payload offset 196, which lands in CBC
|
||
/// block 11 of the `unit[16..]` region) can be recovered with a SINGLE block
|
||
/// decrypt instead of 383. A wrong key fails this 1-byte gate ~255/256 of the
|
||
/// time for the cost of one AES block; the rare survivor is then confirmed with
|
||
/// the full [`decrypt_unit`], so the set of accepted keys is bit-for-bit
|
||
/// identical to the slow path.
|
||
///
|
||
/// The caller MUST pass an aligned, already-[`is_aacs_scrambled`] unit
|
||
/// (`unit.len() >= ALIGNED_UNIT_LEN`). The brute pre-filters its units, so the
|
||
/// per-candidate scramble re-scan is intentionally skipped here.
|
||
///
|
||
/// NOTE: this is a search accelerator — it never writes the input and never
|
||
/// participates in the content decrypt path. Aggregate correctness (does a key
|
||
/// validate against *any* of the disc's units) is preserved because a true key
|
||
/// restores offset-196 on every standard BD-TS unit.
|
||
pub fn unit_key_validates(unit: &[u8], unit_key: &[u8; 16]) -> bool {
|
||
if unit.len() < ALIGNED_UNIT_LEN {
|
||
return false;
|
||
}
|
||
// Per-unit decrypt key: AES-ECB-encrypt the 16-byte plaintext header with
|
||
// the unit key, XOR with the header (same derivation as `decrypt_unit`).
|
||
let mut header = [0u8; 16];
|
||
header.copy_from_slice(&unit[..16]);
|
||
let derived = aes_ecb_encrypt(unit_key, &header);
|
||
let mut decrypt_key = [0u8; 16];
|
||
for i in 0..16 {
|
||
decrypt_key[i] = derived[i] ^ header[i];
|
||
}
|
||
|
||
// Cheap gate: recover ONLY payload byte 196 (the 2nd BD-TS packet's sync).
|
||
// The CBC region is `unit[16..]`; payload offset 196 → region offset 180 =
|
||
// block 11, byte 4. P[11] = AES_dec(C[11]) XOR C[10]; C[10] is raw
|
||
// ciphertext (no decrypt needed). Constant offsets for the fixed 6144 unit.
|
||
const SYNC_PAYLOAD_OFF: usize = 196;
|
||
let region_off = SYNC_PAYLOAD_OFF - 16; // 180
|
||
let blk = region_off / 16; // 11
|
||
let byte = region_off % 16; // 4
|
||
let c11 = 16 + blk * 16; // absolute offset of C[11] in `unit` (=192)
|
||
let cipher = Aes128::new(GenericArray::from_slice(&decrypt_key));
|
||
let mut b = GenericArray::clone_from_slice(&unit[c11..c11 + 16]);
|
||
cipher.decrypt_block(&mut b);
|
||
let prev = unit[c11 - 16 + byte]; // C[10] byte (region block 10)
|
||
if b[byte] ^ prev != TS_SYNC {
|
||
return false;
|
||
}
|
||
|
||
// Survivor (~1/256 of candidates): confirm with the authoritative full
|
||
// decrypt + verify, so the verdict matches `decrypt_unit` exactly.
|
||
let mut full = [0u8; ALIGNED_UNIT_LEN];
|
||
full.copy_from_slice(&unit[..ALIGNED_UNIT_LEN]);
|
||
decrypt_unit(&mut full, unit_key)
|
||
}
|
||
|
||
/// Outcome of [`decrypt_unit_try_keys`].
|
||
///
|
||
/// Distinguishes "the unit was already clear, no key was consumed" from "key
|
||
/// at index `i` decrypted it" — the bare `Option<usize>` form conflated the two
|
||
/// (a clear unit reported `Some(0)`, indistinguishable from key index 0, and
|
||
/// possibly out of range when `unit_keys` is empty).
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum UnitKeyResult {
|
||
/// The unit was not scrambled; it was left untouched and no key was used.
|
||
AlreadyClear,
|
||
/// The unit was decrypted in place by `unit_keys[index]`.
|
||
DecryptedWith(usize),
|
||
}
|
||
|
||
/// Decrypt one aligned unit trying multiple unit keys.
|
||
///
|
||
/// Returns [`UnitKeyResult::AlreadyClear`] if the unit was not scrambled (no key
|
||
/// consumed), [`UnitKeyResult::DecryptedWith(i)`] if key `i` decrypted it, or
|
||
/// `None` if no key worked (the unit is restored to its original bytes).
|
||
pub fn decrypt_unit_try_keys(unit: &mut [u8], unit_keys: &[[u8; 16]]) -> Option<UnitKeyResult> {
|
||
if !is_aacs_scrambled(unit) {
|
||
return Some(UnitKeyResult::AlreadyClear);
|
||
}
|
||
|
||
// Save original for retry. Stack-backed buffer — no heap allocation, and the
|
||
// restore-on-failure contract holds uniformly regardless of key count.
|
||
let mut original = [0u8; ALIGNED_UNIT_LEN];
|
||
original.copy_from_slice(&unit[..ALIGNED_UNIT_LEN]);
|
||
|
||
for (i, key) in unit_keys.iter().enumerate() {
|
||
unit[..ALIGNED_UNIT_LEN].copy_from_slice(&original);
|
||
if decrypt_unit(unit, key) {
|
||
return Some(UnitKeyResult::DecryptedWith(i));
|
||
}
|
||
}
|
||
|
||
// Restore original on failure
|
||
unit[..ALIGNED_UNIT_LEN].copy_from_slice(&original);
|
||
None
|
||
}
|
||
|
||
/// Remove bus encryption from an aligned unit (AACS 2.0 / UHD).
|
||
/// Bus encryption uses read_data_key, decrypting bytes 16..2047 of each 2048-byte sector.
|
||
pub fn decrypt_bus(unit: &mut [u8], read_data_key: &[u8; 16]) {
|
||
for sector_start in (0..ALIGNED_UNIT_LEN).step_by(SECTOR_LEN) {
|
||
if sector_start + SECTOR_LEN > unit.len() {
|
||
break;
|
||
}
|
||
// First 16 bytes of each sector are plaintext
|
||
aes_cbc_decrypt(
|
||
read_data_key,
|
||
&mut unit[sector_start + 16..sector_start + SECTOR_LEN],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Full decrypt of an aligned unit: bus decrypt (if needed) then AACS decrypt.
|
||
pub fn decrypt_unit_full(
|
||
unit: &mut [u8],
|
||
unit_key: &[u8; 16],
|
||
read_data_key: Option<&[u8; 16]>,
|
||
) -> bool {
|
||
if !is_aacs_scrambled(unit) {
|
||
return true;
|
||
}
|
||
if let Some(rdk) = read_data_key {
|
||
decrypt_bus(unit, rdk);
|
||
}
|
||
decrypt_unit(unit, unit_key)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn test_aes_ecb_roundtrip() {
|
||
let key = [
|
||
0x15u8, 0x66, 0x5F, 0x98, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
|
||
0x0B, 0x0C,
|
||
];
|
||
let plain = [0x41u8; 16];
|
||
let enc = aes_ecb_encrypt(&key, &plain);
|
||
let dec = aes_ecb_decrypt(&key, &enc);
|
||
assert_eq!(dec, plain);
|
||
}
|
||
|
||
#[test]
|
||
fn test_decrypt_unit_unencrypted() {
|
||
// A clear unit (TS syncs intact) is not scrambled → passes through.
|
||
let mut unit = vec![0u8; ALIGNED_UNIT_LEN];
|
||
let mut off = 4;
|
||
while off < ALIGNED_UNIT_LEN {
|
||
unit[off] = TS_SYNC;
|
||
off += TS_PACKET_LEN;
|
||
}
|
||
let key = [0u8; 16];
|
||
assert!(!is_aacs_scrambled(&unit));
|
||
assert!(decrypt_unit(&mut unit, &key));
|
||
}
|
||
|
||
#[test]
|
||
fn ts_packet_total_no_off_by_one() {
|
||
// The maximum sync count is exactly the number of stride
|
||
// positions the counting loop visits (offset 4, 196, ...), i.e.
|
||
// len / 192, NOT (len - 4) / 192 + 1. For the 6144-byte aligned unit
|
||
// the loop checks offsets 4..=5956 → 32 positions.
|
||
let unit = vec![0u8; ALIGNED_UNIT_LEN];
|
||
assert_eq!(ts_packet_total(&unit), 32);
|
||
// Confirm the loop visits exactly that many stride positions.
|
||
let visited = (4..ALIGNED_UNIT_LEN).step_by(TS_PACKET_LEN).count();
|
||
assert_eq!(visited, ts_packet_total(&unit));
|
||
}
|
||
|
||
#[test]
|
||
fn scramble_detection_at_16_32_boundary() {
|
||
// With 32 stride positions the majority threshold is
|
||
// total/2 = 16. A unit with EXACTLY half its syncs intact (16) must
|
||
// NOT be over-counted into the "scrambled" bucket by an inflated
|
||
// total: 16 > 16 is false → not-intact → scrambled. 17 intact → clear.
|
||
// The fix is that `total` is 32 (not 33), so the boundary sits cleanly
|
||
// at the real midpoint.
|
||
let set_syncs = |n: usize| {
|
||
let mut unit = vec![0u8; ALIGNED_UNIT_LEN];
|
||
let mut off = 4;
|
||
let mut placed = 0;
|
||
while off < ALIGNED_UNIT_LEN && placed < n {
|
||
unit[off] = TS_SYNC;
|
||
off += TS_PACKET_LEN;
|
||
placed += 1;
|
||
}
|
||
unit
|
||
};
|
||
|
||
assert_eq!(ts_sync_count(&set_syncs(16)), 16);
|
||
assert_eq!(ts_sync_count(&set_syncs(17)), 17);
|
||
|
||
// Exactly half intact → classified scrambled (16 > 16 is false).
|
||
assert!(is_aacs_scrambled(&set_syncs(16)));
|
||
// One past half → classified clear.
|
||
assert!(!is_aacs_scrambled(&set_syncs(17)));
|
||
}
|
||
|
||
#[test]
|
||
fn scramble_detection_extremes() {
|
||
// Detection semantics for the clear-cut cases must be preserved:
|
||
// a fully-clear unit (all 32 syncs) is NOT scrambled; a unit with no
|
||
// syncs (fully scrambled body) IS scrambled.
|
||
let mut clear = vec![0u8; ALIGNED_UNIT_LEN];
|
||
let mut off = 4;
|
||
while off < ALIGNED_UNIT_LEN {
|
||
clear[off] = TS_SYNC;
|
||
off += TS_PACKET_LEN;
|
||
}
|
||
assert_eq!(ts_sync_count(&clear), 32);
|
||
assert!(
|
||
!is_aacs_scrambled(&clear),
|
||
"fully-clear unit → not scrambled"
|
||
);
|
||
|
||
let scrambled = vec![0u8; ALIGNED_UNIT_LEN];
|
||
assert_eq!(ts_sync_count(&scrambled), 0);
|
||
assert!(is_aacs_scrambled(&scrambled), "no syncs → scrambled");
|
||
}
|
||
|
||
#[test]
|
||
fn test_aes_cbc_roundtrip() {
|
||
let key = [
|
||
0x11u8, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE,
|
||
0xFF, 0x00,
|
||
];
|
||
let original = vec![0x42u8; 128]; // 8 blocks
|
||
let mut data = original.clone();
|
||
|
||
// Encrypt with CBC manually (forward direction)
|
||
fn aes_cbc_encrypt(key: &[u8; 16], data: &mut [u8]) {
|
||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||
let mut prev = super::AACS_IV;
|
||
let num_blocks = data.len() / 16;
|
||
for i in 0..num_blocks {
|
||
let offset = i * 16;
|
||
for j in 0..16 {
|
||
data[offset + j] ^= prev[j];
|
||
}
|
||
let mut block = GenericArray::clone_from_slice(&data[offset..offset + 16]);
|
||
cipher.encrypt_block(&mut block);
|
||
data[offset..offset + 16].copy_from_slice(&block);
|
||
prev.copy_from_slice(&data[offset..offset + 16]);
|
||
}
|
||
}
|
||
|
||
aes_cbc_encrypt(&key, &mut data);
|
||
assert_ne!(data, original); // should be different after encrypt
|
||
|
||
super::aes_cbc_decrypt(&key, &mut data);
|
||
assert_eq!(data, original); // should match after roundtrip
|
||
}
|
||
|
||
#[test]
|
||
fn test_decrypt_unit_synthetic() {
|
||
// Build a fake 6144-byte aligned unit with known TS sync pattern,
|
||
// encrypt it with the AACS algorithm, then decrypt and verify.
|
||
let unit_key = [0xAAu8; 16];
|
||
|
||
// Build plaintext unit with TS sync bytes every 192 bytes starting at offset 4
|
||
let mut plain = vec![0u8; ALIGNED_UNIT_LEN];
|
||
let mut offset = 4;
|
||
while offset < ALIGNED_UNIT_LEN {
|
||
plain[offset] = TS_SYNC;
|
||
offset += TS_PACKET_LEN;
|
||
}
|
||
// No flag set: CBC-encrypting the body below scrambles packets 1..31's
|
||
// TS syncs, which is exactly what `is_aacs_scrambled` (raw-sync) detects.
|
||
|
||
// Now encrypt bytes 16..6143 using the AACS algorithm (reverse of decrypt)
|
||
let header: [u8; 16] = plain[..16].try_into().unwrap();
|
||
let derived = aes_ecb_encrypt(&unit_key, &header);
|
||
let mut encrypt_key = [0u8; 16];
|
||
for i in 0..16 {
|
||
encrypt_key[i] = derived[i] ^ header[i];
|
||
}
|
||
|
||
// CBC encrypt bytes 16..6143
|
||
let cipher = Aes128::new(GenericArray::from_slice(&encrypt_key));
|
||
let mut prev = AACS_IV;
|
||
let num_blocks = (ALIGNED_UNIT_LEN - 16) / 16;
|
||
for i in 0..num_blocks {
|
||
let off = 16 + i * 16;
|
||
for j in 0..16 {
|
||
plain[off + j] ^= prev[j];
|
||
}
|
||
let mut block = GenericArray::clone_from_slice(&plain[off..off + 16]);
|
||
cipher.encrypt_block(&mut block);
|
||
plain[off..off + 16].copy_from_slice(&block);
|
||
prev.copy_from_slice(&plain[off..off + 16]);
|
||
}
|
||
|
||
// Now plain contains encrypted data. Decrypt it.
|
||
let mut unit = plain;
|
||
assert!(is_aacs_scrambled(&unit));
|
||
assert!(decrypt_unit(&mut unit, &unit_key));
|
||
assert!(!is_aacs_scrambled(&unit)); // decrypted: TS syncs restored
|
||
|
||
// Verify TS sync bytes
|
||
let mut count = 0;
|
||
let mut off = 4;
|
||
while off < ALIGNED_UNIT_LEN {
|
||
if unit[off] == TS_SYNC {
|
||
count += 1;
|
||
}
|
||
off += TS_PACKET_LEN;
|
||
}
|
||
// Assert against the single canonical packet count, not the old
|
||
// `(len - 4) / 192 + 1` form that `ts_packet_total` corrected away from.
|
||
assert_eq!(count, ts_packet_total(&unit));
|
||
}
|
||
}
|