libfreemkv 0.31.2: comprehensive spec-grounded test suite (~950 tests)
Test-hardening release, no runtime changes. Adds spec-grounded unit tests across the silent-corruption surfaces — UDF/MPLS/CLPI/IFO parsing, BD/DVD title + extent assembly, AACS/CSS key handling, TS/PS demux + codec parsers, MKV/EBML container output, the mux pipeline, sector prefetch + decrypt decorator, drive/SCSI sense decoding, label extraction, and core I/O. Each test is grounded in the format spec or real on-disc behavior and verified to fail under a targeted source mutation. No behavior changed.
This commit is contained in:
@@ -142,4 +142,178 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// All five tables have exactly the lengths the CSS cipher requires.
|
||||
/// TAB3 is 9-bit-indexed (the LFSR1 low word carries a 9th bit), hence
|
||||
/// 512 entries; every other table is byte-indexed (256). A truncated or
|
||||
/// padded table would index out of bounds or read stale data inside the
|
||||
/// LFSR loops.
|
||||
///
|
||||
/// Grounding: lfsr.rs indexes TAB3 with `*lfsr1_lo as usize` where
|
||||
/// `lfsr1_lo` can be up to 0x1FF (9 bits), so TAB3 MUST be >= 512 long.
|
||||
/// Mutation: change `[u8; 512]` to `[u8; 256]` (drop the second half) ->
|
||||
/// fails to compile / length assert fails.
|
||||
#[test]
|
||||
fn table_lengths_match_css_index_widths() {
|
||||
assert_eq!(TAB1.len(), 256, "TAB1 is byte-indexed");
|
||||
assert_eq!(TAB2.len(), 256, "TAB2 is byte-indexed");
|
||||
assert_eq!(TAB3.len(), 512, "TAB3 is 9-bit-indexed (LFSR1 low word)");
|
||||
assert_eq!(TAB4.len(), 256, "TAB4 is byte-indexed");
|
||||
assert_eq!(TAB5.len(), 256, "TAB5 is byte-indexed");
|
||||
}
|
||||
|
||||
/// TAB1 is a bijection on 0..256. CSS uses it as an invertible output
|
||||
/// permutation in css_DecryptKey's chained-XOR rounds; if two inputs
|
||||
/// collided, the key mangling would not be invertible.
|
||||
///
|
||||
/// Mutation: duplicate any value (e.g. set TAB1[1] = TAB1[0]) -> the
|
||||
/// "maps two inputs" assert fires.
|
||||
#[test]
|
||||
fn tab1_is_a_permutation() {
|
||||
let mut seen = [false; 256];
|
||||
for (i, &v) in TAB1.iter().enumerate() {
|
||||
assert!(
|
||||
!seen[v as usize],
|
||||
"TAB1 maps two inputs to {v:#04x} (collision at index {i:#04x})"
|
||||
);
|
||||
seen[v as usize] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// TAB1's fixed structural anchors from the CSS spec table:
|
||||
/// TAB1[0x00] == 0x33 and the inverse TAB1[0x33] == 0x00. These two
|
||||
/// entries are the canonical first-row / inverse-lookup landmarks of the
|
||||
/// published CSS TAB1 and pin the table's orientation.
|
||||
///
|
||||
/// Grounding: CSS specification TAB1, row 0 col 0 = 0x33; index 0x33
|
||||
/// (row 3 col 3) = 0x00.
|
||||
/// Mutation: change the first literal `0x33` in TAB1 -> first assert fails.
|
||||
#[test]
|
||||
fn tab1_known_spec_anchors() {
|
||||
assert_eq!(TAB1[0x00], 0x33, "TAB1[0] is the published 0x33");
|
||||
assert_eq!(TAB1[0x33], 0x00, "TAB1[0x33] is the published 0x00");
|
||||
}
|
||||
|
||||
/// TAB2 is a permutation of 0..256 (it is the LFSR1 high-byte feedback
|
||||
/// substitution). A non-bijective TAB2 would bias the LFSR1 keystream.
|
||||
///
|
||||
/// Mutation: set TAB2[8] = 0x00 (collides with TAB2[0]) -> assert fires.
|
||||
#[test]
|
||||
fn tab2_is_a_permutation() {
|
||||
let mut seen = [false; 256];
|
||||
for (i, &v) in TAB2.iter().enumerate() {
|
||||
assert!(
|
||||
!seen[v as usize],
|
||||
"TAB2 maps two inputs to {v:#04x} (collision at index {i:#04x})"
|
||||
);
|
||||
seen[v as usize] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// TAB3 is generated by the CSS LFSR1 low-word rule:
|
||||
/// TAB3[i] == BASE[i & 7] ^ (i >> 7)
|
||||
/// where BASE = [0x00,0x24,0x49,0x6d,0x92,0xb6,0xdb,0xff] is the 8-value
|
||||
/// feedback block (BASE[j] is the 9-bit-spread of the 3 high feedback
|
||||
/// bits). The 9-bit index splits into a 3-bit selector (i & 7) and a
|
||||
/// 2-bit carry group (i >> 7) that XORs the base value. This pins all 512
|
||||
/// entries to one closed-form spec rule.
|
||||
///
|
||||
/// Derivation verified offline against the published TAB3 byte layout.
|
||||
/// Mutation: flip any single byte in the TAB3 literal (e.g. the 9th entry
|
||||
/// 0x00 -> 0x01) -> the formula check fails at that index.
|
||||
#[test]
|
||||
fn tab3_matches_lfsr1_generating_formula() {
|
||||
const BASE: [u8; 8] = [0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff];
|
||||
for i in 0..512usize {
|
||||
let expected = BASE[i & 7] ^ ((i >> 7) as u8);
|
||||
assert_eq!(
|
||||
TAB3[i], expected,
|
||||
"TAB3[{i:#05x}] = {:#04x}, formula BASE[i&7]^(i>>7) = {expected:#04x}",
|
||||
TAB3[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// TAB3's value depends only on the bottom 3 bits and the top group:
|
||||
/// within a 128-entry block (constant i>>7) every 8-aligned run repeats.
|
||||
/// Specifically TAB3[i] == TAB3[i & 0x187] (mask keeping bits 0..2 and
|
||||
/// bits 7..8). This is the structural redundancy the generating formula
|
||||
/// implies and a different cross-check on the same data.
|
||||
///
|
||||
/// Mutation: change TAB3[16] (currently a repeat of TAB3[0]=0x00) to
|
||||
/// 0x24 -> the repeat check fails.
|
||||
#[test]
|
||||
fn tab3_repeats_within_block() {
|
||||
for (i, &v) in TAB3.iter().enumerate() {
|
||||
let canonical = (i & 0b1_1000_0111) & 0x1FF;
|
||||
assert_eq!(
|
||||
v, TAB3[canonical],
|
||||
"TAB3[{i:#05x}] should repeat TAB3[{canonical:#05x}]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// TAB4 is the exact bit-reversal of each byte (CSS uses it to permute
|
||||
/// LFSR0 bytes on seed and output). TAB4[b] reverses b's 8 bits MSB<->LSB.
|
||||
/// Therefore it is also an involution: TAB4[TAB4[b]] == b.
|
||||
///
|
||||
/// Grounding: TAB4[0x01]=0x80, TAB4[0x80]=0x01, TAB4[0x00]=0x00,
|
||||
/// TAB4[0xFF]=0xFF.
|
||||
/// Mutation: set TAB4[1] = 0x40 (not the reversal 0x80) -> bit-reversal
|
||||
/// check fails at index 1.
|
||||
#[test]
|
||||
fn tab4_is_exact_bit_reversal_and_involution() {
|
||||
for b in 0u16..256 {
|
||||
let rev = (0..8).fold(0u8, |acc, k| acc | (((b as u8 >> k) & 1) << (7 - k)));
|
||||
assert_eq!(
|
||||
TAB4[b as usize], rev,
|
||||
"TAB4[{b:#04x}] is not the bit-reversal {rev:#04x}"
|
||||
);
|
||||
}
|
||||
for b in 0..256usize {
|
||||
assert_eq!(
|
||||
TAB4[TAB4[b] as usize], b as u8,
|
||||
"TAB4 not an involution at {b:#04x}"
|
||||
);
|
||||
}
|
||||
// Spec landmark entries.
|
||||
assert_eq!(TAB4[0x01], 0x80);
|
||||
assert_eq!(TAB4[0x80], 0x01);
|
||||
assert_eq!(TAB4[0x00], 0x00);
|
||||
assert_eq!(TAB4[0xFF], 0xFF);
|
||||
}
|
||||
|
||||
/// TAB4 is a permutation (bit-reversal is bijective). Distinct from the
|
||||
/// reversal test: a table that is "reversal except two swapped entries"
|
||||
/// would still be a permutation, and a table that is "reversal except one
|
||||
/// duplicated entry" would fail this but might pass a sampled reversal
|
||||
/// check — the two tests pin different failure modes.
|
||||
///
|
||||
/// Mutation: set TAB4[2] = TAB4[1] -> permutation assert fires.
|
||||
#[test]
|
||||
fn tab4_is_a_permutation() {
|
||||
let mut seen = [false; 256];
|
||||
for &v in TAB4.iter() {
|
||||
assert!(!seen[v as usize], "TAB4 maps two inputs to {v:#04x}");
|
||||
seen[v as usize] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// TAB5 is also a permutation (complement of a bijection is a bijection)
|
||||
/// and its own self-consistency landmark: TAB5[0x00] == 0xFF (TAB4[0]^0xFF)
|
||||
/// and TAB5[0xFF] == 0x00 (TAB4[0xFF]^0xFF). Pins orientation independent
|
||||
/// of the complement-loop test.
|
||||
///
|
||||
/// Mutation: change the first TAB5 literal 0xff -> 0xfe -> the landmark
|
||||
/// and permutation checks both catch it.
|
||||
#[test]
|
||||
fn tab5_is_permutation_with_anchors() {
|
||||
let mut seen = [false; 256];
|
||||
for &v in TAB5.iter() {
|
||||
assert!(!seen[v as usize], "TAB5 maps two inputs to {v:#04x}");
|
||||
seen[v as usize] = true;
|
||||
}
|
||||
assert_eq!(TAB5[0x00], 0xFF, "TAB5[0] = TAB4[0]^0xFF = 0xFF");
|
||||
assert_eq!(TAB5[0xFF], 0x00, "TAB5[0xFF] = TAB4[0xFF]^0xFF = 0x00");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user