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:
+196
@@ -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}`"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user