Compare commits

...
3 Commits
Author SHA1 Message Date
Matthew Jackson 41a6d89cd1 libfreemkv 0.31.5: do not force monotonic block timestamps on video
B-frame video PTS is legitimately non-monotonic in decode/storage order; the
audio-oriented monotonic nudge was clobbering it to prev+1ms, which decoders
flagged as non-monotonic DTS (thousands per title). Apply the nudge to
audio/subtitle only; video keeps its true PES PTS. + regression test.
2026-06-08 09:00:35 -07:00
Matthew Jackson f79c2a0aa9 libfreemkv 0.31.4: prune 144 vacuous tests (keep spec-grounded subset) 2026-06-08 07:28:55 -07:00
Matthew Jackson d181362460 libfreemkv 0.31.3: use RFC 5737 doc address in network URL test fixture 2026-06-07 22:42:09 -07:00
51 changed files with 102 additions and 2150 deletions
+26
View File
@@ -1,5 +1,31 @@
# Changelog
## 0.31.5 (2026-06-08)
### Fixed
- MKV mux: stop forcing strictly-monotonic block timestamps on the **video**
track. The monotonic nudge (added for audio PES that collide on a
millisecond) was clobbering B-frame video PTS — which are legitimately
non-monotonic in decode/storage order — to prev+1ms. A `copy` remux
preserved the wrong value, but decoding derived DTS from the HEVC POC and
found them colliding, emitting thousands of "non monotonically increasing
dts" warnings per title. Video now keeps its true PES PTS (Matroska
SimpleBlock permits non-monotonic block timestamps); audio/subtitle tracks
still get the nudge. Verified: a re-mux drops the warning count to zero.
## 0.31.4 (2026-06-08)
Test cleanup — no runtime changes. Removed 144 unit tests flagged in adversarial
review as vacuous (assert-current-behavior / duplicate / non-failing), leaving
the spec-grounded, mutation-verified subset. 1927 tests remain, all green.
## 0.31.3 (2026-06-08)
Test fixture hygiene — no runtime changes. Replaced a private-range example
address in a `network://` URL test with an RFC 5737 documentation address so
the public-repo leak-guard stays green.
## 0.31.2 (2026-06-08)
Test-hardening release — no runtime changes. Adds a comprehensive,
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "libfreemkv"
version = "0.31.2"
version = "0.31.5"
edition = "2024"
rust-version = "1.86"
license = "AGPL-3.0-only"
-11
View File
@@ -766,17 +766,6 @@ mod tests {
}
}
#[test]
fn decrypt_bus_processes_all_three_sectors() {
// 6144 / 2048 = 3 sectors. Confirm the loop covers all three: corrupt
// the body of sector 2 (the last) and confirm decrypt_bus touches it
// (i.e. it isn't skipped). We do this by checking that round-tripping
// only works when all three are processed — encrypt all 3, decrypt,
// expect full recovery (covered above); here assert the step count.
let starts: Vec<usize> = (0..ALIGNED_UNIT_LEN).step_by(SECTOR_LEN).collect();
assert_eq!(starts, vec![0, 2048, 4096]);
}
// ── decrypt_unit_full: bus-then-AACS ordering, and clear passthrough ───
#[test]
-33
View File
@@ -1184,24 +1184,6 @@ mod tests {
}
}
/// EP-map num_streams == 0 → empty maps (explicit guard). A CPI whose
/// EP map header declares zero stream PID entries carries no EP data.
#[test]
fn ep_map_zero_streams_yields_empty() {
let mut ep_map = Vec::new();
ep_map.push(0); // reserved
ep_map.push(0); // num_streams = 0
ep_map.extend_from_slice(&[0u8; 16]); // filler so len checks pass
let mut cpi = Vec::new();
cpi.extend_from_slice(&((2 + ep_map.len()) as u32).to_be_bytes());
cpi.extend_from_slice(&[0u8; 2]);
cpi.extend_from_slice(&ep_map);
let data = build_clpi(1000, Some(&cpi));
let clip = parse(&data).expect("should parse");
assert!(clip.ep_coarse.is_empty());
assert!(clip.ep_fine.is_empty());
}
/// ep_map_offset that points past the EP map (`ep_map_offset + 4 >
/// ep_map.len()`) → empty maps (bounds guard), not panic. Patch the
/// EP_map_start field to a huge value.
@@ -1271,21 +1253,6 @@ mod tests {
assert_eq!(clip.ep_coarse[0].pts_coarse, 10);
}
/// CLPI between 40 and 60 bytes: passes the len<40 guard, but
/// source_packet_count needs [56..60]. Must yield 0, not panic.
/// (Mirrors parse_truncated_clipinfo_no_panic but asserts EP empty.)
#[test]
fn clipinfo_40_to_60_bytes_empty_ep() {
for len in 40..60usize {
let mut data = vec![0u8; len];
data[0..4].copy_from_slice(b"HDMV");
let clip = parse(&data).expect("short CLPI parses");
assert_eq!(clip.source_packet_count, 0);
assert!(clip.ep_coarse.is_empty());
assert!(clip.streams.is_empty());
}
}
/// resolved_ep_map: the LAST coarse group's fine range extends to
/// ep_fine.len() (no "next coarse" bound). Verify all trailing fine
/// entries are assigned to the final coarse group.
-36
View File
@@ -593,42 +593,6 @@ mod tests {
// ── CSS constant-table integrity ───────────────────────────────────────
/// The CSSCryptKey lookup tables are each a full 256-entry byte table and
/// the variant tables are 32 entries (one per CSS variant). The cipher
/// indexes CRYPT_TAB0..3 with arbitrary bytes (0..256) and indexes
/// VARIANTS / PERM_VARIANT with the css_variant (0..32). A short table
/// would index out of bounds.
///
/// Grounding: crypt_key indexes `CRYPT_TABx[idx]` where idx is a u8 cast
/// to usize (0..256); `VARIANTS[css_variant]` and `PERM_VARIANT[k][variant]`
/// with variant 0..32.
/// Mutation: drop the last entry of CRYPT_TAB0 (make it [u8;255]) ->
/// compile error / length assert fails.
#[test]
fn crypt_tables_have_spec_lengths() {
assert_eq!(CRYPT_TAB0.len(), 256);
assert_eq!(CRYPT_TAB1.len(), 256);
assert_eq!(CRYPT_TAB2.len(), 256);
assert_eq!(CRYPT_TAB3.len(), 256);
assert_eq!(VARIANTS.len(), 32, "one CSS variant byte per variant 0..32");
assert_eq!(PERM_VARIANT.len(), 2);
assert_eq!(PERM_VARIANT[0].len(), 32);
assert_eq!(PERM_VARIANT[1].len(), 32);
assert_eq!(
PERM_CHALLENGE.len(),
3,
"one challenge perm per key_type 0..3"
);
for p in &PERM_CHALLENGE {
assert_eq!(
p.len(),
10,
"challenge permutation covers all 10 challenge bytes"
);
}
assert_eq!(SECRET.len(), 5);
}
/// Each PERM_CHALLENGE row is a permutation of indices 0..10 (it reorders
/// the 10 challenge bytes). A non-permutation would drop/duplicate
/// challenge bytes, weakening or corrupting the bus key derivation.
-46
View File
@@ -378,31 +378,6 @@ mod tests {
}
}
/// recover_title_key with exactly 10 plaintext bytes is accepted at the
/// length guard (it may still return None from the attack, but must not be
/// rejected by the `plain.len() < 10` check). Pins the boundary at the
/// inclusive value 10.
///
/// Grounding: `if sector.len() < SECTOR_SIZE || plain.len() < 10 { None }`
/// — 10 is the minimum accepted length.
/// Mutation: change `< 10` to `< 11` -> a 10-byte plaintext would be
/// rejected. We detect acceptance by observing the function runs the
/// attack (it returns None for this synthetic data, but a 9-byte plain
/// returns None *at the guard*; to distinguish, we assert a 9-byte input
/// is rejected and a 10-byte input is not panicking and consistent).
#[test]
fn recover_accepts_exactly_10_plain_bytes() {
let mut sector = vec![0x00u8; SECTOR_SIZE];
sector[FLAG_BYTE] = 0x30;
sector[SEED_OFFSET..SEED_OFFSET + 5].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55]);
let plain9 = [0u8; 9];
let plain10 = [0u8; 10];
// 9 bytes: rejected at the guard.
assert!(recover_title_key(&sector, &plain9).is_none());
// 10 bytes: passes the guard and runs to completion without panic.
let _ = recover_title_key(&sector, &plain10);
}
// ── crack_title_key early-return guards (flag uses bits 4-5) ────────────
/// crack_title_key uses the same bits-4-5 scramble field. A sector with
@@ -466,27 +441,6 @@ mod tests {
}
}
/// recover_title_key, when it DOES return a key, XORs the sector seed into
/// the recovered raw key (the final step `result_key[i] ^= seed[i]`).
/// We cannot easily force a hit on this crate's (non-functional) attack,
/// so instead pin the structural guard that the seed is read from the
/// documented offset 0x54..0x59 and that a None result is returned for an
/// all-zero scrambled sector (the search exhausts without a match rather
/// than panicking on the seed XOR).
///
/// Grounding: SEED_OFFSET == 0x54; seed slice is `sector[0x54..0x59]`.
/// Mutation: change SEED_OFFSET to 0x55 -> the seed slice shifts; the
/// search still completes (None) but on a functional path the recovered
/// key would be wrong. This test pins the no-panic completion only.
#[test]
fn recover_all_zero_scrambled_completes_none() {
let mut sector = vec![0x00u8; SECTOR_SIZE];
sector[FLAG_BYTE] = 0x30;
let plain = [0x00u8, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
// All-zero body: the textbook attack finds no consistent state.
assert!(recover_title_key(&sector, &plain).is_none());
}
/// Build a scrambled sector with known plaintext (both an MPEG PES header
/// at 0x80 and an exact-plaintext probe), then assert that the Stevenson
/// recovery actually recovers a key whose descramble round-trips the body.
-54
View File
@@ -595,60 +595,6 @@ mod tests {
}
}
/// seed_lfsr0 applies the per-byte TAB4 bit-reversal to the 4 bytes of the
/// packed LFSR0 seed value. The seeding expression for the all-zero key is
/// `(0<<17)|(0<<9)|((0<<1)+8-(0&7)) == 8`, so the raw lfsr0 = 0x00000008.
/// Each byte is then TAB4-reversed and re-packed big-endian-ish per the
/// code. Byte (lfsr0 & 0xFF) == 0x08 -> TAB4[0x08] == 0x10 placed in the
/// top byte (<<24). The other three source bytes are 0 -> TAB4[0]=0. So
/// the seed for an all-zero key must be 0x10 << 24 == 0x10000000.
///
/// Grounding: seed_lfsr0 body + TAB4[0x08] = bit-reverse(0x08=0b00001000)
/// = 0b00010000 = 0x10.
/// Mutation: change the `<< 24` on the first TAB4 term to `<< 16` -> the
/// expected seed changes and the round-trip-anchored value below fails.
#[test]
fn seed_lfsr0_zero_key_matches_spec_packing() {
// We cannot call seed_lfsr0 directly (private), but decrypt_key seeds
// LFSR0 with it. Instead pin the documented TAB4 anchor the seed
// relies on, plus the algebraic seed value, so a regression in either
// the packing constant or TAB4 is caught.
assert_eq!(
TAB4[0x08], 0x10,
"bit-reverse(0x08) == 0x10 drives the zero-key seed"
);
// Algebraic check of the raw (pre-TAB4) seed for an all-zero key.
let key = [0u8; 5];
let raw = ((key[4] as u32) << 17)
| ((key[3] as u32) << 9)
| (((key[2] as u32) << 1) + 8 - (key[2] as u32 & 7));
assert_eq!(
raw, 8,
"all-zero key packs to raw LFSR0 seed 8 per the CSS formula"
);
}
/// decrypt_key never panics and always returns exactly 5 bytes across the
/// full single-byte input space for both invert values. This is the
/// "never panic / never truncate" property for the key-mangling core.
///
/// Grounding: return type is [u8; 5]; all table indexes are masked to byte
/// range inside css_step.
/// Mutation: (sanity) it is a type-level guarantee; the loop also exercises
/// every TAB1 index 0..256 via p_crypted, catching an out-of-range index
/// if a table were shortened.
#[test]
fn decrypt_key_total_over_byte_space() {
for invert in [0x00u8, 0xFF] {
for b in 0u16..256 {
let key = [b as u8; 5];
let crypted = [b as u8, 0, 255, b as u8, 0];
let out = decrypt_key(invert, &key, &crypted);
let _ = out; // length is [u8;5] by type; the call must not panic.
}
}
}
/// The invert byte (0x00 vs 0xFF) selects the LFSR0 output index in
/// css_step via `TAB4[(o_lfsr0 ^ invert) as usize]`. For a non-degenerate
/// key it must change the keystream and hence the result. (Pins that the
-23
View File
@@ -381,27 +381,4 @@ mod tests {
assert!(res.is_none());
assert_eq!(src.reads.borrow().len(), 0);
}
/// crack_key only invokes the (expensive) per-sector cracker on SCRAMBLED
/// sectors. Clear sectors are scanned (counted) but never cracked, so a
/// long run of clear sectors returns None after exhausting the extent
/// rather than producing a spurious key. This pins the `is_scrambled(&buf)`
/// gate.
///
/// Grounding: `if read.is_ok() && is_scrambled(&buf) { crack::... }`.
/// Mutation: drop the `&& is_scrambled(&buf)` gate -> crack runs the
/// 169-pattern Stevenson attack on every clear sector. Functionally this
/// would still return None for our zeroed data, but it would be vastly
/// slower; we cannot time it deterministically, so this test primarily
/// documents the contract and confirms a clear scan terminates with None.
#[test]
fn crack_key_clear_sectors_yield_none() {
let mut src = MockSource::new(0x00);
let extents = [Extent {
start_lba: 0,
sector_count: 100,
}];
assert!(crack_key(&mut src, &extents).is_none());
assert_eq!(src.reads.borrow().len(), 100);
}
}
-38
View File
@@ -143,25 +143,6 @@ 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.
@@ -234,25 +215,6 @@ mod tests {
}
}
/// 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.
-28
View File
@@ -157,11 +157,6 @@ mod tests {
assert_eq!(mask_string("café9"), "AAAé0");
}
#[test]
fn mask_string_empty_is_empty() {
assert_eq!(mask_string(""), "");
}
#[test]
fn mask_bytes_matches_string_masking_for_ascii() {
// mask_bytes is the byte-wise analogue: letters→b'A', digits→b'0'.
@@ -178,19 +173,6 @@ mod tests {
assert_eq!(mask_bytes(&input), vec![0x00, b'A', 0x20, b'0', 0xFF, b'-']);
}
#[test]
fn mask_bytes_length_preserved() {
// Masking is 1:1 — output length always equals input length so
// fixed-offset fields stay aligned.
let input = vec![0u8; 96];
assert_eq!(mask_bytes(&input).len(), 96);
}
#[test]
fn mask_bytes_empty_is_empty() {
assert!(mask_bytes(&[]).is_empty());
}
#[test]
fn feature_table_has_no_duplicate_codes() {
// capture_drive_data iterates FEATURES once per code; a duplicate
@@ -211,14 +193,4 @@ mod tests {
"AACS feature 0x010D must be captured"
);
}
#[test]
fn feature_table_codes_are_sorted_ascending() {
// The table is maintained in ascending MMC-6 code order; a code
// inserted out of order is a maintenance smell that this pins.
let codes: Vec<u16> = FEATURES.iter().map(|&(c, _)| c).collect();
let mut sorted = codes.clone();
sorted.sort_unstable();
assert_eq!(codes, sorted, "FEATURES must stay in ascending code order");
}
}
-20
View File
@@ -1341,17 +1341,6 @@ mod command_tests {
assert_eq!(d.get_config_feature(0x0000), None);
}
#[test]
fn get_config_feature_clamps_overlong_transfer_count() {
// Doc: a bridge reporting more bytes than the 256-byte buffer
// must be clamped (end = bytes_transferred.min(buf.len())) — no
// slice panic. FixedTransport reports min(payload,buf)=256 here,
// so we get buf[8..256] = 248 bytes, never a panic.
let mut d = drive_with(vec![0xAB; 1024]);
let got = d.get_config_feature(0x010C).unwrap();
assert_eq!(got.len(), 256 - 8, "clamped to buffer, header stripped");
}
// ── report_key / mode_sense / read_buffer empty-vs-some ─────────
#[test]
@@ -1408,15 +1397,6 @@ mod command_tests {
));
}
#[test]
fn ready_predicates_false_without_driver() {
// is_ready / is_unlocked default false when no platform driver.
let d = drive_with(vec![]);
assert!(!d.is_ready());
assert!(!d.is_unlocked());
assert!(!d.has_profile());
}
// ── decode_read_capacity additional boundaries ──────────────────
#[test]
-24
View File
@@ -1455,28 +1455,4 @@ mod tests {
let expected = format!("E{}: 5/3", E_MUX_TRACK_RANGE);
assert_eq!(e.to_string(), expected);
}
/// All keydb error codes are 8xxx.
/// Mutation: creating E_KEYDB_PARSE = 9004 (colliding with E_KEYDB_LOAD range)
/// breaks the CLI's range-based keydb error dispatch.
#[test]
fn keydb_error_codes_all_in_8xxx_range() {
let keydb_codes = [
E_KEYDB_CONNECT,
E_KEYDB_HTTP,
E_KEYDB_INVALID,
E_KEYDB_WRITE,
E_KEYDB_PARSE,
E_KEYDB_LOAD,
E_KEYDB_UNSUPPORTED_SCHEME,
E_KEYDB_TOO_MANY_REDIRECTS,
];
for code in keydb_codes {
assert!(
(8000..9000).contains(&code),
"keydb code {} must be in 8xxx range",
code
);
}
}
}
-189
View File
@@ -127,41 +127,6 @@ pub fn ignore(_event: Event) {}
mod tests {
use super::*;
/// EventKind::BytesRead carries bytes and total as u64.
/// Mutation: making `bytes` a u32 silently truncates progress on large discs (>4 GiB).
#[test]
fn bytes_read_fields_are_u64() {
// A 4K UHD disc is ~100 GiB — bytes must be u64 to hold it.
let event = Event {
kind: EventKind::BytesRead {
bytes: u64::MAX,
total: u64::MAX,
},
};
match event.kind {
EventKind::BytesRead { bytes, total } => {
assert_eq!(bytes, u64::MAX);
assert_eq!(total, u64::MAX);
}
_ => panic!("wrong variant"),
}
}
/// EventKind::SectorSkipped carries a u64 sector number.
/// Mutation: using u32 truncates LBAs > 4 GiB sectors (large BD-R discs).
#[test]
fn sector_skipped_field_is_u64() {
let event = Event {
kind: EventKind::SectorSkipped { sector: u64::MAX },
};
match event.kind {
EventKind::SectorSkipped { sector } => {
assert_eq!(sector, u64::MAX);
}
_ => panic!("wrong variant"),
}
}
/// BatchSizeReason::Shrunk != BatchSizeReason::Probed.
/// These two variants carry distinct meanings (error vs. recovery); they
/// must not compare as equal.
@@ -172,14 +137,6 @@ mod tests {
assert_ne!(BatchSizeReason::Shrunk, BatchSizeReason::Probed);
}
/// BatchSizeReason::Shrunk == BatchSizeReason::Shrunk (reflexive equality).
/// Mutation: a broken PartialEq impl that always returns false would fail this.
#[test]
fn batch_size_reason_eq_is_reflexive() {
assert_eq!(BatchSizeReason::Shrunk, BatchSizeReason::Shrunk);
assert_eq!(BatchSizeReason::Probed, BatchSizeReason::Probed);
}
/// BatchSizeReason is Clone + Copy: cloning does not move the original.
/// This is required because EventKind::BatchSizeChanged embeds it by value.
/// Mutation: removing Copy would require the caller to clone explicitly;
@@ -190,150 +147,4 @@ mod tests {
let _r2 = r; // copy, not move
let _r3 = r; // r still usable after copy
}
/// EventKind::BatchSizeChanged can be constructed and destructured.
/// Mutation: renaming the `reason` field to `cause` breaks all pattern-matches.
#[test]
fn batch_size_changed_constructs_and_destructs() {
let event = Event {
kind: EventKind::BatchSizeChanged {
new_size: 32,
reason: BatchSizeReason::Shrunk,
},
};
match event.kind {
EventKind::BatchSizeChanged { new_size, reason } => {
assert_eq!(new_size, 32);
assert_eq!(reason, BatchSizeReason::Shrunk);
}
_ => panic!("wrong variant"),
}
}
/// EventKind::ExtentStart carries all three fields at u64.
/// Mutation: making start_sector a u32 truncates large-disc LBAs.
#[test]
fn extent_start_fields_are_correct_types() {
let event = Event {
kind: EventKind::ExtentStart {
index: 0,
start_sector: u64::MAX,
sector_count: u64::MAX,
},
};
match event.kind {
EventKind::ExtentStart {
index,
start_sector,
sector_count,
} => {
assert_eq!(index, 0);
assert_eq!(start_sector, u64::MAX);
assert_eq!(sector_count, u64::MAX);
}
_ => panic!("wrong variant"),
}
}
/// EventKind::Complete carries bytes (u64) and errors (u32).
/// Mutation: making bytes a u32 truncates total-bytes-written on large outputs.
#[test]
fn complete_fields_are_correct_types() {
let event = Event {
kind: EventKind::Complete {
bytes: u64::MAX,
errors: u32::MAX,
},
};
match event.kind {
EventKind::Complete { bytes, errors } => {
assert_eq!(bytes, u64::MAX);
assert_eq!(errors, u32::MAX);
}
_ => panic!("wrong variant"),
}
}
/// ignore() accepts any Event variant without panicking.
/// This is trivially true but ensures the function signature matches all
/// EventKind variants (would fail to compile if a new variant is added
/// without updating the function or the test).
/// Mutation: making ignore() generic over a wrong type causes a compile error.
#[test]
fn ignore_accepts_any_event() {
ignore(Event {
kind: EventKind::DriveReady,
});
ignore(Event {
kind: EventKind::BytesRead { bytes: 0, total: 0 },
});
ignore(Event {
kind: EventKind::SectorSkipped { sector: 0 },
});
ignore(Event {
kind: EventKind::Complete {
bytes: 0,
errors: 0,
},
});
ignore(Event {
kind: EventKind::BatchSizeChanged {
new_size: 16,
reason: BatchSizeReason::Probed,
},
});
}
/// EventKind::SpeedChange carries speed_kbs as u16.
/// Spec: 0xFFFF is the sentinel meaning "max speed" (value from CD-ROM MMC spec).
/// Mutation: using u8 for speed_kbs truncates values > 255 to 0 or wrong values.
#[test]
fn speed_change_sentinel_max_is_0xffff() {
let event = Event {
kind: EventKind::SpeedChange { speed_kbs: 0xFFFF },
};
match event.kind {
EventKind::SpeedChange { speed_kbs } => {
assert_eq!(
speed_kbs, 0xFFFF,
"0xFFFF is the max-speed sentinel (MMC spec)"
);
}
_ => panic!("wrong variant"),
}
}
/// EventKind::ReadError carries an error with a sector field.
/// Mutation: using i64 for sector would allow negative sector values (nonsensical).
#[test]
fn read_error_carries_error_and_sector() {
use crate::error::Error;
let event = Event {
kind: EventKind::ReadError {
sector: 99_999,
error: Error::Halted,
},
};
match event.kind {
EventKind::ReadError { sector, .. } => {
assert_eq!(sector, 99_999u64);
}
_ => panic!("wrong variant"),
}
}
/// EventKind::Retry carries attempt as u32 (1-based).
/// Mutation: u8 for attempt overflows after 255 retries without warning.
#[test]
fn retry_attempt_is_u32() {
let event = Event {
kind: EventKind::Retry { attempt: u32::MAX },
};
match event.kind {
EventKind::Retry { attempt } => {
assert_eq!(attempt, u32::MAX);
}
_ => panic!("wrong variant"),
}
}
}
-80
View File
@@ -177,73 +177,6 @@ mod tests {
// ── New comprehensive tests ────────────────────────────────────────────────
/// Default::default() produces a fresh, uncancelled token (same as new()).
/// Spec: doc says "The Default impl forwards to new() — both produce a fresh,
/// uncancelled token."
/// Mutation: having Default initialize to `cancelled=true` would break all
/// callers that rely on a default-constructed Halt being uncancelled.
#[test]
fn default_produces_uncancelled_token() {
let h = Halt::default();
assert!(
!h.is_cancelled(),
"Default::default() must produce uncancelled token"
);
}
/// Multiple clones of the same Halt all observe a cancel from any one of them.
/// Mutation: cloning the Arc by value (separate allocation) means clones don't share state.
#[test]
fn multiple_clones_all_share_same_flag() {
let h0 = Halt::new();
let h1 = h0.clone();
let h2 = h0.clone();
let h3 = h1.clone();
// None cancelled yet.
assert!(!h0.is_cancelled());
assert!(!h1.is_cancelled());
assert!(!h2.is_cancelled());
assert!(!h3.is_cancelled());
// Cancel via h2; all others must observe it.
h2.cancel();
assert!(h0.is_cancelled());
assert!(h1.is_cancelled());
assert!(h3.is_cancelled());
}
/// is_cancelled is non-destructive — reading the flag multiple times returns
/// the same result.
/// Mutation: using swap(false) instead of load would clear the flag on read.
#[test]
fn is_cancelled_is_non_destructive() {
let h = Halt::new();
h.cancel();
assert!(h.is_cancelled());
assert!(h.is_cancelled(), "second read must also return true");
assert!(h.is_cancelled(), "third read must also return true");
}
/// from_arc followed by cancel(), then as_arc() load: the raw Arc must see the write.
/// This is the round-trip that proves from_arc and as_arc are exact inverses.
/// Mutation: from_arc doing `Arc::new(flag.load(...))` (copy not share) breaks this.
#[test]
fn from_arc_and_as_arc_are_inverses() {
let original = Arc::new(AtomicBool::new(false));
let halt = Halt::from_arc(original.clone());
// Cancel via the Halt; read via the original Arc.
halt.cancel();
assert!(
original.load(Ordering::Relaxed),
"cancel() must be visible via the original Arc"
);
// The Arc retrieved by as_arc() must be the same one.
let retrieved = halt.as_arc();
assert!(
std::ptr::eq(Arc::as_ptr(retrieved), Arc::as_ptr(&original)),
"as_arc must return the same Arc pointer as was passed to from_arc"
);
}
/// POLL_INTERVAL is 250ms — a specific value that the multi-thread halt
/// loops depend on for responsiveness guarantees.
/// Mutation: setting POLL_INTERVAL to 5s makes stop requests take 5s to notice.
@@ -255,17 +188,4 @@ mod tests {
"POLL_INTERVAL must be 250ms for the guaranteed ~quarter-second cancel latency"
);
}
/// cancel() then clone: the clone of an already-cancelled Halt starts cancelled.
/// Mutation: cloning by re-reading the bool (not the Arc) would give a fresh false.
#[test]
fn clone_of_cancelled_halt_is_also_cancelled() {
let h = Halt::new();
h.cancel();
let cloned = h.clone();
assert!(
cloned.is_cancelled(),
"clone of a cancelled Halt must itself be cancelled"
);
}
}
-65
View File
@@ -326,39 +326,6 @@ mod tests {
);
}
/// match_key trims whitespace from all four fields.
/// Spec: comment says "All fields trimmed for consistent matching."
/// Mutation: removing .trim() from one field adds trailing spaces to the key.
#[test]
fn match_key_trims_all_fields() {
let mut inquiry = vec![0u8; 96];
// Pad vendor_id and product_id with trailing spaces (as drives do).
inquiry[8..16].copy_from_slice(b"HL-DT-ST"); // no padding room
inquiry[16..32].copy_from_slice(b"BD-RE BU40N "); // 5 trailing spaces
inquiry[32..36].copy_from_slice(b"1.03");
inquiry[36..43].copy_from_slice(b"NM00000");
let id = DriveId::from_inquiry(&inquiry, "211810241934");
// No trailing spaces in the key.
assert_eq!(id.match_key(), "HL-DT-ST|BD-RE BU40N|1.03|NM00000");
}
/// Display trims all four fields and does not include the firmware date.
/// Mutation: not trimming product_id adds trailing spaces to the display string.
#[test]
fn display_trims_fields() {
let mut inquiry = vec![0u8; 96];
inquiry[8..16].copy_from_slice(b"HL-DT-ST");
inquiry[16..32].copy_from_slice(b"BD-RE BU40N ");
inquiry[32..36].copy_from_slice(b"1.03");
inquiry[36..43].copy_from_slice(b"NM00000");
let id = DriveId::from_inquiry(&inquiry, "ignored");
let s = id.to_string();
// No double spaces from un-trimmed padding.
assert!(!s.contains(" "), "display must trim fields: `{s}`");
assert!(s.contains("HL-DT-ST"), "vendor present: `{s}`");
assert!(s.contains("BD-RE BU40N"), "product present: `{s}`");
}
/// from_inquiry stores the raw inquiry bytes in raw_inquiry unchanged.
/// Mutation: copying only a slice of inquiry into raw_inquiry truncates it.
#[test]
@@ -372,23 +339,6 @@ mod tests {
);
}
/// from_inquiry leaves serial_number and raw_gc_010c empty.
/// These are only available from a live drive probe via from_drive().
/// Mutation: populating serial_number in from_inquiry would violate the contract.
#[test]
fn from_inquiry_leaves_serial_and_gc_empty() {
let inquiry = vec![0u8; 96];
let id = DriveId::from_inquiry(&inquiry, "");
assert!(
id.serial_number.is_empty(),
"serial_number must be empty from from_inquiry"
);
assert!(
id.raw_gc_010c.is_empty(),
"raw_gc_010c must be empty from from_inquiry"
);
}
/// GET CONFIGURATION failure (transport error) must not abort the
/// identity probe — firmware_date is empty, raw_gc_010c is empty.
/// Mutation: propagating the GET_CONFIGURATION error with `?` aborts from_drive.
@@ -435,19 +385,4 @@ mod tests {
"raw_gc_010c must be empty when GC fails"
);
}
/// match_key uses '|' as the separator between all four fields.
/// Mutation: using ':' or ' ' as separator changes the key format.
#[test]
fn match_key_uses_pipe_separator() {
let inquiry = vec![0u8; 96];
let id = DriveId::from_inquiry(&inquiry, "");
let key = id.match_key();
// Should have exactly 3 pipes (4 fields separated by 3 '|' chars).
let pipe_count = key.chars().filter(|&c| c == '|').count();
assert_eq!(
pipe_count, 3,
"match_key must have exactly 3 '|' separators, got {pipe_count} in `{key}`"
);
}
}
-64
View File
@@ -999,15 +999,6 @@ mod tests {
assert!((secs - 10.0 * 3600.0).abs() < 0.01, "got {secs}");
}
/// bcd_byte boundary: 0x9A has lo=0xA (>9) → invalid → 0. And 0xA0 has
/// hi=0xA (>9) → 0. Confirms BOTH nibbles are validated.
#[test]
fn bcd_byte_partial_invalid_nibble() {
assert_eq!(bcd_byte(0x9A), 0); // lo nibble invalid
assert_eq!(bcd_byte(0xA0), 0); // hi nibble invalid
assert_eq!(bcd_byte(0x90), 90); // both valid
}
/// sub_slice uses saturating_add so an offset near usize::MAX cannot
/// wrap and bypass the bounds check. Must return Err, not panic/OOB.
#[test]
@@ -1068,17 +1059,6 @@ mod tests {
assert_eq!(attr2.codec, Codec::Unknown(1));
}
/// Audio channels = (b1>>4 & 0x0F) + 1 (stored as channels-minus-1).
/// b1 = 0x70 → 7+1 = 8 channels (7.1). Verify the +1 and nibble.
#[test]
fn audio_attr_channel_count_plus_one() {
let mut data = vec![0u8; 8];
data[0] = 0x00; // AC3
data[1] = 0x70; // channels-1 = 7
let attr = parse_audio_attr(&data, 0).unwrap();
assert_eq!(attr.channels, 8);
}
/// Audio language bytes [offset+2..+4]: when both bytes are 0x00 the
/// language is the empty string (unspecified), per source.
#[test]
@@ -1169,40 +1149,6 @@ mod tests {
assert_eq!(streams[8].sub_stream_id, Some(0x87));
}
/// parse_pgc: cells are 24-byte records; first_sector at cell+8,
/// last_sector at cell+20 (both u32 BE). The cell table starts at
/// PGC + cell_playback_offset (read from PGC+0xE8 as u16 BE). Build a
/// PGC with a non-trivial cell_playback_offset and verify cells.
#[test]
fn pgc_cell_offsets_first_and_last_sector() {
let mut pgc = vec![0u8; 0xEA];
pgc[0x02] = 1; // nr_programs
pgc[0x03] = 1; // nr_cells = 1
// PGC-level BCD time zero so duration is recomputed from cells.
// cell_playback_offset at 0xE8 (u16 BE) = 0xEA.
pgc[0xE8] = 0x00;
pgc[0xE9] = 0xEA;
pgc.resize(0xEA + 24, 0);
let co = 0xEA;
// cell BCD time at +4..+8: 0h 0m 10s, no frames.
pgc[co + 6] = 0x10; // seconds BCD 10
// first_sector at +8 = 0x000004D2 = 1234
pgc[co + 8..co + 12].copy_from_slice(&1234u32.to_be_bytes());
// last_sector at +20 = 0x0000162E = 5678
pgc[co + 20..co + 24].copy_from_slice(&5678u32.to_be_bytes());
let title = parse_pgc(&pgc, 0, 3).unwrap();
assert_eq!(title.cells.len(), 1);
assert_eq!(title.cells[0].first_sector, 1234);
assert_eq!(title.cells[0].last_sector, 5678);
// PGC time was 0 → recomputed from cell time = 10s.
assert!(
(title.duration_secs - 10.0).abs() < 0.01,
"got {}",
title.duration_secs
);
}
/// parse_pgc requires `pgc_offset + 0xEA <= data.len()` (needs the cell
/// playback offset at 0xE8). A PGC shorter than 0xEA → IfoParse error,
/// not panic.
@@ -1336,16 +1282,6 @@ mod tests {
);
}
/// VMG magic check: parse_video_attr et al. aside, the top-level VMG
/// must start with "DVDVIDEO-VMG". We exercise the constant directly to
/// guard against an accidental edit to the 12-byte magic.
#[test]
fn vmg_vts_magic_constants() {
assert_eq!(VMG_MAGIC, b"DVDVIDEO-VMG");
assert_eq!(VTS_MAGIC, b"DVDVIDEO-VTS");
assert_eq!(SECTOR_SIZE, 2048);
}
/// parse_pgc with cell_playback_offset == 0 must produce NO cells (the
/// `cell_playback_offset > 0 && num_cells > 0` guard). Even with
/// nr_cells set, a zero offset means the table is absent.
-53
View File
@@ -269,32 +269,6 @@ mod tests {
);
}
/// Boundary: an op that finishes well within the deadline returns
/// Ok even when a (live, never-cancelled) halt token is supplied.
/// The halt-poll path must not spuriously convert a completed op
/// into Halted/Timeout. Grounds the `Ok(v) => return Ok(v)` arm of
/// the recv_timeout match (line 134) with a non-None halt.
#[test]
fn live_halt_token_does_not_interfere_with_fast_op() {
let halt = Halt::new(); // never cancelled
let r = bounded_syscall(Some(&halt), Duration::from_secs(5), || 123u64);
assert!(matches!(r, Ok(123)));
assert!(!halt.is_cancelled());
}
/// The op's return value is propagated byte-for-byte, not just a
/// success flag. A non-Copy heap type proves the worker's
/// `tx.send(op())` moves the real value across the rendezvous
/// channel (line 125) to the receiver (line 134).
#[test]
fn returns_owned_value_unchanged() {
let r = bounded_syscall(None, Duration::from_secs(2), || vec![9u8, 8, 7, 6]);
match r {
Ok(v) => assert_eq!(v, vec![9u8, 8, 7, 6]),
other => panic!("expected Ok(vec), got {other:?}"),
}
}
/// Timeout boundary: with a tiny deadline and an op that sleeps
/// much longer, the helper must return Timeout and must do so
/// roughly at the deadline — NOT wait for the op to finish (that
@@ -318,31 +292,4 @@ mod tests {
"timeout did not return near deadline: {elapsed:?} (op should be leaked, not awaited)"
);
}
/// A worker that returns a non-Copy value AND completes within the
/// deadline must hand the value back; the rendezvous channel has
/// capacity 0, so the worker's send blocks until the receiver is
/// ready — exercising the happy-path handshake rather than the
/// buffered-send path. Mutation: changing `sync_channel::<R>(0)` to
/// a buffered channel would still pass; changing the recv arm to
/// drop the value would fail here.
#[test]
fn zero_capacity_rendezvous_delivers_string() {
let r = bounded_syscall(None, Duration::from_secs(2), || String::from("rendezvous"));
assert!(matches!(r.as_deref(), Ok("rendezvous")));
}
/// Halt that fires AFTER the op has already completed must still
/// yield Ok — there is no race that turns a delivered result into
/// Halted. The op completes instantly; we cancel the halt
/// afterwards and confirm the earlier call returned Ok. This pins
/// the precedence: a value already in the channel wins over a
/// subsequent halt.
#[test]
fn op_completion_wins_over_later_halt() {
let halt = Halt::new();
let r = bounded_syscall(Some(&halt), Duration::from_secs(2), || 55u32);
halt.cancel();
assert!(matches!(r, Ok(55)));
}
}
-19
View File
@@ -423,25 +423,6 @@ mod tests {
});
}
/// Backpressure / recycle exhaustion does not deadlock: a source
/// larger than the whole in-flight pool (FORWARD_DEPTH +
/// RECYCLE_DEPTH chunks) must still drain fully when the consumer
/// recycles. 10 chunks of 256 bytes = 2560 bytes; pool holds far
/// fewer. Proves the producer parks on recycle_rx and resumes as
/// the consumer returns buffers (lines 106-117). Mutation: dropping
/// the recycle seed loop (lines 90-92) would deadlock on the first
/// recv and within() times out.
#[test]
fn large_source_drains_with_recycling() {
within(10, || {
let src: Vec<u8> = (0..2560u32).map(|i| (i % 251) as u8).collect();
let pf = BytePrefetcher::new(Cursor::new(src.clone()), 256, None).expect("spawn");
let (got, err) = drain_to_vec(pf);
assert!(err.is_none());
assert_eq!(got, src);
});
}
/// Exact-multiple boundary: when the source length is an exact
/// multiple of chunk_bytes, the final non-empty chunk is followed
/// by an `Ok(0)` EOF read, NOT a spurious empty Ok batch. 12 bytes
-35
View File
@@ -416,20 +416,6 @@ mod tests {
);
}
/// Reading entirely beyond EOF (seek lands past the end) must also
/// error rather than silently return zeros. Grounding: read_exact
/// over an empty remainder is UnexpectedEof.
#[test]
fn read_wholly_beyond_eof_errors() {
let dir = tempdir().unwrap();
let path = dir.path().join("beyond.iso");
make_iso(&path, 4);
let mut src = FileSectorSource::open(&path).unwrap();
let mut buf = vec![0u8; SECTOR_SIZE];
let r = src.read_sectors(10, 1, &mut buf, false);
assert!(r.is_err(), "read starting past EOF must error");
}
/// On a successful full read the returned count MUST equal
/// `count * 2048` exactly — the declared byte count. Grounding:
/// `Ok(bytes)` where `bytes = count * SECTOR_SIZE`.
@@ -496,27 +482,6 @@ mod tests {
assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
}
/// Repeated reads of the SAME sector must return identical bytes —
/// the per-read seek makes each call independent of prior position,
/// and the DONTNEED/prefetch hooks are advisory only (no data
/// effect). Grounding: `seek(SeekFrom::Start(offset))` before every
/// read.
#[test]
fn repeated_same_sector_is_stable() {
let dir = tempdir().unwrap();
let path = dir.path().join("stable.iso");
make_iso(&path, 8);
let mut src = FileSectorSource::open(&path).unwrap();
let mut a = vec![0u8; SECTOR_SIZE];
let mut b = vec![0u8; SECTOR_SIZE];
src.read_sectors(5, 1, &mut a, false).unwrap();
// Read a different sector in between to move the file cursor.
src.read_sectors(0, 1, &mut b, false).unwrap();
src.read_sectors(5, 1, &mut b, false).unwrap();
assert_eq!(a, b, "same-LBA reads must be position-independent");
assert!(a.iter().all(|x| *x == (5u8)));
}
/// A DONTNEED drop crossing the chunk threshold must not corrupt or
/// short subsequent reads — the eviction is a pure page-cache hint.
/// We read past the DEFAULT 32 MiB drop chunk (16384 sectors) so the
-54
View File
@@ -926,22 +926,6 @@ mod tests {
}
}
/// FIFO ordering: items must be delivered to `apply` in send order.
/// crossbeam's `bounded` channel is FIFO; this pins that the
/// pipeline does not reorder. Mutation: if the consumer loop reused
/// a stale item or sorted, the equality fails.
#[test]
fn items_delivered_in_fifo_order() {
let pipe =
Pipeline::spawn(DEFAULT_PIPELINE_DEPTH, OrderSink { seen: Vec::new() }).expect("spawn");
let input: Vec<u64> = (0..50).map(|i| i * 7 + 1).collect();
for &i in &input {
pipe.send(i).expect("send");
}
let seen = pipe.finish().expect("finish");
assert_eq!(seen, input, "pipeline reordered or dropped items");
}
/// Zero items sent: closing the pipeline immediately must still
/// call `close()` exactly once and return its Output. The consumer
/// loop's `while let Ok = rx.recv()` exits on the dropped tx with
@@ -1117,23 +1101,6 @@ mod tests {
assert!(!halt.is_cancelled(), "halt must not have been the cause");
}
/// `send_with_halt` happy path: when there is room in the channel
/// it must deliver the item (Ok) and the consumer must process it.
/// Pins the `Ok(()) => return Ok(())` arm (line 390). Mutation:
/// inverting that arm to Err would drop the item and the sum would
/// be wrong.
#[test]
fn send_with_halt_delivers_when_room_available() {
let pipe = Pipeline::spawn(DEFAULT_PIPELINE_DEPTH, SumSink { total: 0 }).expect("spawn");
let halt = crate::halt::Halt::new();
for i in 1..=5u64 {
pipe.send_with_halt(i, &halt, Duration::from_secs(5))
.expect("send_with_halt should deliver when room is available");
}
let total = pipe.finish().expect("finish");
assert_eq!(total, 15, "1+2+3+4+5");
}
/// `send_with_halt` with a pre-cancelled halt must return the item
/// immediately without attempting to enqueue. Pins the pre-check at
/// line 365 (`if halt.is_cancelled()`). Mutation: removing that
@@ -1154,27 +1121,6 @@ mod tests {
assert_eq!(total, 0, "item was enqueued despite pre-cancelled halt");
}
/// `finish_with_halt` must propagate a consumer panic as
/// `PipelineConsumerPanicked` — same as `finish`. The consumer
/// panics on the first apply; finish_with_halt sees `is_finished()`
/// true and joins, mapping the panic payload (lines 454-458).
#[test]
fn finish_with_halt_propagates_consumer_panic() {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let pipe = Pipeline::spawn(DEFAULT_PIPELINE_DEPTH, PanickingSink).expect("spawn");
let _ = pipe.send(1);
for i in 0..5u64 {
let _ = pipe.send(i);
}
let res = pipe.finish_with_halt(None);
std::panic::set_hook(prev);
assert!(
matches!(res, Err(Error::PipelineConsumerPanicked)),
"expected PipelineConsumerPanicked, got {res:?}"
);
}
/// `finish_with_halt(None)` with a wedged consumer and NO halt
/// token must NOT return early — it must keep polling until the
/// JOIN_TIMEOUT_SECS deadline (it cannot observe a halt that was
-13
View File
@@ -230,17 +230,4 @@ mod tests {
);
assert_eq!(&bytes[10..14], b"TAIL");
}
/// `write` (single call) returns the BufWriter's accepted count.
/// For a buffer under the 4 MiB capacity this is the full length
/// (lines 108-110). Mutation: a wrong count return would break
/// callers relying on `Write::write`'s contract.
#[test]
fn write_returns_full_count_under_capacity() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("count.bin");
let mut s = LocalFileSink::create(&p).unwrap();
let n = s.write(&[1u8; 1000]).unwrap();
assert_eq!(n, 1000);
}
}
-34
View File
@@ -216,24 +216,6 @@ mod tests {
assert_eq!(bytes.load(Ordering::SeqCst), 3);
}
/// Default `finish()` dispatched through a `dyn SequentialSink`
/// trait object must still reach the default `flush` (vtable path).
/// This guards that there is no accidental override that turns the
/// default into a no-op via dyn dispatch.
#[test]
fn default_finish_flushes_through_dyn() {
let flushed = Arc::new(AtomicBool::new(false));
let bytes = Arc::new(AtomicUsize::new(0));
let sink = FlushTracker {
flushed: flushed.clone(),
bytes: bytes.clone(),
};
let mut boxed: Box<dyn SequentialSink> = Box::new(sink);
boxed.write_all(b"xy").unwrap();
boxed.finish().unwrap();
assert!(flushed.load(Ordering::SeqCst));
}
/// `open_for_mkv` with `None` size hint must still produce a working
/// random-access sink (the `match size_hint { None => ... }` arm,
/// lines 103-106). Round-trip a seek-back patch through it to prove
@@ -252,20 +234,4 @@ mod tests {
drop(sink);
assert_eq!(std::fs::read(&p).unwrap(), b"AAAACCCC");
}
/// finish() through a `dyn RandomAccessSink` (the supertrait of
/// SequentialSink) for a LocalFileSink must also flush+fsync. The
/// existing regression test boxes as `dyn SequentialSink`; this
/// pins the `dyn RandomAccessSink` vtable path too, since
/// `open_for_mkv` returns exactly that boxed type.
#[test]
fn finish_through_random_access_dyn_persists() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("ra-finish.bin");
let mut sink: Box<dyn RandomAccessSink> = open_for_mkv(&p, None).unwrap();
sink.write_all(b"durable").unwrap();
sink.finish().unwrap();
// Visible to a separate reader before drop.
assert_eq!(&std::fs::read(&p).unwrap()[..], b"durable");
}
}
-21
View File
@@ -328,27 +328,6 @@ mod tests {
assert_eq!(received, b"unflushed-tail");
}
/// `SocketSink::write` must report the exact byte count it accepted
/// into the BufWriter (forwarded from `BufWriter::write`, lines
/// 78-80). For a buffer smaller than the 1 MiB capacity this equals
/// the full length. Mutation: returning a wrong/clamped count would
/// break `Write::write_all`'s loop downstream; we pin the count
/// here directly.
#[test]
fn write_reports_accepted_count() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let _accept = thread::spawn(move || {
let _ = listener.accept();
});
let mut sink = SocketSink::connect(addr, None).unwrap();
let n = sink.write(&[7u8; 100]).unwrap();
assert_eq!(
n, 100,
"buffered write under capacity must accept all bytes"
);
}
/// UDP `write` must emit ONE datagram per call carrying exactly the
/// bytes passed — no buffering, no coalescing (doc lines 100-108).
/// Two writes of different lengths must arrive as two separate
-24
View File
@@ -522,30 +522,6 @@ mod tests {
assert_eq!(&bytes, b"hello");
}
/// `flush` must not be a durability barrier nor reorder bytes, but
/// it also must not lose buffered data. We interleave write_all and
/// flush and confirm exact byte order survives to disk. (Distinct
/// from the existing `flush_is_observed_in_order` which uses 3
/// words; this exercises many small flushes to stress the
/// passthrough flush path at line 199-201.) Mutation: if `flush`
/// dropped pending bytes the reassembly fails.
#[test]
fn many_interleaved_flushes_preserve_order() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("many-flush.bin");
let mut w = WritebackFile::create(&p).unwrap();
let mut expected = Vec::new();
for i in 0u8..32 {
let chunk = [i; 4];
w.write_all(&chunk).unwrap();
expected.extend_from_slice(&chunk);
w.flush().unwrap();
}
w.sync_all().unwrap();
drop(w);
assert_eq!(read_back(&p), expected);
}
/// `sync_all` is idempotent: calling it twice (and then Drop, which
/// also finalizes) must not corrupt data or panic. Doc lines
/// 256-262: `finalize` is idempotent so explicit sync_all then drop
-77
View File
@@ -410,48 +410,6 @@ mod tests {
assert_eq!(val, "/new/path", "value must be trimmed");
}
/// parse_url with no explicit port defaults to 80.
/// Spec: HTTP default port is 80 (RFC 7230 §2.7.1).
/// Mutation: defaulting to 443 instead makes plain-HTTP URLs go to the wrong port.
#[test]
fn parse_url_no_port_defaults_to_80() {
let (_, port, _) = parse_url("http://example.com/path").unwrap();
assert_eq!(port, 80, "default HTTP port must be 80 (RFC 7230 §2.7.1)");
}
/// parse_url with explicit port parses it correctly.
/// Mutation: ignoring the port component and defaulting to 80 changes the port.
#[test]
fn parse_url_explicit_port_is_parsed() {
let (host, port, path) = parse_url("http://mirror.example:9000/key.zip").unwrap();
assert_eq!(host, "mirror.example");
assert_eq!(port, 9000);
assert_eq!(path, "/key.zip");
}
/// parse_url with no path component yields "/" as the path.
/// RFC 7230 §5.3.1: origin-form must start with "/"; empty → root.
/// Mutation: returning "" as the path makes the HTTP request malformed.
#[test]
fn parse_url_no_path_yields_root() {
let (_, _, path) = parse_url("http://example.com").unwrap();
assert_eq!(
path, "/",
"missing path must default to '/' (RFC 7230 §5.3.1)"
);
}
/// parse_url rejects a URL whose scheme is not http.
/// Mutation: accepting ftp:// silently leads to a TCP connection receiving
/// binary FTP data instead of HTTP.
#[test]
fn parse_url_rejects_ftp_scheme() {
assert!(matches!(
parse_url("ftp://ftp.example.com/file"),
Err(Error::KeydbUnsupportedScheme { .. })
));
}
/// save() rejects data that is not a valid keydb (no recognisable entries).
/// Spec: entries are lines starting with "0x", "| DK", "| PK", or "| HC".
/// Mutation: dropping the entries==0 check lets an empty file be saved.
@@ -589,32 +547,6 @@ mod tests {
assert!(result.is_ok(), "exactly MAX_KEYDB_BYTES must be accepted");
}
/// parse_url path round-trips: the extracted path is the same string that
/// was in the URL.
/// Mutation: dropping the leading '/' from the path breaks the HTTP request.
#[test]
fn parse_url_path_includes_leading_slash() {
let (_, _, path) = parse_url("http://example.com/a/b/c.zip").unwrap();
assert!(
path.starts_with('/'),
"path must start with '/', got `{path}`"
);
assert_eq!(path, "/a/b/c.zip");
}
/// resolve_redirect with an absolute http URL parses it fresh
/// (ignores the current host/port entirely).
/// Mutation: keeping the current host instead of parsing the new one
/// points the next request at the wrong server.
#[test]
fn resolve_redirect_absolute_http_ignores_current_host() {
let (h, p, path) =
resolve_redirect("http://new.host:8080/k.zip", "old.host", 9000).unwrap();
assert_eq!(h, "new.host");
assert_eq!(p, 8080);
assert_eq!(path, "/k.zip");
}
/// parse_status returns 0 for an empty status line (not a panic).
/// Mutation: calling unwrap() instead of unwrap_or(0) panics on empty input.
#[test]
@@ -622,13 +554,4 @@ mod tests {
assert_eq!(parse_status(""), 0);
assert_eq!(parse_status("\r\n"), 0);
}
/// parse_status handles HTTP/1.0 and HTTP/1.1 both.
/// Mutation: only matching "HTTP/1.0 " misses HTTP/1.1 responses.
#[test]
fn parse_status_handles_http_versions() {
assert_eq!(parse_status("HTTP/1.0 404 Not Found"), 404);
assert_eq!(parse_status("HTTP/1.1 200 OK"), 200);
assert_eq!(parse_status("HTTP/1.1 302 Found\r\nLocation: /new"), 302);
}
}
-186
View File
@@ -101,103 +101,6 @@ mod tests {
// ── DiscInputs structural tests ────────────────────────────────────────────
/// DiscInputs can be constructed with all-zero volume_id ([0u8;16]) to
/// represent "no authenticated handshake ran".
/// Spec: doc says "[0u8; 16] when no authenticated handshake ran".
/// Mutation: using Option<[u8;16]> would require callers to handle None explicitly.
#[test]
fn disc_inputs_zero_volume_id_represents_no_handshake() {
let inputs = DiscInputs {
disc_hash: "0x1234".to_string(),
volume_id: [0u8; 16],
mkb: Vec::new(),
unit_key_ro: Vec::new(),
samples: Vec::new(),
volume_label: None,
};
assert_eq!(
inputs.volume_id, [0u8; 16],
"all-zero volume_id must be valid (represents no handshake)"
);
}
/// DiscInputs disc_hash is a string in "0x"-prefixed hex format.
/// Spec: doc says "SHA-1 of Unit_Key_RO.inf, 0x-prefixed hex."
/// Mutation: storing the hash without the "0x" prefix would silently change
/// the keydb lookup key format.
#[test]
fn disc_inputs_disc_hash_is_0x_prefixed() {
let hash = "0xabcdef0123456789abcdef0123456789abcdef01".to_string();
let inputs = DiscInputs {
disc_hash: hash.clone(),
volume_id: [0u8; 16],
mkb: Vec::new(),
unit_key_ro: Vec::new(),
samples: Vec::new(),
volume_label: None,
};
assert!(
inputs.disc_hash.starts_with("0x"),
"disc_hash must be 0x-prefixed per spec"
);
assert_eq!(
inputs.disc_hash.len(),
42,
"SHA-1 in 0x-prefixed hex: 2 ('0x') + 40 (20 bytes hex) = 42 chars"
);
}
/// DiscInputs samples is intentionally empty by default (filled by caller).
/// Spec: doc says "Populated by the application — libfreemkv::Disc::inputs
/// leaves it empty for the caller to fill."
/// Mutation: auto-filling samples in Disc::inputs would force all callers
/// to read content data even for local keydb lookups.
#[test]
fn disc_inputs_samples_defaults_to_empty() {
let inputs = DiscInputs {
disc_hash: "0x0000000000000000000000000000000000000000".to_string(),
volume_id: [0u8; 16],
mkb: Vec::new(),
unit_key_ro: Vec::new(),
samples: Vec::new(),
volume_label: None,
};
assert!(
inputs.samples.is_empty(),
"samples must start empty — populated by the application, not Disc::inputs"
);
}
/// DiscInputs volume_label is Option<String>: None means not captured.
/// Spec: doc says "None when not captured."
/// Mutation: using an empty string instead of None would conflate "not captured"
/// with "the disc has an empty label" — a semantic difference.
#[test]
fn disc_inputs_volume_label_none_vs_some() {
let no_label = DiscInputs {
disc_hash: "0x0000000000000000000000000000000000000000".to_string(),
volume_id: [0u8; 16],
mkb: Vec::new(),
unit_key_ro: Vec::new(),
samples: Vec::new(),
volume_label: None,
};
assert!(
no_label.volume_label.is_none(),
"not-captured label must be None"
);
let with_label = DiscInputs {
disc_hash: "0x0000000000000000000000000000000000000000".to_string(),
volume_id: [0u8; 16],
mkb: Vec::new(),
unit_key_ro: Vec::new(),
samples: Vec::new(),
volume_label: Some("WICKED_FOR_GOOD".to_string()),
};
assert_eq!(with_label.volume_label.as_deref(), Some("WICKED_FOR_GOOD"));
}
// ── KeySource default-method behaviour ────────────────────────────────────
/// KeySource::needs_samples() defaults to false.
@@ -231,93 +134,4 @@ mod tests {
let s = MinimalSource;
assert!(!s.errored(), "errored must default to false");
}
/// A source that returns None and has errored()==true can be distinguished
/// from a source that simply has no key.
/// Spec: doc says "After exhaustion the caller must consult errored()".
/// Mutation: errored() always returning false hides network/parse failures.
#[test]
fn errored_source_is_distinguishable_from_empty_source() {
struct FailedSource;
impl KeySource for FailedSource {
fn next_key(&mut self, _inputs: &DiscInputs) -> Option<Key> {
None
}
fn errored(&self) -> bool {
true
}
}
struct EmptySource;
impl KeySource for EmptySource {
fn next_key(&mut self, _inputs: &DiscInputs) -> Option<Key> {
None
}
// errored() defaults to false
}
let inputs = DiscInputs {
disc_hash: String::new(),
volume_id: [0u8; 16],
mkb: vec![],
unit_key_ro: vec![],
samples: vec![],
volume_label: None,
};
let mut failed = FailedSource;
let mut empty = EmptySource;
// Both return None (exhausted).
assert!(failed.next_key(&inputs).is_none());
assert!(empty.next_key(&inputs).is_none());
// But only FailedSource reports an error.
assert!(failed.errored(), "FailedSource must report errored=true");
assert!(!empty.errored(), "EmptySource must report errored=false");
}
/// DiscInputs mkb field stores raw MKB bytes and can be empty.
/// Mutation: using Option<Vec<u8>> for mkb forces callers to handle Option.
#[test]
fn disc_inputs_mkb_can_be_empty_or_populated() {
let empty_mkb = DiscInputs {
disc_hash: String::new(),
volume_id: [0u8; 16],
mkb: Vec::new(),
unit_key_ro: Vec::new(),
samples: Vec::new(),
volume_label: None,
};
assert!(empty_mkb.mkb.is_empty());
let populated_mkb = DiscInputs {
disc_hash: String::new(),
volume_id: [0u8; 16],
mkb: vec![0x01, 0x02, 0x03],
unit_key_ro: vec![0xFF],
samples: Vec::new(),
volume_label: None,
};
assert_eq!(populated_mkb.mkb, vec![0x01, 0x02, 0x03]);
assert_eq!(populated_mkb.unit_key_ro, vec![0xFF]);
}
/// A source that overrides needs_samples() to true is handled correctly.
/// Mutation: ignoring the needs_samples() return means online sources
/// never get the ciphertext samples they need for validation.
#[test]
fn needs_samples_can_be_overridden_to_true() {
struct SamplesNeededSource;
impl KeySource for SamplesNeededSource {
fn next_key(&mut self, _inputs: &DiscInputs) -> Option<Key> {
None
}
fn needs_samples(&self) -> bool {
true
}
}
let s = SamplesNeededSource;
assert!(
s.needs_samples(),
"an online source that validates against ciphertext must return needs_samples=true"
);
}
}
-167
View File
@@ -485,18 +485,6 @@ mod tests {
assert_eq!(title, "Primary Title");
}
/// Spec reference: BDA disc-library metadata schema — `<di:title>` is a
/// secondary carrier used when `<di:name>` is absent.
/// Mutation: insert a `<di:name>` element → test goes red (di:name wins).
#[test]
fn di_title_used_when_no_di_name() {
let xml = r#"<discInfo xmlns:di="urn:BDA:bdmv;disclibmeta">
<di:title>Fallback Title</di:title>
</discInfo>"#;
let (title, _, _) = parse_bdmt_xml(xml).unwrap();
assert_eq!(title, "Fallback Title");
}
/// Spec reference: BDA disc-library metadata §3.3.2 — `<di:tableOfContents>`
/// with nested `<di:titleName>` is a vendor-specific variant.
/// Mutation: rename `titleName` → `movieName` → test goes red (None).
@@ -513,20 +501,6 @@ mod tests {
assert_eq!(title, "Winner");
}
/// Spec reference: BDA disc-library metadata §3.3.2 — titleName inside
/// tableOfContents is the last-resort title fallback.
/// Mutation: rename `titleName` to `movieTitle` → test goes red (None returned).
#[test]
fn table_of_contents_title_name_is_last_resort() {
let xml = r#"<discInfo xmlns:di="urn:BDA:bdmv;disclibmeta">
<di:tableOfContents>
<di:titleName>TOC Title</di:titleName>
</di:tableOfContents>
</discInfo>"#;
let (title, _, _) = parse_bdmt_xml(xml).unwrap();
assert_eq!(title, "TOC Title");
}
/// Spec reference: BDA §3.3.2 — an empty `<di:name>` element must be
/// treated as absent, falling through to the next candidate.
/// Mutation: change `<di:name></di:name>` to `<di:name>X</di:name>` → red.
@@ -540,85 +514,6 @@ mod tests {
assert_eq!(title, "Non-Empty Title");
}
/// Spec reference: BDA §3.3.5 — MAX_BDMT_BYTES must be exactly 1 MiB
/// so a crafted entry with declared size 1,048,576 passes while
/// 1,048,577 is rejected.
/// Mutation: change MAX_BDMT_BYTES from 1_048_576 to e.g. 512*1024 → boundary test red.
#[test]
fn max_bdmt_bytes_boundary_exact_1mib() {
// Spec: MAX_BDMT_BYTES = 1 MiB = 1_048_576.
// Exactly at the limit: accepted.
assert!(bdmt_size_acceptable(1_048_576));
// One byte over: rejected.
assert!(!bdmt_size_acceptable(1_048_577));
}
/// Mutation: remove the `n > total` rejection check → test goes red
/// (Disc 5 of 2 would no longer be None).
#[test]
fn disc_set_rejects_disc_number_greater_than_total() {
let xml = r#"<discInfo xmlns:di="urn:BDA:bdmv;disclibmeta">
<di:name>X</di:name>
<di:discNumber>5</di:discNumber>
<di:numSets>3</di:numSets>
</discInfo>"#;
assert_eq!(parse_bdmt_xml(xml).unwrap().2, None);
}
/// Mutation: change `n < 1` check to `n < 0` → zero numerator accepted.
#[test]
fn disc_set_rejects_zero_numerator() {
let xml = r#"<discInfo xmlns:di="urn:BDA:bdmv;disclibmeta">
<di:name>X</di:name>
<di:discNumber>0</di:discNumber>
<di:numSets>5</di:numSets>
</discInfo>"#;
assert_eq!(parse_bdmt_xml(xml).unwrap().2, None);
}
/// Mutation: change `total < 1` to `total < 0` → zero denominator accepted.
#[test]
fn disc_set_rejects_zero_denominator() {
let xml = r#"<discInfo xmlns:di="urn:BDA:bdmv;disclibmeta">
<di:name>X</di:name>
<di:discNumber>1</di:discNumber>
<di:numSets>0</di:numSets>
</discInfo>"#;
assert_eq!(parse_bdmt_xml(xml).unwrap().2, None);
}
/// `<di:numberOfSets>` is an alternate spelling for `<di:numSets>`.
/// Spec reference: BDA vendor variation observed in the wild.
/// Mutation: rename `numberOfSets` to `setCount` → disc_number is None.
#[test]
fn number_of_sets_alternate_tag_accepted() {
let xml = r#"<discInfo xmlns:di="urn:BDA:bdmv;disclibmeta">
<di:name>Box Film</di:name>
<di:discNumber>4</di:discNumber>
<di:numberOfSets>8</di:numberOfSets>
</discInfo>"#;
let (_, _, set) = parse_bdmt_xml(xml).unwrap();
assert_eq!(set, Some((4, 8)));
}
/// Mutation: remove the `looks_like_xml` filter → XML-fragment descriptions
/// pass through as the description string.
#[test]
fn description_containing_inner_tag_is_rejected() {
// The description element starts with a `<` after trimming — the
// `looks_like_xml` filter must drop it.
let xml = r#"<discInfo xmlns:di="urn:BDA:bdmv;disclibmeta">
<di:name>Film</di:name>
<di:description><inner>garbage</inner></di:description>
</discInfo>"#;
let (_, description, _) = parse_bdmt_xml(xml).unwrap();
assert!(
description.is_none(),
"XML-fragment description must be dropped, got {:?}",
description
);
}
/// Mutation: remove the `!s.is_empty()` filter → empty descriptions
/// come through as Some("").
#[test]
@@ -631,29 +526,6 @@ mod tests {
assert_eq!(description, None);
}
/// Mutation: remove the `len != 3` guard in `lang_code_from_filename`
/// → 2-char or 4-char codes would be accepted.
#[test]
fn lang_code_rejects_two_char_code() {
assert_eq!(lang_code_from_filename("bdmt_en.xml"), None);
}
/// Mutation: remove the `is_ascii_alphabetic` guard → numeric codes
/// (e.g. `en3`) would be accepted.
#[test]
fn lang_code_rejects_non_alphabetic_code() {
assert_eq!(lang_code_from_filename("bdmt_en3.xml"), None);
assert_eq!(lang_code_from_filename("bdmt_e_g.xml"), None);
}
/// Mutation: change `strip_prefix("bdmt_")` to `strip_prefix("bmt_")` →
/// bdmt_ prefix check broken.
#[test]
fn lang_code_rejects_wrong_prefix() {
assert_eq!(lang_code_from_filename("bmt_eng.xml"), None);
assert_eq!(lang_code_from_filename("meta_eng.xml"), None);
}
/// Disc N of N (e.g. 3 of 3) is valid — not an off-by-one error.
/// Mutation: change `n > total` to `n >= total` → last disc of set is None.
#[test]
@@ -680,31 +552,6 @@ mod tests {
assert_eq!(set, None);
}
/// A title with embedded XML entities: we do NOT decode entities.
/// Spec: our xml helpers do not handle entity decoding; the raw text
/// is passed through. This documents the limitation explicitly.
/// Mutation: add entity decoding → this test goes red (value changes).
#[test]
fn title_with_entities_passes_through_raw() {
let xml = r#"<discInfo xmlns:di="urn:BDA:bdmv;disclibmeta">
<di:name>Arthur &amp; Max</di:name>
</discInfo>"#;
let (title, _, _) = parse_bdmt_xml(xml).unwrap();
// We don't decode &amp; — passes through as literal text between tags.
assert!(!title.is_empty(), "title must not be empty");
}
/// `is_bdmt_filename` is just a thin wrapper — verify the delegation.
/// Mutation: break is_bdmt_filename to always return true → sibling
/// files that aren't bdmt XML would be picked up.
#[test]
fn is_bdmt_filename_delegates_correctly() {
assert!(is_bdmt_filename("bdmt_eng.xml"));
assert!(is_bdmt_filename("BDMT_DEU.XML"));
assert!(!is_bdmt_filename("other.xml"));
assert!(!is_bdmt_filename("bdmt_engl.xml"));
}
/// Whitespace-only title element must be treated as empty (trimmed → "").
/// Spec: xml::text trims; an all-whitespace element produces "" after trim,
/// which the title-extraction logic should skip.
@@ -719,18 +566,4 @@ mod tests {
let (title, _, _) = parse_bdmt_xml(xml).unwrap();
assert_eq!(title, "Real Title");
}
/// A zero-byte size entry should be accepted (legitimate empty-but-present files).
/// Mutation: change `size <= MAX_BDMT_BYTES` to `size < MAX_BDMT_BYTES` → zero fails.
#[test]
fn zero_size_entry_is_acceptable() {
assert!(bdmt_size_acceptable(0));
}
/// MAX value of u64 must definitely be rejected.
/// Mutation: add a `MIN_SIZE` check that always passes → u64::MAX accepted.
#[test]
fn u64_max_size_rejected() {
assert!(!bdmt_size_acceptable(u64::MAX));
}
}
-55
View File
@@ -345,61 +345,6 @@ mod tests {
assert_eq!((co, mo, m, d), (0, 0, 0, 0));
}
/// Spec: (false, false) branch — both coding_types absent, equal language → Match.
/// The spec comment says "compare language fields; Divergent if they differ, else Match".
/// Mutation: return Divergent for any (false, false) case → this test goes red.
#[test]
fn class_both_coding_absent_equal_none_lang_is_match() {
let r = ClpiVsMplsRow {
pid: 0x1100,
clpi_coding_type: None,
clpi_language: None,
mpls_coding_type: None,
mpls_language: None,
};
// Both languages are None == None → Match.
assert_eq!(r.class(), ClpiVsMplsClass::Match);
}
/// Spec: all four classes form an exhaustive disjoint cover.
/// This test verifies the discriminant logic using boundary coding_type values.
/// Mutation: swap the ClpiOnly/MplsOnly branches → wrong classification.
#[test]
fn class_boundary_coding_types_all_four_classes_reachable() {
let clpi_only = ClpiVsMplsRow {
pid: 1,
clpi_coding_type: Some(1),
clpi_language: None,
mpls_coding_type: None,
mpls_language: None,
};
let mpls_only = ClpiVsMplsRow {
pid: 2,
clpi_coding_type: None,
clpi_language: None,
mpls_coding_type: Some(1),
mpls_language: None,
};
let match_ = ClpiVsMplsRow {
pid: 3,
clpi_coding_type: Some(0x83),
clpi_language: Some("eng".into()),
mpls_coding_type: Some(0x83),
mpls_language: Some("eng".into()),
};
let divergent = ClpiVsMplsRow {
pid: 4,
clpi_coding_type: Some(0x83),
clpi_language: Some("eng".into()),
mpls_coding_type: Some(0x83),
mpls_language: Some("fra".into()),
};
assert_eq!(clpi_only.class(), ClpiVsMplsClass::ClpiOnly);
assert_eq!(mpls_only.class(), ClpiVsMplsClass::MplsOnly);
assert_eq!(match_.class(), ClpiVsMplsClass::Match);
assert_eq!(divergent.class(), ClpiVsMplsClass::Divergent);
}
/// Spec: class_counts tuple order is (clpi_only, mpls_only, matches, divergent).
/// Verifies each counter increments the RIGHT slot.
/// Mutation: swap any two counters → wrong slot increments.
-13
View File
@@ -1730,19 +1730,6 @@ mod apply_tests {
// ── codec_hint_consistent hardening ───────────────────────────────────────
/// Spec: a hint naming only "TrueHD" is consistent with a TrueHD stream;
/// inconsistent with AC-3, AC-3+, DTS, etc.
/// Mutation: make all hints consistent with every codec → the unshuffle logic stops working.
#[test]
fn codec_hint_consistent_truehd_families() {
assert!(codec_hint_consistent("TrueHD 7.1", &Codec::TrueHd));
assert!(codec_hint_consistent("Dolby TrueHD", &Codec::TrueHd));
assert!(!codec_hint_consistent("TrueHD 7.1", &Codec::Ac3));
assert!(!codec_hint_consistent("TrueHD 7.1", &Codec::Ac3Plus));
assert!(!codec_hint_consistent("TrueHD 7.1", &Codec::Dts));
assert!(!codec_hint_consistent("TrueHD 7.1", &Codec::Lpcm));
}
/// Spec: "Dolby Digital" (AC-3) hint is consistent ONLY with AC-3 streams;
/// NOT with DD+ or TrueHD.
/// Mutation: accept "Dolby Digital" as consistent with AC-3+ → DD+ mislabeled.
-143
View File
@@ -548,33 +548,6 @@ mod tests {
assert_eq!(codec("dts"), "dts");
}
/// Spec: COMPOUND_LANGS must be ordered longest-first so that
/// "Brazilian Portuguese" is matched before bare "Portuguese".
/// Mutation: put "portuguese" before "brazilian portuguese" in the table →
/// Brazilian Portuguese returns variant="", losing the regional info.
#[test]
fn compound_lang_longest_match_wins() {
let r = lang("Brazilian Portuguese 5.1 Dolby").unwrap();
assert_eq!(r.code, "por");
assert_eq!(r.variant, "Brazilian");
let r = lang("Castilian Spanish").unwrap();
assert_eq!(r.code, "spa");
assert_eq!(r.variant, "Castilian");
let r = lang("Latin American Spanish").unwrap();
assert_eq!(r.code, "spa");
assert_eq!(r.variant, "Latin American");
}
/// Spec: bare language name lookup uses word-boundary matching.
/// Mutation: use `.contains()` instead of `has_word()` → "engineering" matches "english".
#[test]
fn lang_no_false_positive_substring() {
assert_eq!(lang("Audio Engineering"), None);
assert_eq!(lang("Francispeople"), None);
}
/// Spec: all 36 bare-lang entries must resolve correctly.
/// Mutation: swap two entries in BARE_LANGS → wrong code returned.
#[test]
@@ -605,122 +578,6 @@ mod tests {
}
}
/// Spec: `purpose()` recognizes "commentary" (word-boundary).
/// Mutation: use `contains("comment")` → "commenter" wrongly matches.
#[test]
fn purpose_commentary_word_boundary() {
assert_eq!(purpose("English Commentary"), LabelPurpose::Commentary);
assert_eq!(purpose("Commenter Track"), LabelPurpose::Normal);
assert_eq!(purpose("recommentary"), LabelPurpose::Normal);
}
/// Spec: "Director's Commentary" is a recognized phrase.
/// Mutation: require exact "commentary" without apostrophe prefix → fails.
#[test]
fn purpose_directors_commentary_recognized() {
assert_eq!(purpose("Director's Commentary"), LabelPurpose::Commentary);
}
/// Spec: `purpose()` recognizes "audio description" compound phrase.
/// Mutation: remove the compound `audio description` check → Descriptive broken.
#[test]
fn purpose_audio_description_compound() {
assert_eq!(purpose("Audio Description"), LabelPurpose::Descriptive);
assert_eq!(
purpose("English Audio Description"),
LabelPurpose::Descriptive
);
}
/// Spec: "descriptive service" maps to Descriptive via compound check.
/// Mutation: remove "descriptive service" compound → Normal returned.
#[test]
fn purpose_descriptive_service_compound() {
assert_eq!(
purpose("English Descriptive Service"),
LabelPurpose::Descriptive
);
}
/// Spec: "music only" maps to Score via compound check.
/// Mutation: remove "music only" compound → Normal returned.
#[test]
fn purpose_music_only_maps_to_score() {
assert_eq!(purpose("Music Only"), LabelPurpose::Score);
assert_eq!(purpose("English Music Only Track"), LabelPurpose::Score);
}
/// Spec: "score" (bare word) maps to Score.
/// Mutation: remove `has_word(&lower, "score")` check → Normal returned.
#[test]
fn purpose_score_bare_word() {
assert_eq!(purpose("Isolated Score"), LabelPurpose::Score);
assert_eq!(purpose("Score Track"), LabelPurpose::Score);
}
/// Spec: "ime" maps to Ime (alternate music track).
/// Mutation: remove `has_word(&lower, "ime")` check → Normal returned.
#[test]
fn purpose_ime_recognized() {
assert_eq!(purpose("IME"), LabelPurpose::Ime);
assert_eq!(purpose("English ime track"), LabelPurpose::Ime);
}
/// Spec: "ime" inside "time" or "anime" must NOT match.
/// Mutation: use `contains("ime")` → "anime", "time" falsely match.
#[test]
fn purpose_ime_no_substring_match() {
assert_eq!(purpose("Showtime Audio"), LabelPurpose::Normal);
assert_eq!(purpose("Anime Commentary"), LabelPurpose::Commentary);
}
/// Spec: `qualifier()` prioritizes SDH over Forced when both present.
/// Mutation: reverse the SDH check order → Forced returned when both present.
#[test]
fn qualifier_sdh_priority_over_forced() {
assert_eq!(qualifier("English Forced SDH"), LabelQualifier::Sdh);
assert_eq!(qualifier("SDH Forced"), LabelQualifier::Sdh);
}
/// Spec: "captions" maps to Sdh (closed-caption subtitles for deaf).
/// Mutation: remove `has_word(&lower, "captions")` → "captions" returns None.
#[test]
fn qualifier_captions_maps_to_sdh() {
assert_eq!(qualifier("English Captions"), LabelQualifier::Sdh);
assert_eq!(qualifier("Closed Captions"), LabelQualifier::Sdh);
}
/// Spec: "forced narrative" → Forced qualifier.
/// Mutation: remove "forced" check → None returned.
#[test]
fn qualifier_forced_narrative() {
assert_eq!(qualifier("Forced Narrative"), LabelQualifier::Forced);
assert_eq!(
qualifier("English Forced Subtitles"),
LabelQualifier::Forced
);
}
/// Spec: "rnib" → DescriptiveService qualifier.
/// Mutation: remove `has_word(&lower, "rnib")` → None returned.
#[test]
fn qualifier_rnib_maps_to_descriptive_service() {
assert_eq!(
qualifier("English RNIB"),
LabelQualifier::DescriptiveService
);
}
/// Spec: "descriptive service" compound → DescriptiveService.
/// Mutation: remove compound check → None returned.
#[test]
fn qualifier_descriptive_service_compound() {
assert_eq!(
qualifier("English Descriptive Service"),
LabelQualifier::DescriptiveService
);
}
/// Word boundary: "sdh" inside "lambdash" must not match.
/// Mutation: use `contains("sdh")` → "lambdash" falsely triggers SDH.
#[test]
-120
View File
@@ -1018,32 +1018,6 @@ mod tests {
assert!(parse(&data).is_err());
}
/// Spec: version field is bytes [4..8], copied verbatim. A "0300"
/// (UHD) playlist must report version "0300", not "0200".
#[test]
fn version_field_reflects_bytes_4_to_8() {
let mut data = build_mpls(&[(b"00001", 1, 0, 9000000)], (0, 0, 0, 0, 0, 0, 0, 0), &[]);
data[4..8].copy_from_slice(b"0300");
let pl = parse(&data).expect("should parse");
assert_eq!(pl.version, "0300");
}
/// Spec: num_play_items is a u16 at pl[6..8]. The loop must produce
/// exactly that many items when the buffer holds them. Tests that the
/// count is read from the right offset (not e.g. pl[4..6]).
#[test]
fn num_play_items_read_from_offset_6() {
// build_mpls writes num_play_items at pl[6..8]; supply 2 items.
let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None);
let data = build_mpls(
&[(b"00001", 1, 0, 4500000), (b"00002", 1, 4500000, 9000000)],
(1, 0, 0, 0, 0, 0, 0, 0),
&[video],
);
let pl = parse(&data).expect("should parse");
assert_eq!(pl.play_items.len(), 2);
}
/// Spec: connection_condition is the LOW nibble of PlayItem byte[9]
/// (high nibble is reserved/flags). A byte 0xF5 must yield 5, not 0xF5.
#[test]
@@ -1064,40 +1038,6 @@ mod tests {
assert_eq!(pl.play_items[0].connection_condition, 0x05);
}
/// Spec: in_time/out_time are big-endian u32 at PlayItem [12..16] and
/// [16..20]. Verify byte order is BE (not LE).
#[test]
fn in_out_time_big_endian() {
let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None);
let data = build_mpls(
&[(b"00001", 1, 0x01020304, 0x05060708)],
(1, 0, 0, 0, 0, 0, 0, 0),
&[video],
);
let pl = parse(&data).expect("should parse");
assert_eq!(pl.play_items[0].in_time, 0x01020304);
assert_eq!(pl.play_items[0].out_time, 0x05060708);
}
/// Spec: streams come ONLY from the first PlayItem's STN table (doc'd
/// in parse()). A second item carrying STN counts must NOT contribute
/// streams. build_mpls only writes STN on idx 0, so we verify the
/// `item_idx == 0` guard by confirming a 2-item playlist with streams
/// on item 0 reports exactly item-0's streams.
#[test]
fn streams_only_from_first_play_item() {
let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None);
let audio = build_stream_entry_audio(0x1100, 0x83, 6, 1, b"eng");
let data = build_mpls(
&[(b"00001", 1, 0, 4500000), (b"00002", 1, 4500000, 9000000)],
(1, 1, 0, 0, 0, 0, 0, 0),
&[video, audio],
);
let pl = parse(&data).expect("should parse");
assert_eq!(pl.play_items.len(), 2);
assert_eq!(pl.streams.len(), 2); // only from item 0
}
/// stream_entry() PID location for type 0x02 (stream in a SubPath
/// SubClip): subpath_id(1)+subclip_id(1) precede the PID, so PID is at
/// +4 within the entry. A parser that read +2 (type-1 layout) would
@@ -1455,66 +1395,6 @@ mod tests {
assert_eq!(pl.play_items[0].in_time, 90000);
}
/// clip_id is the 5 ASCII bytes at PlayItem [0..5]. Verify exact decode
/// (e.g. "01234"), not a truncated/padded version.
#[test]
fn clip_id_five_bytes() {
let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None);
let data = build_mpls(
&[(b"01234", 1, 0, 9000000)],
(1, 0, 0, 0, 0, 0, 0, 0),
&[video],
);
let pl = parse(&data).expect("should parse");
assert_eq!(pl.play_items[0].clip_id, "01234");
}
/// STN counts are read at STN_OFFSET+4..+12 in PlayItem order:
/// video, audio, pg, ig, sec_audio, sec_video, pip_pg, dv. Verify that
/// supplying 2 video + 2 audio + 1 PG retains all 5 in that order with
/// correct types — catches an off-by-one in the count-byte offsets.
#[test]
fn stn_counts_ordering_video_audio_pg() {
let v0 = build_stream_entry_video(0x1011, 0x1B, 6, 1, None);
let v1 = build_stream_entry_video(0x1012, 0x1B, 6, 1, None);
let a0 = build_stream_entry_audio(0x1100, 0x83, 6, 1, b"eng");
let a1 = build_stream_entry_audio(0x1101, 0x81, 3, 1, b"fra");
let pg = build_stream_entry_pg(0x1200, 0x90, b"spa");
let data = build_mpls(
&[(b"00001", 1, 0, 9000000)],
(2, 2, 1, 0, 0, 0, 0, 0),
&[v0, v1, a0, a1, pg],
);
let pl = parse(&data).expect("should parse");
assert_eq!(pl.streams.len(), 5);
assert_eq!(pl.streams[0].stream_type, 1);
assert_eq!(pl.streams[1].stream_type, 1);
assert_eq!(pl.streams[1].pid, 0x1012);
assert_eq!(pl.streams[2].stream_type, 2);
assert_eq!(pl.streams[3].stream_type, 2);
assert_eq!(pl.streams[3].pid, 0x1101);
assert_eq!(pl.streams[3].language, "fra");
assert_eq!(pl.streams[4].stream_type, 3);
assert_eq!(pl.streams[4].language, "spa");
}
/// Audio sample/channel nibbles: sa[1] high nibble = audio_format,
/// low nibble = audio_rate (BD spec audio format/sample_rate packing).
/// 0xC5 → format 12 (7.1), rate 5 (192kHz).
#[test]
fn audio_format_rate_nibble_split() {
let audio = build_stream_entry_audio(0x1100, 0x86, 12, 5, b"eng");
let data = build_mpls(
&[(b"00001", 1, 0, 9000000)],
(0, 1, 0, 0, 0, 0, 0, 0),
&[audio],
);
let pl = parse(&data).expect("should parse");
assert_eq!(pl.streams[0].audio_format, 12);
assert_eq!(pl.streams[0].audio_rate, 5);
assert_eq!(pl.streams[0].language, "eng");
}
/// data.len() exactly 40 with valid magic but playlist_start past the
/// header: parse() must hit the `playlist_start + 10 > data.len()`
/// guard. A 40-byte buffer with playlist_start=40 has no PlayList body.
-21
View File
@@ -631,27 +631,6 @@ mod tests {
// --- YCbCr → RGB green channel + neutral chroma ---
#[test]
fn ycbcr_green_channel_formula() {
// G = Y - 0.344*(Cb-128) - 0.714*(Cr-128). For pure-ish green choose
// Y=145, Cb=54, Cr=34: G should be high, R and B low. (Full-range BT.601
// per the module's deliberate convention.)
let [r, g, b] = ycbcr_to_rgb(&[0x00, 145, 54, 34]);
assert!(g > 200, "G high for green, got {g}");
assert!(r < 80, "R low for green, got {r}");
assert!(b < 80, "B low for green, got {b}");
}
#[test]
fn ycbcr_neutral_chroma_is_grey() {
// Cb=Cr=128 (neutral) → R=G=B=Y for any Y. (Confirms the chroma terms
// vanish at 128.)
for y in [0u8, 64, 128, 200, 255] {
let [r, g, b] = ycbcr_to_rgb(&[0x00, y, 128, 128]);
assert_eq!([r, g, b], [y, y, y], "neutral chroma → grey at Y={y}");
}
}
#[test]
fn ycbcr_blue_channel_clamps_high() {
// B = Y + 1.772*(Cb-128). Y=128, Cb=255 → 128 + 1.772*127 ≈ 353 → clamp 255.
-11
View File
@@ -1518,17 +1518,6 @@ mod tests {
// --- IRAP keyframe boundary values ---
#[test]
fn irap_lower_boundary_type_16_is_keyframe() {
// BLA_W_LP = 16, the inclusive lower boundary of NAL_BLA_W_LP..=23.
let mut parser = HevcParser::new();
let mut data = vec![0x00, 0x00, 0x01];
data.extend_from_slice(&hevc_nal_header(16));
data.extend_from_slice(&[0x10, 0x20]);
let f = parser.parse(&make_pes(data, Some(0)));
assert!(f[0].keyframe);
}
#[test]
fn type_15_just_below_irap_not_keyframe() {
// Type 15 (RASL_R) is one below the IRAP range and must NOT be a keyframe.
-11
View File
@@ -225,17 +225,6 @@ mod tests {
assert_eq!(f[0].data, vec![0xAB], "5 BD bytes → 1 PCM byte");
}
#[test]
fn bd_exactly_four_bytes_dropped() {
// Exactly 4 bytes = header only: `len <= offset` (4 <= 4) → dropped.
let mut parser = LpcmParser::new();
assert!(
parser
.parse(&make_pes(vec![0x00, 0x01, 0x00, 0x91], Some(0)))
.is_empty()
);
}
#[test]
fn bd_three_bytes_dropped() {
// Fewer than the 4-byte header → dropped, no panic / no underflow slice.
-14
View File
@@ -803,20 +803,6 @@ mod tests {
assert_eq!(f2[0].pts_ns, 0, "no PTS/DTS → 0");
}
#[test]
fn frame_data_is_whole_pes_not_just_picture() {
// The emitted frame data is the ENTIRE PES payload (pes.data.clone()),
// not just the picture NAL — MPEG-2 ES is muxed as-is. Confirm a seq
// header + picture PES emits the whole buffer.
let mut parser = Mpeg2Parser::new();
let mut data = make_seq_header(720, 480, 3, 4);
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
data.extend_from_slice(&[0x12, 0x34]);
let f = parser.parse(&make_pes(data.clone(), Some(0)));
assert_eq!(f.len(), 1);
assert_eq!(f[0].data, data, "frame data = whole PES payload");
}
#[test]
fn parser_resolution_method() {
let mut parser = Mpeg2Parser::new();
-11
View File
@@ -433,17 +433,6 @@ mod tests {
// --- duration computation and clamping ---
#[test]
fn duration_is_clear_minus_display() {
// BlockDuration = clear_pts - display_pts (in ns). display @ 90000 (1s),
// clear @ 450000 (5s) → duration 4s.
let mut parser = PgsParser::new();
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
let f = parser.parse(&make_pes(pcs_bytes(0), Some(450000)));
assert_eq!(f[0].pts_ns, 1_000_000_000);
assert_eq!(f[0].duration_ns, Some(4_000_000_000));
}
#[test]
fn duration_clamps_to_zero_when_clear_precedes_display() {
// A clear PTS earlier than the display PTS (corrupt/out-of-order stream)
-9
View File
@@ -163,15 +163,6 @@ mod tests {
// --- skip_start_code: boundary / form selection ---
#[test]
fn skip_4byte_preferred_over_3byte_when_extra_zero_present() {
// `00 00 00 01`: the function must recognise the 4-byte form (return
// pos+4), not stop at a phantom 3-byte interpretation. data[pos+2]==0x00
// and data[pos+3]==0x01 select the 4-byte branch.
let data = [0x00, 0x00, 0x00, 0x01, 0x42];
assert_eq!(skip_start_code(&data, 0), Some(4));
}
#[test]
fn skip_start_code_at_nonzero_pos() {
// skip must honour pos: a 3-byte code at offset 2 returns 2+3 = 5.
-6
View File
@@ -526,12 +526,6 @@ mod tests {
assert_eq!(truehd_channels(1 << 20), None);
}
#[test]
fn truehd_channels_71_layout_low5_bits() {
// Standard 7.1: 8ch bits 0-4 = L/R(2)+C(1)+LFE(1)+Ls/Rs(2)+Lb/Rb(2) = 8.
assert_eq!(truehd_channels(0x1F), Some(8));
}
// --- truehd_channels_from_stream: major-sync variant bit + scan ---
#[test]
-41
View File
@@ -588,47 +588,6 @@ mod tests {
assert_eq!(parse_vc1_resolution(&sh), Some((8192, 8192)));
}
#[test]
fn resolution_field_is_12_bits_no_higher() {
// Asserting the field width: a width one step above the max (8194 →
// coded_w 4096) overflows the 12-bit MAX_CODED_WIDTH field (4096 & 0xFFF
// = 0), so it cannot encode 8194 — it wraps to (0+1)*2 = 2. This proves
// the 12-bit masking in the parser, i.e. it never reads a 13th bit.
let sh = make_ap_seq_header(8194, 720);
assert_eq!(
parse_vc1_resolution(&sh),
Some((2, 720)),
"coded_w field is masked to 12 bits → 4096 wraps to 0 → width 2"
);
}
#[test]
fn resolution_deescapes_emulation_prevention() {
// VC-1 Annex-B EBDU payload may carry an emulation-prevention 0x03 after
// a 00 00 run. The resolution parser must de-escape before bit
// extraction; an EP byte in the first few payload bytes would otherwise
// shift every later bit and corrupt the dimensions. Build a header whose
// de-escaped payload encodes 1280x720, then splice 00 00 03 into the raw
// payload and confirm it still decodes 1280x720.
let base = make_ap_seq_header(1280, 720);
// base = [00 00 01 0F][5 payload bytes]. Insert a benign EP run that
// de-escapes away: find a spot where two zeros precede our inserted 0x03.
// Construct payload manually: prepend 00 00 03 then the real 5 bytes; the
// de-escaper drops the 0x03, leaving 00 00 + the 5 bytes → but that
// shifts the fields. Instead, the real coverage: the de-escaper collects
// 5 bytes skipping EP. Put the EP at the very front so after stripping we
// still recover the 5 meaningful bytes... that changes leading bits.
// Simpler grounded check: a payload with a trailing EP byte (after the 5
// needed bytes) must not change the result, since only 5 are collected.
let mut sh = base.clone();
sh.extend_from_slice(&[0x00, 0x00, 0x03, 0xFF]); // trailing EP run
assert_eq!(
parse_vc1_resolution(&sh),
Some((1280, 720)),
"trailing EP bytes beyond the 5 collected must not affect parsing"
);
}
// --- codec_private BITMAPINFOHEADER field layout ---
#[test]
-8
View File
@@ -551,14 +551,6 @@ mod tests {
assert!(!starts_with_start_code(&[]));
}
#[test]
fn three_byte_start_code_only_buffer_passes_through() {
// A buffer that is exactly a 3-byte start code prefix is passed through
// (the probe wins before length parsing).
let raw = [0x00, 0x00, 0x01, 0x40, 0x01];
assert_eq!(length_prefixed_to_annex_b(&raw), raw);
}
// --- HevcMux: params-once + error semantics ---
#[test]
+73 -20
View File
@@ -270,6 +270,26 @@ fn monotonic_ts(prev: Option<i64>, pts_ms: i64) -> i64 {
}
}
/// Per-track block timestamp. The strictly-monotonic nudge is applied to
/// AUDIO/SUBTITLE tracks only; VIDEO (track 0) is returned UNCHANGED.
///
/// With B-frames, a video frame's presentation PTS is legitimately
/// non-monotonic in decode/storage order (a B-frame sits between its anchors,
/// below the frame stored just before it). Forcing it strictly-increasing
/// clobbers those PTS to prev+1ms — a `copy` remux preserves the (wrong) value,
/// but a decoder derives DTS from the HEVC POC and finds them colliding
/// ("non monotonically increasing dts", thousands per title). Matroska
/// SimpleBlock permits non-monotonic block timestamps (signed block-relative
/// offsets), so video keeps its true PES PTS; only no-reorder tracks (audio,
/// subtitles), where a same-millisecond collision IS a real defect, get nudged.
fn block_ts(track_idx: usize, prev: Option<i64>, pts_ms: i64) -> i64 {
if track_idx == 0 {
pts_ms
} else {
monotonic_ts(prev, pts_ms)
}
}
/// Encode a Matroska track number as an EBML VINT into a stack buffer,
/// returning the buffer and the used length. Track numbers are small (1-based,
/// a handful of tracks), so 1 byte covers `< 0x80` and 2 bytes covers the rest;
@@ -550,12 +570,19 @@ impl<W: Write + Seek> MkvMuxer<W> {
// kept keyframe are clamped to t=0 rather than corrupting the timeline.
let pts_ms = (raw_ms - base).max(0);
// Enforce strictly-monotonic per-track block timestamps. Some audio PES
// PTS truncate to the same millisecond as the previous frame (or, rarely,
// tick back 1ms), which surfaces as "non-monotonic DTS" and is rejected
// by ffmpeg/strict players. Nudge to prev+1ms — sub-frame, inaudible,
// and A/V sync is unaffected at millisecond granularity.
let pts_ms = monotonic_ts(self.last_pts_ms.get(&track_idx).copied(), pts_ms);
// Strictly-monotonic block timestamps — AUDIO/SUBTITLE ONLY. Some audio
// PES PTS truncate to the same millisecond as the previous frame (or
// tick back 1ms); nudge those to prev+1ms (sub-frame, inaudible).
//
// VIDEO (track 0) is EXEMPT: with B-frames, presentation PTS is
// legitimately non-monotonic in decode/storage order (a B-frame's PTS
// sits between its anchors, below the frame stored before it). Forcing
// it strictly-increasing clobbers those PTS to prev+1ms, which a `copy`
// remux preserves but a decoder rejects — it derives DTS from the HEVC
// POC and finds them colliding ("non monotonically increasing dts").
// Matroska SimpleBlock permits non-monotonic block timestamps (negative
// block-relative offsets), so leave the true PES PTS intact for video.
let pts_ms = block_ts(track_idx, self.last_pts_ms.get(&track_idx).copied(), pts_ms);
let needs_new_cluster = !self.cluster_open
|| (is_video_key && (pts_ms - self.cluster_ts_ms) >= CLUSTER_DURATION_MS);
@@ -974,6 +1001,46 @@ mod tests {
assert_eq!(out, [1000, 1001, 1002, 1003, 1032, 1033, 1064]);
}
#[test]
fn block_ts_exempts_video_from_monotonic_nudge() {
// VIDEO (track 0) keeps its true PTS even when non-monotonic in storage
// order — a B-frame whose presentation PTS sits below the frame stored
// before it must NOT be nudged to prev+1ms (that clobbering is what
// produced the "non monotonically increasing dts" flood on decode).
assert_eq!(
block_ts(0, Some(1040), 1000),
1000,
"video B-frame PTS preserved"
);
assert_eq!(
block_ts(0, Some(1000), 1000),
1000,
"video dup-ms PTS preserved"
);
// A realistic decode-order GOP (I, then B-frames dipping below it):
// every value passes through untouched for video.
let gop = [1000i64, 960, 920, 1080, 1040];
let mut prev = None;
let out: Vec<i64> = gop
.iter()
.map(|&p| {
let t = block_ts(0, prev, p);
prev = Some(t);
t
})
.collect();
assert_eq!(out, gop, "video timestamps must be left exactly as-is");
// AUDIO/SUBTITLE (track != 0) still get the strictly-monotonic nudge —
// a same-ms collision there is a real defect.
assert_eq!(block_ts(1, Some(1000), 1000), 1001, "audio dup-ms nudged");
assert_eq!(
block_ts(2, Some(1001), 1000),
1002,
"subtitle back-tick nudged"
);
}
#[test]
fn mkv_multiple_tracks() {
let buf = Cursor::new(Vec::new());
@@ -2036,20 +2103,6 @@ mod tests {
// element ID bytes.
// ============================================================
#[test]
fn seekhead_seek_id_values_match_target_element_ids() {
let tracks = [make_video_track(), make_audio_track()];
let (data, _) = mux_to_bytes(&tracks, &[], &frames_for(10.0, 1.0));
let entries = parse_seekhead(&data);
// The decoded SeekID for each entry must equal a real Matroska element
// ID (Info, Tracks, Cues). parse_seekhead reads SeekID as a uint; the
// value is the big-endian element ID.
let ids: Vec<u32> = entries.iter().map(|(id, _)| *id).collect();
assert!(ids.contains(&ebml::INFO));
assert!(ids.contains(&ebml::TRACKS));
assert!(ids.contains(&ebml::CUES));
}
// ============================================================
// dolby_vision_config (dvcC / DOVIDecoderConfigurationRecord) bit
// packing. Byte 2: profile(7 bits) << 1 | level high bit. Byte 3:
-8
View File
@@ -1210,14 +1210,6 @@ mod tests {
// 0x1100 + (tnum-2) formula).
// ============================================================
#[test]
fn ts_pid_for_track_mid_range_formula() {
// tnum 10 → 0x1100 + 8 = 0x1108.
assert_eq!(ts_pid_for_track(10).unwrap(), 0x1108);
// tnum 0x100 → 0x1100 + 0xFE = 0x11FE.
assert_eq!(ts_pid_for_track(0x100).unwrap(), 0x11FE);
}
// ============================================================
// CLUSTER_TIMESTAMP overflow guard — a value above i64::MAX would cast
// to a large negative i64 and poison every block PTS in the cluster.
+2 -12
View File
@@ -152,8 +152,8 @@ mod tests {
fn path_str_returns_address_for_network() {
// network:// path_str is the host:port address verbatim.
assert_eq!(
parse_url("network://192.168.1.1:9000").path_str(),
"192.168.1.1:9000"
parse_url("network://203.0.113.5:9000").path_str(),
"203.0.113.5:9000"
);
}
@@ -231,16 +231,6 @@ mod tests {
);
}
#[test]
fn write_seek_blanket_impl_covers_cursor() {
// WriteSeek is the MKV sink bound (Write + Seek). The blanket impl
// must opt in any T: Write+Seek; Cursor<Vec<u8>> is the canonical
// in-memory seekable sink. Compile-time proof via a generic fn.
fn assert_writeseek<T: super::super::WriteSeek>(_: &T) {}
let cur = std::io::Cursor::new(Vec::<u8>::new());
assert_writeseek(&cur);
}
#[test]
fn first_matching_scheme_wins_no_double_prefix_confusion() {
// A path component that itself looks like another scheme must be
-26
View File
@@ -87,30 +87,4 @@ mod tests {
// size, track index, or post-finish state.
sink.write(&frame).unwrap();
}
/// info() must return the title the sink was constructed with, unchanged
/// — the Stream trait contract requires info() be stable and reflect the
/// supplied metadata (the muxer reads stream layout from it).
#[test]
fn info_reflects_constructed_title() {
let mut title = DiscTitle::empty();
title.playlist = "BenchTitle".into();
title.playlist_id = 7;
let sink = NullStream::new(&title);
assert_eq!(sink.info().playlist, "BenchTitle");
assert_eq!(sink.info().playlist_id, 7);
}
/// The write-only read() guard must hold on EVERY call, not just the
/// first — a caller that retries read() after the initial error must
/// keep getting StreamWriteOnly, never a stale Ok(None).
#[test]
fn read_stays_write_only_across_repeated_calls() {
let title = DiscTitle::empty();
let mut sink = NullStream::new(&title);
for _ in 0..3 {
let err = Stream::read(&mut sink).expect_err("read on a sink must always error");
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
}
}
}
-23
View File
@@ -889,21 +889,6 @@ mod tests {
assert_eq!(parse_pts(&encode_pts(max, 0x20)), max);
}
#[test]
fn parse_pts_ignores_marker_bits_in_value() {
// The marker bits (LSB of bytes 0,2,4) are NOT part of the 33-bit
// value. Two encodings differing only in marker bits decode equal.
let v = 0x1_2345_6789u64 & ((1 << 33) - 1);
let a = encode_pts(v, 0x20);
let mut b = a;
// markers are already 1; the value bits must dominate regardless.
b[0] |= 0x01;
b[2] |= 0x01;
b[4] |= 0x01;
assert_eq!(parse_pts(&a), v);
assert_eq!(parse_pts(&b), v);
}
// ── pack header (0xBA) framing ────────────────────────────────────────
#[test]
@@ -1151,14 +1136,6 @@ mod tests {
);
}
#[test]
fn empty_feed_then_flush_is_empty() {
// No input at all → nothing to emit, no panic.
let mut demuxer = PsDemuxer::new();
assert!(demuxer.feed(&[]).is_empty());
assert!(demuxer.flush().is_empty());
}
#[test]
fn pes_header_data_length_skips_pts_when_flag_unset() {
// If pts_dts_flags == 0 the 5 "PTS" bytes after the fixed header are
-26
View File
@@ -700,13 +700,6 @@ mod tests {
);
}
/// output() to null:// must succeed (it's the canonical write sink).
#[test]
fn output_null_succeeds() {
let t = DiscTitle::empty();
assert!(output("null://", &t).is_ok());
}
/// output() to an unknown scheme must surface StreamUrlInvalid
/// (E9002 → InvalidInput).
#[test]
@@ -964,23 +957,4 @@ mod tests {
);
assert!(res.is_err(), "zero batch_sectors must be rejected");
}
/// info() on the assembled pipeline returns the title it was built with —
/// the consumer reads stream layout from here before muxing.
#[test]
fn build_iso_pipeline_info_returns_title() {
let mut title = aac_audio_title(0x1100);
title.playlist = "PipelineTitle".into();
let stream = build_iso_pipeline(
MemSource { data: Vec::new() },
title,
DecryptKeys::None,
8192,
ContentFormat::BdTs,
None,
None,
)
.unwrap();
assert_eq!(stream.info().playlist, "PipelineTitle");
}
}
-7
View File
@@ -231,13 +231,6 @@ mod tests {
assert_eq!(s.codec_private(99), None);
}
/// info() on the write side reflects the supplied title.
#[test]
fn output_info_reflects_title() {
let s = StdioStream::output(&title_with_codec_privates());
assert_eq!(s.info().playlist, "StdioTitle");
}
/// A fresh input stream defaults to an empty title until a header is
/// parsed — info() must not invent stream metadata.
#[test]
-23
View File
@@ -885,27 +885,4 @@ mod tests {
assert_eq!(got.len(), big.len(), "no bytes lost in the PES split");
assert_eq!(got, big, "split audio reassembles byte-for-byte");
}
#[test]
fn af_plus_payload_always_fills_184() {
// Invariant from write_pes_chain: af_bytes + payload_len == 184 on
// every packet (so the 192-byte frame is exact). Verify for a video
// keyframe (which forces an RAI adaptation field on packet 1).
let mut sink: Vec<u8> = Vec::new();
{
let mut mux = TsMuxer::new(&mut sink, &[VIDEO_PID]);
let idr = fake_hevc_nal(19, 400);
mux.write_frame(0, 0, true, &idr).unwrap();
mux.finish().unwrap();
}
let packets = parse_bd_ts(&sink);
for p in packets.iter().filter(|p| p.pid == VIDEO_PID) {
let af_total = p.af.as_ref().map(|a| a.len() + 1).unwrap_or(0); // +1 length byte
assert_eq!(
af_total + p.payload.len(),
184,
"AF area + payload must fill the 184-byte TS body"
);
}
}
}
-69
View File
@@ -493,55 +493,6 @@ mod tests {
);
}
/// serialize rejects data larger than MAX_FRAME_SIZE.
/// Spec: doc says "A frame larger than this is rejected on write rather than written
/// and then hard-erroring mid-stream on read."
/// Mutation: removing the size check serializes an unreadable frame.
#[test]
fn serialize_rejects_data_exceeding_max_frame_size() {
// We can't actually allocate 256 MiB in a test; instead we construct a
// PesFrame whose data len is exactly MAX_FRAME_SIZE+1 by building a
// custom case. We test the boundary via the const.
assert!(
MAX_FRAME_SIZE == 256 * 1024 * 1024,
"MAX_FRAME_SIZE constant changed — update this test"
);
// Verify the error path via the const: MAX_FRAME_SIZE+1 won't fit.
// We can't allocate 256 MiB + 1 in CI, so we test the length check
// indirectly: a frame at exactly MAX_FRAME_SIZE must succeed on
// serialize (the len itself fits in u32). We can also do a small
// trick: check that the error kind is correct for a simulated large size.
// The simplest safe test: confirm MAX_FRAME_SIZE fits in a u32.
assert!(
MAX_FRAME_SIZE <= u32::MAX as usize,
"MAX_FRAME_SIZE must fit in u32 for wire length field"
);
}
/// deserialize round-trips a negative pts (i64 can be negative).
/// Spec: pts is a signed i64 nanosecond timestamp; negative values are valid
/// (e.g. pts before stream start). Wire format is little-endian i64.
/// Mutation: using u64 for pts interpretation makes negative values wrap.
#[test]
fn deserialize_round_trips_negative_pts() {
let frame = PesFrame {
track: 1,
pts: -12345678_i64,
keyframe: false,
data: vec![0xDE, 0xAD],
duration_ns: None,
};
let mut buf = Vec::new();
frame.serialize(&mut buf).unwrap();
let mut cursor = std::io::Cursor::new(buf);
let got = PesFrame::deserialize(&mut cursor).unwrap().unwrap();
assert_eq!(
got.pts, -12345678_i64,
"negative pts must survive round-trip"
);
assert_eq!(got.data, vec![0xDE, 0xAD]);
}
/// deserialize round-trips pts=0 and pts=i64::MAX correctly.
/// Mutation: off-by-one in byte indices [1..9] shifts the pts value.
#[test]
@@ -666,24 +617,4 @@ mod tests {
cs.write(&f2).unwrap();
assert_eq!(cs.bytes_written(), 5, "must accumulate 3+2=5 bytes");
}
/// CountingStream.finish() delegates to the inner stream (no panic).
/// Mutation: not calling inner.finish() silently drops any buffered data.
#[test]
fn counting_stream_finish_delegates_to_inner() {
let mut cs = CountingStream::new(Box::new(MockStream::new(Vec::new())));
// Must not panic.
cs.finish().unwrap();
}
/// CountingStream.info() and codec_private() delegate to inner.
/// Mutation: returning a default title instead of inner.info() drops disc metadata.
#[test]
fn counting_stream_delegates_info_and_codec_private() {
let cs = CountingStream::new(Box::new(MockStream::new(Vec::new())));
// info() must return the inner stream's title without panicking.
let _ = cs.info();
// codec_private defaults to None for MockStream.
assert!(cs.codec_private(0).is_none());
}
}
-34
View File
@@ -386,17 +386,6 @@ mod tests {
// ── New comprehensive tests ────────────────────────────────────────────────
/// decode_hex rejects odd-length hex strings.
/// Spec: hex encoding uses pairs of hex digits; odd length is malformed.
/// Mutation: padding an odd-length string instead of erroring silently
/// misinterprets the last nibble.
#[test]
fn decode_hex_rejects_odd_length() {
assert!(decode_hex("abc").is_err(), "odd length must be rejected");
assert!(decode_hex("a").is_err());
assert!(decode_hex("abcde").is_err());
}
/// decode_hex accepts empty string → empty Vec.
/// Mutation: returning an error on empty input breaks empty-field handling.
#[test]
@@ -422,15 +411,6 @@ mod tests {
);
}
/// decode_hex rejects non-hex ASCII characters.
/// Mutation: treating 'g' or 'z' as 0 silently corrupts key material.
#[test]
fn decode_hex_rejects_non_hex_ascii() {
assert!(decode_hex("gg").is_err(), "'g' is not a hex digit");
assert!(decode_hex("0z").is_err(), "'z' is not a hex digit");
assert!(decode_hex("0 ").is_err(), "space is not a hex digit");
}
/// parse_hex4 rejects an 8-hex-char string (4 bytes) correctly.
/// Spec: the signature field is exactly 4 bytes = 8 hex chars.
/// Mutation: accepting 6 hex chars (3 bytes) would pass a wrong-length signature.
@@ -462,20 +442,6 @@ mod tests {
assert_eq!(Platform::Renesas.name(), "Renesas");
}
/// Platform names do not contain English prose — they are identifiers.
/// Mutation: adding " (unsupported)" to the Renesas name would break key lookup.
#[test]
fn platform_name_has_no_whitespace_only_words() {
for p in [Platform::Mt1959A, Platform::Mt1959B, Platform::Renesas] {
let name = p.name();
assert!(!name.is_empty(), "platform name must not be empty");
// Each whitespace-separated token must be non-empty (no trailing spaces).
for token in name.split_whitespace() {
assert!(!token.is_empty());
}
}
}
/// find_by_drive_id: exact match (including firmware_date) wins over loose match.
/// Spec: two-pass — first an exact match including firmware_date, then looser.
/// Build two synthetic ProfilesFile entries that differ only by firmware_date,
-44
View File
@@ -810,22 +810,6 @@ mod parse_sense_tests {
assert_eq!(d.ascq, 0x05, "n=14 reaches ASCQ at offset 13");
}
#[test]
fn sb_len_wr_clamped_to_slice_len() {
// Doc: n = min(sb_len_wr, sense.len()). A caller claiming 200
// bytes written into a 14-byte slice must not read out of bounds;
// the effective n is the slice length.
let mut s = [0u8; 14];
s[0] = 0x70;
s[2] = 0x03;
s[12] = 0x11;
s[13] = 0x05;
let d = parse_sense(&s, 200);
assert_eq!(d.sense_key, 3);
assert_eq!(d.asc, 0x11);
assert_eq!(d.ascq, 0x05);
}
#[test]
fn n_exactly_three_decodes_key_only() {
// n==3 is the minimum that passes the n<3 early-return. For fixed
@@ -900,34 +884,6 @@ mod scsi_sense_predicate_tests {
}
}
#[test]
fn hardware_error_is_not_marginal() {
// HARDWARE ERROR (4) is explicitly non-recoverable per doc.
assert!(!s(SENSE_KEY_HARDWARE_ERROR).is_marginal());
assert!(s(SENSE_KEY_HARDWARE_ERROR).is_hardware_error());
}
#[test]
fn data_protect_not_marginal() {
// DATA PROTECT (7) = AACS/region/write-protect; retry won't help.
assert!(!s(SENSE_KEY_DATA_PROTECT).is_marginal());
assert!(s(SENSE_KEY_DATA_PROTECT).is_data_protect());
}
#[test]
fn illegal_request_not_marginal() {
// ILLEGAL REQUEST (5) = bad CDB; not marginal.
assert!(!s(SENSE_KEY_ILLEGAL_REQUEST).is_marginal());
assert!(s(SENSE_KEY_ILLEGAL_REQUEST).is_illegal_request());
}
#[test]
fn unit_attention_not_marginal() {
// UNIT ATTENTION (6) = state change; caller rescans, not retries.
assert!(!s(SENSE_KEY_UNIT_ATTENTION).is_marginal());
assert!(s(SENSE_KEY_UNIT_ATTENTION).is_unit_attention());
}
#[test]
fn each_specific_predicate_is_exclusive() {
// Each is_* predicate matches exactly its one key and no other.