Route DVD PS demuxer through codec parsers, fix CSS test expectations

- DVD PS path now calls parser.parse() like BD-TS path does
- MPEG-2 sequence headers extracted for codec_private
- Keyframe detection from parser instead of always-true
- Fix CSS roundtrip tests: descramble uses TAB1 permutation, not pure XOR
This commit is contained in:
MattJackson
2026-04-16 04:49:46 +00:00
parent 65c6c8cfe0
commit a1a30bf3c0
2 changed files with 82 additions and 122 deletions
+50 -22
View File
@@ -270,17 +270,29 @@ impl crate::pes::Stream for DiscStream {
.unwrap_or(1), .unwrap_or(1),
_ => continue, _ => continue,
}; };
if track < self.title.streams.len() { if track >= self.title.streams.len() {
let pts_ns = ps continue;
.pts }
.map(|p| (p as i64) * 1_000_000_000 / 90_000) let pid = self
.unwrap_or(0); .pid_to_track
self.pending_frames.push_back(crate::pes::PesFrame { .iter()
track, .find(|(_, idx)| *idx == track)
pts: pts_ns, .map(|(p, _)| *p)
keyframe: true, .unwrap_or(0);
data: ps.data.clone(), let pes = super::ts::PesPacket {
}); pid,
pts: ps.pts.map(|p| p as i64),
dts: ps.dts.map(|d| d as i64),
data: ps.data.clone(),
};
if let Some((_, parser)) =
self.parsers.iter_mut().find(|(p, _)| *p == pid)
{
for frame in parser.parse(&pes) {
self.pending_frames.push_back(
crate::pes::PesFrame::from_codec_frame(track, frame),
);
}
} }
} }
} }
@@ -323,17 +335,33 @@ impl crate::pes::Stream for DiscStream {
.unwrap_or(1), .unwrap_or(1),
_ => continue, _ => continue,
}; };
if track < self.title.streams.len() { if track >= self.title.streams.len() {
let pts_ns = ps continue;
.pts }
.map(|p| (p as i64) * 1_000_000_000 / 90_000)
.unwrap_or(0); // Convert PsPacket to PesPacket for codec parser (same as BD-TS path)
self.pending_frames.push_back(crate::pes::PesFrame { let pid = self
track, .pid_to_track
pts: pts_ns, .iter()
keyframe: true, .find(|(_, idx)| *idx == track)
data: ps.data.clone(), .map(|(p, _)| *p)
}); .unwrap_or(0);
let pes = super::ts::PesPacket {
pid,
pts: ps.pts.map(|p| p as i64),
dts: ps.dts.map(|d| d as i64),
data: ps.data.clone(),
};
if let Some((_, parser)) =
self.parsers.iter_mut().find(|(p, _)| *p == pid)
{
for frame in parser.parse(&pes) {
self.pending_frames.push_back(
crate::pes::PesFrame::from_codec_frame(track, frame),
);
}
} }
} }
} }
+32 -100
View File
@@ -11,51 +11,27 @@ use libfreemkv::css;
// ── CSS Public API Tests ──────────────────────────────────────────────────── // ── CSS Public API Tests ────────────────────────────────────────────────────
/// Test: css_descramble_sector_roundtrip_via_public_api /// Test: css_descramble_sector modifies encrypted region and preserves header.
///
/// The public css::descramble_sector() wraps the LFSR descrambler.
/// Since the cipher is XOR-based, calling descramble twice (with restored
/// flags) should roundtrip the data.
#[test] #[test]
fn css_descramble_sector_roundtrip_via_public_api() { fn css_descramble_sector_roundtrip_via_public_api() {
let state = css::CssState { let state = css::CssState {
title_key: [0x42, 0x13, 0x37, 0xBE, 0xEF], title_key: [0x42, 0x13, 0x37, 0xBE, 0xEF],
}; };
// Build a sector with scramble flag set let mut sector = vec![0xAAu8; 2048];
let mut sector = vec![0x00u8; 2048]; sector[0x14] = 0x30;
sector[0x14] = 0x30; // scramble flag sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]);
sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]); // seed
// PES header at byte 128
sector[0x80] = 0x00;
sector[0x81] = 0x00;
sector[0x82] = 0x01;
sector[0x83] = 0xE0;
// Fill content
for (i, byte) in sector.iter_mut().enumerate().take(2048).skip(0x84) {
*byte = (i & 0xFF) as u8;
}
let original = sector.clone(); let original = sector.clone();
// First descramble
css::descramble_sector(&state, &mut sector); css::descramble_sector(&state, &mut sector);
assert_eq!(sector[0x14] & 0x30, 0x00, "flag not cleared"); assert_eq!(sector[0x14] & 0x30, 0x00, "flag not cleared");
assert_ne!( // Header preserved (except flag byte)
&sector[0x80..0x84], for i in 0..128 {
&original[0x80..0x84], if i == 0x14 { continue; }
"content unchanged" assert_eq!(sector[i], original[i], "header byte {} changed", i);
); }
// Encrypted region modified
// Restore flag for second pass assert_ne!(&sector[128..256], &original[128..256]);
sector[0x14] = 0x30;
// Second descramble = roundtrip
css::descramble_sector(&state, &mut sector);
assert_eq!(
&sector[0x80..2048],
&original[0x80..2048],
"double descramble did not roundtrip"
);
} }
/// Test: css_is_scrambled detects scramble flags correctly. /// Test: css_is_scrambled detects scramble flags correctly.
@@ -508,98 +484,54 @@ fn aacs_bus_decrypt_cross_validation() {
// ── CSS roundtrip test vectors ───────────────────────────────────────────── // ── CSS roundtrip test vectors ─────────────────────────────────────────────
/// CSS descramble is XOR-based: applying it twice with restored scramble /// CSS descramble modifies encrypted region deterministically.
/// flag must recover the original plaintext. This test uses a structured
/// MPEG-2 sector and stores a snapshot of the intermediate ciphertext to
/// catch any regressions in the cipher implementation.
#[test] #[test]
fn css_roundtrip_with_snapshot() { fn css_roundtrip_with_snapshot() {
let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF]; let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF];
let seed: [u8; 5] = [0xDE, 0xAD, 0xBE, 0xEF, 0x42];
let mut sector = vec![0x00u8; 2048]; let mut sector = vec![0x00u8; 2048];
sector[0] = 0x00;
sector[1] = 0x00;
sector[2] = 0x01;
sector[3] = 0xBA;
sector[0x14] = 0x30; sector[0x14] = 0x30;
sector[0x54..0x59].copy_from_slice(&seed); sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]);
sector[0x80] = 0x00; for i in 0x80..2048 {
sector[0x81] = 0x00;
sector[0x82] = 0x01;
sector[0x83] = 0xE0;
sector[0x84] = 0x07;
sector[0x85] = 0xEC;
sector[0x86] = 0x80;
sector[0x87] = 0x80;
sector[0x88] = 0x05;
sector[0x89] = 0x21;
for i in 0x8A..2048 {
sector[i] = ((i * 7 + 3) & 0xFF) as u8; sector[i] = ((i * 7 + 3) & 0xFF) as u8;
} }
let original = sector.clone(); let original = sector.clone();
// First descramble = "encrypt" via XOR
css::lfsr::descramble_sector(&title_key, &mut sector); css::lfsr::descramble_sector(&title_key, &mut sector);
// Snapshot the first 32 bytes of the encrypted region for regression
let snapshot: Vec<u8> = sector[0x80..0xA0].to_vec();
assert_eq!(snapshot.len(), 32);
assert_eq!(sector[0x14] & 0x30, 0x00, "flag not cleared"); assert_eq!(sector[0x14] & 0x30, 0x00, "flag not cleared");
assert_ne!(&sector[0x80..0xA0], &original[0x80..0xA0]); assert_ne!(&sector[0x80..0xA0], &original[0x80..0xA0]);
// Restore scramble flag and roundtrip // Determinism: same input → same output
sector[0x14] = 0x30; let mut sector2 = original.clone();
css::lfsr::descramble_sector(&title_key, &mut sector); css::lfsr::descramble_sector(&title_key, &mut sector2);
assert_eq!( assert_eq!(&sector[0x80..2048], &sector2[0x80..2048], "not deterministic");
&sector[0x80..2048],
&original[0x80..2048],
"CSS roundtrip failed"
);
} }
/// Multiple key/seed combinations to exercise different LFSR states. /// Multiple key/seed combinations produce different outputs.
#[test] #[test]
fn css_roundtrip_multiple_keys() { fn css_roundtrip_multiple_keys() {
let cases: &[([u8; 5], [u8; 5])] = &[ let cases: &[([u8; 5], [u8; 5])] = &[
( ([0x00, 0x00, 0x00, 0x00, 0x00], [0x00, 0x00, 0x00, 0x00, 0x00]),
[0x00, 0x00, 0x00, 0x00, 0x00], ([0xFF, 0xFF, 0xFF, 0xFF, 0xFF], [0xFF, 0xFF, 0xFF, 0xFF, 0xFF]),
[0x00, 0x00, 0x00, 0x00, 0x00], ([0x01, 0x02, 0x03, 0x04, 0x05], [0xAA, 0xBB, 0xCC, 0xDD, 0xEE]),
), ([0xAB, 0xCD, 0xEF, 0x01, 0x23], [0x12, 0x34, 0x56, 0x78, 0x9A]),
(
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
),
(
[0x01, 0x02, 0x03, 0x04, 0x05],
[0xAA, 0xBB, 0xCC, 0xDD, 0xEE],
),
(
[0xAB, 0xCD, 0xEF, 0x01, 0x23],
[0x12, 0x34, 0x56, 0x78, 0x9A],
),
]; ];
let mut results = Vec::new();
for (idx, (key, seed)) in cases.iter().enumerate() { for (idx, (key, seed)) in cases.iter().enumerate() {
let mut sector = vec![0x00u8; 2048]; let mut sector = vec![0xAAu8; 2048];
sector[0x14] = 0x30; sector[0x14] = 0x30;
sector[0x54..0x59].copy_from_slice(seed); sector[0x54..0x59].copy_from_slice(seed);
for i in 0x80..2048 {
sector[i] = ((i + idx) & 0xFF) as u8;
}
let original = sector.clone();
css::lfsr::descramble_sector(key, &mut sector); css::lfsr::descramble_sector(key, &mut sector);
assert_eq!(sector[0x14] & 0x30, 0x00, "case {}: flag not cleared", idx); assert_eq!(sector[0x14] & 0x30, 0x00, "case {}: flag not cleared", idx);
results.push(sector[0x80..0xA0].to_vec());
sector[0x14] = 0x30; }
css::lfsr::descramble_sector(key, &mut sector); // Different keys/seeds should produce different outputs
assert_eq!( for i in 0..results.len() {
&sector[0x80..2048], for j in (i+1)..results.len() {
&original[0x80..2048], assert_ne!(results[i], results[j], "cases {} and {} produced same output", i, j);
"case {}: roundtrip failed", }
idx
);
} }
} }