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:
@@ -120,3 +120,105 @@ pub fn mask_bytes(data: &[u8]) -> Vec<u8> {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! Privacy-masking + capture-orchestration tests.
|
||||
//!
|
||||
//! `mask_string` / `mask_bytes` redact identifying characters before
|
||||
//! a drive capture leaves the machine: every ASCII letter → 'A',
|
||||
//! every ASCII digit → '0', everything else (punctuation, spaces,
|
||||
//! control bytes, non-ASCII) is preserved verbatim so structural
|
||||
//! framing (offsets, separators) survives for diffing.
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mask_string_letters_become_a_digits_become_zero() {
|
||||
// Mixed case letters all collapse to 'A'; digits to '0'.
|
||||
assert_eq!(mask_string("HL-DT-ST"), "AA-AA-AA");
|
||||
assert_eq!(mask_string("BU40N"), "AA00A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_string_preserves_non_alnum_punctuation_and_space() {
|
||||
// Separators and spaces must be preserved so the masked output
|
||||
// keeps the same shape as the original (the whole point of a
|
||||
// structure-preserving redaction).
|
||||
assert_eq!(mask_string("1.04"), "0.00");
|
||||
assert_eq!(mask_string("a b-c.d_e"), "A A-A.A_A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_string_preserves_non_ascii_chars() {
|
||||
// is_ascii_alphabetic/is_ascii_digit are false for non-ASCII, so
|
||||
// multibyte chars pass through unchanged (no mojibake, no panic).
|
||||
// 'c','a','f' are ASCII letters → 'A'; 'é' is non-ASCII →
|
||||
// preserved; '9' → '0'.
|
||||
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'.
|
||||
assert_eq!(mask_bytes(b"HL-DT-ST"), b"AA-AA-AA".to_vec());
|
||||
assert_eq!(mask_bytes(b"1.04"), b"0.00".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_bytes_preserves_non_alnum_and_high_bytes() {
|
||||
// Control bytes (0x00), high bytes (0xFF), and punctuation are
|
||||
// not ASCII alnum and must survive verbatim — INQUIRY payloads
|
||||
// are space-padded binary and the framing must be diffable.
|
||||
let input = [0x00u8, b'A', 0x20, b'7', 0xFF, b'-'];
|
||||
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
|
||||
// code would silently capture the same feature twice (and bloat
|
||||
// the report). Each MMC-6 feature code must be unique.
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for &(code, _name) in FEATURES {
|
||||
assert!(seen.insert(code), "duplicate feature code {code:#06x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_table_includes_aacs_010d() {
|
||||
// AACS (0x010D) is the feature that gates UHD decryption capture;
|
||||
// it must be in the table or AACS drives capture incompletely.
|
||||
assert!(
|
||||
FEATURES.iter().any(|&(c, _)| c == 0x010D),
|
||||
"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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1044,4 +1044,394 @@ mod command_tests {
|
||||
let mut d = drive_with(buf);
|
||||
assert_eq!(d.drive_status(), DriveStatus::DiscPresent);
|
||||
}
|
||||
|
||||
// ── Mocks for Drive::read single-shot semantics + CDB encoding ──
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Records the CDB of every execute() and returns a programmable
|
||||
/// outcome. Lets a test assert both the bytes sent to the drive and
|
||||
/// how the driver translates the transport result.
|
||||
struct RecordingTransport {
|
||||
last_cdb: Arc<Mutex<Vec<u8>>>,
|
||||
last_timeout: Arc<Mutex<u32>>,
|
||||
outcome: TransportOutcome,
|
||||
}
|
||||
enum TransportOutcome {
|
||||
/// Report this many bytes transferred (data left as-is).
|
||||
Ok(usize),
|
||||
/// Fail with a ScsiError carrying this status + optional sense.
|
||||
Scsi(u8, Option<crate::scsi::ScsiSense>),
|
||||
}
|
||||
impl ScsiTransport for RecordingTransport {
|
||||
fn execute(
|
||||
&mut self,
|
||||
cdb: &[u8],
|
||||
_dir: DataDirection,
|
||||
_data: &mut [u8],
|
||||
timeout_ms: u32,
|
||||
) -> Result<ScsiResult> {
|
||||
*self.last_cdb.lock().unwrap() = cdb.to_vec();
|
||||
*self.last_timeout.lock().unwrap() = timeout_ms;
|
||||
match self.outcome {
|
||||
TransportOutcome::Ok(n) => Ok(ScsiResult {
|
||||
status: 0,
|
||||
bytes_transferred: n,
|
||||
sense: [0u8; 32],
|
||||
}),
|
||||
TransportOutcome::Scsi(status, sense) => Err(Error::ScsiError {
|
||||
opcode: cdb[0],
|
||||
status,
|
||||
sense,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recording(outcome: TransportOutcome) -> (Drive, Arc<Mutex<Vec<u8>>>, Arc<Mutex<u32>>) {
|
||||
let cdb = Arc::new(Mutex::new(Vec::new()));
|
||||
let to = Arc::new(Mutex::new(0u32));
|
||||
let t = RecordingTransport {
|
||||
last_cdb: cdb.clone(),
|
||||
last_timeout: to.clone(),
|
||||
outcome,
|
||||
};
|
||||
(Drive::from_transport_for_test(Box::new(t)), cdb, to)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_builds_read10_cdb_with_be_lba_and_count() {
|
||||
// Drive::read issues READ(10) (0x28). LBA bytes 2..5 big-endian,
|
||||
// transfer length bytes 7..8 big-endian (MMC-6). No FUA on this
|
||||
// path (byte 1 == 0). Distinct nibbles catch a swapped shift.
|
||||
let (mut d, cdb, _to) = recording(TransportOutcome::Ok(4096));
|
||||
let mut buf = vec![0u8; 4096];
|
||||
let n = d.read(0x00AB_CDEF, 2, &mut buf, false).unwrap();
|
||||
assert_eq!(n, 4096, "returns transport bytes_transferred");
|
||||
let c = cdb.lock().unwrap();
|
||||
assert_eq!(c[0], crate::scsi::SCSI_READ_10);
|
||||
assert_eq!(c[1], 0x00, "Drive::read path sets no FUA");
|
||||
assert_eq!(&c[2..6], &[0x00, 0xAB, 0xCD, 0xEF], "LBA big-endian");
|
||||
assert_eq!(&c[7..9], &[0x00, 0x02], "transfer length big-endian");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_recovery_flag_selects_60s_timeout() {
|
||||
// recovery=true must use READ_RECOVERY_TIMEOUT_MS (60 s); false
|
||||
// uses READ_TIMEOUT_MS (10 s). Doc: patch pass vs copy sweep.
|
||||
let (mut d, _cdb, to) = recording(TransportOutcome::Ok(2048));
|
||||
let mut buf = vec![0u8; 2048];
|
||||
d.read(0, 1, &mut buf, true).unwrap();
|
||||
assert_eq!(*to.lock().unwrap(), crate::scsi::READ_RECOVERY_TIMEOUT_MS);
|
||||
|
||||
let (mut d2, _c2, to2) = recording(TransportOutcome::Ok(2048));
|
||||
d2.read(0, 1, &mut buf, false).unwrap();
|
||||
assert_eq!(*to2.lock().unwrap(), crate::scsi::READ_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_maps_scsi_error_to_discread_preserving_status_and_sense() {
|
||||
// On a non-Halted failure, Drive::read returns Error::DiscRead
|
||||
// with sector=lba and the transport's status+sense carried
|
||||
// through (extract_scsi_context). A 03/11/05 MEDIUM ERROR.
|
||||
let sense = crate::scsi::ScsiSense {
|
||||
sense_key: 3,
|
||||
asc: 0x11,
|
||||
ascq: 0x05,
|
||||
};
|
||||
let (mut d, _cdb, _to) = recording(TransportOutcome::Scsi(0x02, Some(sense)));
|
||||
let mut buf = vec![0u8; 2048];
|
||||
let err = d.read(0x1234, 1, &mut buf, false).unwrap_err();
|
||||
match err {
|
||||
Error::DiscRead {
|
||||
sector,
|
||||
status,
|
||||
sense: s,
|
||||
} => {
|
||||
assert_eq!(sector, 0x1234, "sector must be the requested LBA");
|
||||
assert_eq!(status, Some(0x02));
|
||||
assert_eq!(s, Some(sense), "sense triple preserved");
|
||||
}
|
||||
other => panic!("expected DiscRead, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_transport_failure_status_preserved_for_marginal_routing() {
|
||||
// Status 0xFF (TRANSPORT_FAILURE) with no sense must surface in
|
||||
// DiscRead.status so is_scsi_transport_failure() routes it.
|
||||
let (mut d, _cdb, _to) = recording(TransportOutcome::Scsi(
|
||||
crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE,
|
||||
None,
|
||||
));
|
||||
let mut buf = vec![0u8; 2048];
|
||||
let err = d.read(7, 1, &mut buf, false).unwrap_err();
|
||||
assert!(err.is_scsi_transport_failure());
|
||||
assert!(err.scsi_sense().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_returns_halted_before_dispatch_without_touching_transport() {
|
||||
// When the halt flag is set, checked_exec returns Halted BEFORE
|
||||
// execute(); the error must be Halted (not DiscRead), so the
|
||||
// recovery loop distinguishes user-stop from a read failure.
|
||||
let (mut d, cdb, _to) = recording(TransportOutcome::Ok(2048));
|
||||
d.halt();
|
||||
let mut buf = vec![0u8; 2048];
|
||||
let err = d.read(0, 1, &mut buf, false).unwrap_err();
|
||||
assert!(matches!(err, Error::Halted));
|
||||
assert!(
|
||||
cdb.lock().unwrap().is_empty(),
|
||||
"transport execute must not run when pre-halted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_halt_reenables_reads() {
|
||||
// halt() then clear_halt() must allow reads again — the flag is
|
||||
// not sticky.
|
||||
let (mut d, _cdb, _to) = recording(TransportOutcome::Ok(2048));
|
||||
d.halt();
|
||||
d.clear_halt();
|
||||
let mut buf = vec![0u8; 2048];
|
||||
assert!(d.read(0, 1, &mut buf, false).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_does_not_truncate_reported_bytes() {
|
||||
// Single-shot contract: Drive::read returns exactly what the
|
||||
// transport reported, never a smaller count silently. Transport
|
||||
// says a full 32-sector batch (65536 bytes) succeeded.
|
||||
let (mut d, _cdb, _to) = recording(TransportOutcome::Ok(65536));
|
||||
let mut buf = vec![0u8; 65536];
|
||||
assert_eq!(d.read(0, 32, &mut buf, false).unwrap(), 65536);
|
||||
}
|
||||
|
||||
// ── drive_status branch coverage (GET EVENT STATUS byte 5) ──────
|
||||
|
||||
#[test]
|
||||
fn drive_status_no_disc_maps_correctly() {
|
||||
// media_status low bits 0b00 = tray closed, no disc.
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[5] = 0x00;
|
||||
let mut d = drive_with(buf);
|
||||
assert_eq!(d.drive_status(), DriveStatus::NoDisc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_status_tray_open_maps_correctly() {
|
||||
// media_status low bits 0b01 = tray open, no media.
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[5] = 0x01;
|
||||
let mut d = drive_with(buf);
|
||||
assert_eq!(d.drive_status(), DriveStatus::TrayOpen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_status_high_bits_in_media_status_ignored() {
|
||||
// Only the low 2 bits of byte 5 are the door/media state; upper
|
||||
// bits (NEA, etc.) must be masked. 0xFE has low bits 0b10 =
|
||||
// DiscPresent.
|
||||
let mut buf = vec![0u8; 8];
|
||||
buf[5] = 0xFE;
|
||||
let mut d = drive_with(buf);
|
||||
assert_eq!(d.drive_status(), DriveStatus::DiscPresent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_status_short_transfer_falls_back_to_tur() {
|
||||
// bytes_transferred < 6 means the GET EVENT reply is unusable;
|
||||
// the code falls back to a TUR. FixedTransport always returns
|
||||
// Ok, so the TUR "succeeds" → DiscPresent. (Buffer length 8 but
|
||||
// payload only 4 bytes → bytes_transferred = 4.)
|
||||
let mut d = drive_with(vec![0u8; 4]);
|
||||
assert_eq!(d.drive_status(), DriveStatus::DiscPresent);
|
||||
}
|
||||
|
||||
/// Transport that fails every command with a programmable error —
|
||||
/// drives the TUR-fallback NotReady/Unknown branches of drive_status.
|
||||
struct AlwaysErr {
|
||||
err: fn() -> Error,
|
||||
}
|
||||
impl ScsiTransport for AlwaysErr {
|
||||
fn execute(
|
||||
&mut self,
|
||||
_cdb: &[u8],
|
||||
_dir: DataDirection,
|
||||
_data: &mut [u8],
|
||||
_timeout_ms: u32,
|
||||
) -> Result<ScsiResult> {
|
||||
Err((self.err)())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_status_tur_not_ready_sense_maps_not_ready() {
|
||||
// GET EVENT fails, fallback TUR fails with NOT READY sense →
|
||||
// DriveStatus::NotReady (drive spinning up). Doc: drive_status
|
||||
// fallback branch.
|
||||
let mut d = Drive::from_transport_for_test(Box::new(AlwaysErr {
|
||||
err: || Error::ScsiError {
|
||||
opcode: 0,
|
||||
status: 0x02,
|
||||
sense: Some(crate::scsi::ScsiSense {
|
||||
sense_key: 2, // NOT READY
|
||||
asc: 0x04,
|
||||
ascq: 0x01,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
assert_eq!(d.drive_status(), DriveStatus::NotReady);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_status_tur_unit_attention_maps_not_ready() {
|
||||
// UNIT ATTENTION (media changed) on the fallback TUR also maps to
|
||||
// NotReady per the is_unit_attention() arm.
|
||||
let mut d = Drive::from_transport_for_test(Box::new(AlwaysErr {
|
||||
err: || Error::ScsiError {
|
||||
opcode: 0,
|
||||
status: 0x02,
|
||||
sense: Some(crate::scsi::ScsiSense {
|
||||
sense_key: 6, // UNIT ATTENTION
|
||||
asc: 0x28,
|
||||
ascq: 0x00,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
assert_eq!(d.drive_status(), DriveStatus::NotReady);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_status_tur_other_error_maps_unknown() {
|
||||
// A fallback TUR failure that is neither NOT READY nor UNIT
|
||||
// ATTENTION (e.g. transport failure, no sense) → Unknown.
|
||||
let mut d = Drive::from_transport_for_test(Box::new(AlwaysErr {
|
||||
err: || Error::ScsiError {
|
||||
opcode: 0,
|
||||
status: crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE,
|
||||
sense: None,
|
||||
},
|
||||
}));
|
||||
assert_eq!(d.drive_status(), DriveStatus::Unknown);
|
||||
}
|
||||
|
||||
// ── get_config_feature: header-strip threshold + clamp ──────────
|
||||
|
||||
#[test]
|
||||
fn get_config_feature_strips_8_byte_header() {
|
||||
// GET CONFIGURATION reply has an 8-byte Feature Header (MMC-6
|
||||
// §5.2.2). get_config_feature returns buf[8..end]. Provide a
|
||||
// 12-byte reply → returns the 4 payload bytes.
|
||||
let mut payload = vec![0u8; 8];
|
||||
payload.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
|
||||
let mut d = drive_with(payload);
|
||||
assert_eq!(
|
||||
d.get_config_feature(0x010D),
|
||||
Some(vec![0xDE, 0xAD, 0xBE, 0xEF])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_config_feature_at_exactly_8_bytes_returns_none() {
|
||||
// end == 8 means header only, no descriptor → None (the `end > 8`
|
||||
// guard). Boundary against an off-by-one that would return an
|
||||
// empty Vec instead of None.
|
||||
let mut d = drive_with(vec![0u8; 8]);
|
||||
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]
|
||||
fn report_key_rpc_state_returns_transferred_prefix() {
|
||||
// Returns buf[..end] where end = bytes_transferred. An 8-byte
|
||||
// reply yields all 8 bytes.
|
||||
let mut d = drive_with(vec![1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
assert_eq!(d.report_key_rpc_state(), Some(vec![1, 2, 3, 4, 5, 6, 7, 8]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn report_key_rpc_state_zero_transfer_returns_none() {
|
||||
// end == 0 → None (the `end > 0` guard), never Some(empty).
|
||||
let mut d = drive_with(vec![]);
|
||||
assert_eq!(d.report_key_rpc_state(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_sense_zero_transfer_returns_none() {
|
||||
let mut d = drive_with(vec![]);
|
||||
assert_eq!(d.mode_sense_page(0x2A), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_buffer_returns_prefix_and_clamps() {
|
||||
// read_buffer allocates `length` bytes; FixedTransport returns
|
||||
// min(payload, length). Request 16 with a 4-byte payload → 4 bytes.
|
||||
let mut d = drive_with(vec![9, 9, 9, 9]);
|
||||
assert_eq!(d.read_buffer(0x02, 0xF1, 16), Some(vec![9, 9, 9, 9]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_buffer_zero_transfer_returns_none() {
|
||||
let mut d = drive_with(vec![]);
|
||||
assert_eq!(d.read_buffer(0x02, 0xF1, 16), None);
|
||||
}
|
||||
|
||||
// ── No-driver paths: init/probe surface UnsupportedDrive ────────
|
||||
|
||||
#[test]
|
||||
fn init_without_driver_is_unsupported_drive() {
|
||||
// from_transport_for_test has no platform driver; init() must
|
||||
// return UnsupportedDrive, not panic or silently succeed.
|
||||
let mut d = drive_with(vec![]);
|
||||
assert!(matches!(d.init(), Err(Error::UnsupportedDrive { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_disc_without_driver_is_unsupported_drive() {
|
||||
let mut d = drive_with(vec![]);
|
||||
assert!(matches!(
|
||||
d.probe_disc(),
|
||||
Err(Error::UnsupportedDrive { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn read_capacity_exactly_4_bytes_decodes() {
|
||||
// bytes_transferred == 4 is the minimum that decodes (the guard
|
||||
// is `< 4`). last_lba in bytes 0..4 big-endian.
|
||||
let buf = [0x00, 0x00, 0x00, 0x05, 0, 0, 0, 0];
|
||||
assert_eq!(decode_read_capacity(&buf, 4).unwrap(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_capacity_zero_last_lba_is_one_sector() {
|
||||
// last_lba 0 → capacity 1 (a single-sector medium), distinct from
|
||||
// the malformed/short-transfer rejection.
|
||||
let buf = [0, 0, 0, 0, 0, 0, 0, 0];
|
||||
assert_eq!(decode_read_capacity(&buf, 8).unwrap(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user