libfreemkv 0.31.2: comprehensive spec-grounded test suite (~950 tests)

Test-hardening release, no runtime changes. Adds spec-grounded unit tests
across the silent-corruption surfaces — UDF/MPLS/CLPI/IFO parsing, BD/DVD
title + extent assembly, AACS/CSS key handling, TS/PS demux + codec parsers,
MKV/EBML container output, the mux pipeline, sector prefetch + decrypt
decorator, drive/SCSI sense decoding, label extraction, and core I/O. Each
test is grounded in the format spec or real on-disc behavior and verified to
fail under a targeted source mutation. No behavior changed.
This commit is contained in:
Matthew Jackson
2026-06-07 22:28:29 -07:00
parent 2a55bab3ed
commit 8000bae177
85 changed files with 22998 additions and 1 deletions
+196
View File
@@ -254,4 +254,200 @@ mod tests {
assert_eq!(id.vendor_specific.trim(), "16/04/");
assert_eq!(id.firmware_date, "201604250000");
}
// ── New comprehensive tests ────────────────────────────────────────────────
/// ascii_field with a buffer shorter than `start` returns empty string
/// rather than panicking.
/// Spec: SPC-4 §6.4.2 — bytes[8:16] are vendor ID; a truncated buffer
/// (e.g. a device that reports fewer than 8 bytes) must not panic.
/// Mutation: removing the `data.len() > start` guard makes it panic on short inputs.
#[test]
fn ascii_field_short_buffer_returns_empty() {
// Buffer of length 5: start=8 is beyond the end → empty string.
let buf = vec![0u8; 5];
let result = ascii_field(&buf, 8, 16); // SPC-4 vendor ID range
assert!(result.is_empty(), "short buffer must yield empty string");
}
/// ascii_field with a buffer that covers start but not end is clamped.
/// Spec: `ascii_field` documents "clamps to data.len()".
/// Mutation: using `end` directly without `min(data.len())` panics here.
#[test]
fn ascii_field_partial_buffer_is_clamped_not_panicked() {
// Buffer of length 12: vendor_id range is [8..16], but only [8..12] present.
let mut buf = vec![0u8; 12];
buf[8..12].copy_from_slice(b"SONY");
let result = ascii_field(&buf, 8, 16);
// Must not panic; the returned string holds what we wrote.
assert_eq!(result, "SONY");
}
/// from_inquiry extracts the product_id field from INQUIRY bytes [16:32].
/// Spec: SPC-4 §6.4.2 — PRODUCT IDENTIFICATION at offset 16, length 16.
/// Mutation: shifting the product_id slice to [8:24] makes this fail.
#[test]
fn from_inquiry_extracts_product_id_at_offset_16() {
let mut inquiry = vec![0u8; 96];
// Leave vendor_id (8..16) as zeros, write product_id at 16..32.
inquiry[16..32].copy_from_slice(b"BD-RW BDR-209M");
let id = DriveId::from_inquiry(&inquiry, "");
assert_eq!(
id.product_id, "BD-RW BDR-209M",
"product_id must come from INQUIRY bytes 16..32 (SPC-4 §6.4.2)"
);
}
/// from_inquiry extracts product_revision from INQUIRY bytes [32:36].
/// Spec: SPC-4 §6.4.2 — PRODUCT REVISION LEVEL at offset 32, length 4.
/// Mutation: reading revision from [36:40] produces the wrong value.
#[test]
fn from_inquiry_extracts_revision_at_offset_32() {
let mut inquiry = vec![0u8; 96];
inquiry[32..36].copy_from_slice(b"1.53");
let id = DriveId::from_inquiry(&inquiry, "");
assert_eq!(
id.product_revision, "1.53",
"product_revision must come from INQUIRY bytes 32..36 (SPC-4 §6.4.2)"
);
}
/// from_inquiry extracts vendor_specific from INQUIRY bytes [36:43].
/// Spec: SPC-4 §6.4.2 — VENDOR SPECIFIC at offset 36, length 8.
/// Mutation: reading vendor_specific from [32:39] returns the revision instead.
#[test]
fn from_inquiry_extracts_vendor_specific_at_offset_36() {
let mut inquiry = vec![0u8; 96];
inquiry[36..43].copy_from_slice(b"MM01234");
let id = DriveId::from_inquiry(&inquiry, "");
assert_eq!(
id.vendor_specific, "MM01234",
"vendor_specific must come from INQUIRY bytes 36..43 (SPC-4 §6.4.2)"
);
}
/// 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]
fn from_inquiry_stores_raw_inquiry() {
let mut inquiry = vec![0u8; 96];
inquiry[8..16].copy_from_slice(b"TESTDRVR");
let id = DriveId::from_inquiry(&inquiry, "");
assert_eq!(
id.raw_inquiry, inquiry,
"raw_inquiry must preserve the full 96-byte buffer"
);
}
/// 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.
#[test]
fn from_drive_gc_failure_yields_empty_firmware_date() {
struct GcFailTransport;
impl ScsiTransport for GcFailTransport {
fn execute(
&mut self,
cdb: &[u8],
_dir: DataDirection,
buf: &mut [u8],
_timeout_ms: u32,
) -> Result<ScsiResult> {
if cdb.first() == Some(&0x12) {
// INQUIRY succeeds with a plausible response.
buf[8..16].copy_from_slice(b"TESTDRV ");
buf[16..32].copy_from_slice(b"FAKE DRIVE MODEL");
buf[32..36].copy_from_slice(b"0001");
buf[36..43].copy_from_slice(b"X000001");
Ok(ScsiResult {
status: 0,
bytes_transferred: buf.len(),
sense: [0u8; 32],
})
} else {
// GET CONFIGURATION fails.
Err(crate::error::Error::ScsiError {
opcode: cdb[0],
status: crate::scsi::SCSI_STATUS_CHECK_CONDITION,
sense: None,
})
}
}
}
let mut t = GcFailTransport;
let id = DriveId::from_drive(&mut t).expect("from_drive must succeed despite GC failure");
assert!(
id.firmware_date.is_empty(),
"firmware_date must be empty when GC fails"
);
assert!(
id.raw_gc_010c.is_empty(),
"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}`"
);
}
}