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:
+46
-18
@@ -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
|
||||||
|
.pid_to_track
|
||||||
|
.iter()
|
||||||
|
.find(|(_, idx)| *idx == track)
|
||||||
|
.map(|(p, _)| *p)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
self.pending_frames.push_back(crate::pes::PesFrame {
|
let pes = super::ts::PesPacket {
|
||||||
track,
|
pid,
|
||||||
pts: pts_ns,
|
pts: ps.pts.map(|p| p as i64),
|
||||||
keyframe: true,
|
dts: ps.dts.map(|d| d as i64),
|
||||||
data: ps.data.clone(),
|
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)
|
|
||||||
|
// Convert PsPacket to PesPacket for codec parser (same as BD-TS path)
|
||||||
|
let pid = self
|
||||||
|
.pid_to_track
|
||||||
|
.iter()
|
||||||
|
.find(|(_, idx)| *idx == track)
|
||||||
|
.map(|(p, _)| *p)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
self.pending_frames.push_back(crate::pes::PesFrame {
|
|
||||||
track,
|
let pes = super::ts::PesPacket {
|
||||||
pts: pts_ns,
|
pid,
|
||||||
keyframe: true,
|
pts: ps.pts.map(|p| p as i64),
|
||||||
|
dts: ps.dts.map(|d| d as i64),
|
||||||
data: ps.data.clone(),
|
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
@@ -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)
|
||||||
§or[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!(§or[128..256], &original[128..256]);
|
||||||
sector[0x14] = 0x30;
|
|
||||||
|
|
||||||
// Second descramble = roundtrip
|
|
||||||
css::descramble_sector(&state, &mut sector);
|
|
||||||
assert_eq!(
|
|
||||||
§or[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!(§or[0x80..0xA0], &original[0x80..0xA0]);
|
assert_ne!(§or[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!(§or[0x80..2048], §or2[0x80..2048], "not deterministic");
|
||||||
§or[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() {
|
||||||
§or[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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user