197 lines
6.4 KiB
Rust
197 lines
6.4 KiB
Rust
//! Drive data capture — read hardware information via SCSI.
|
|
|
|
use crate::drive::Drive;
|
|
use crate::error::Result;
|
|
|
|
/// Raw data captured from a drive's SCSI responses.
|
|
#[derive(Debug, Clone)]
|
|
pub struct DriveCapture {
|
|
/// Raw INQUIRY response (96 bytes)
|
|
pub inquiry: Vec<u8>,
|
|
/// Raw GET_CONFIG 010C response
|
|
pub gc_010c: Vec<u8>,
|
|
/// GET_CONFIG feature responses: (feature_code, feature_name, data)
|
|
pub features: Vec<CapturedFeature>,
|
|
/// REPORT_KEY RPC state
|
|
pub rpc_state: Option<Vec<u8>>,
|
|
/// MODE SENSE page 2A (capabilities)
|
|
pub mode_2a: Option<Vec<u8>>,
|
|
/// READ_BUFFER 0xF1 (Pioneer vendor data)
|
|
pub rb_f1: Option<Vec<u8>>,
|
|
/// READ_BUFFER mode 6 (MTK vendor data)
|
|
pub rb_mode6: Option<Vec<u8>>,
|
|
}
|
|
|
|
/// A single GET CONFIGURATION feature response from the drive.
|
|
#[derive(Debug, Clone)]
|
|
pub struct CapturedFeature {
|
|
/// MMC-6 GET CONFIGURATION feature code (e.g. `0x010D` = AACS).
|
|
pub code: u16,
|
|
/// Static human-readable label from the internal `FEATURES` table —
|
|
/// not a device-reported string.
|
|
pub name: &'static str,
|
|
/// Raw feature-descriptor payload bytes, with the 8-byte GET
|
|
/// CONFIGURATION header stripped (i.e. `buf[8..]`). Unlike
|
|
/// [`DriveCapture::gc_010c`], which retains the full header.
|
|
pub data: Vec<u8>,
|
|
}
|
|
|
|
/// Feature codes to capture.
|
|
const FEATURES: &[(u16, &str)] = &[
|
|
(0x0000, "Profile List"),
|
|
(0x0001, "Core"),
|
|
(0x0003, "Removable Medium"),
|
|
(0x0010, "Random Readable"),
|
|
(0x001D, "Multi-Read"),
|
|
(0x001E, "CD Read"),
|
|
(0x001F, "DVD Read"),
|
|
(0x0040, "BD Read"),
|
|
(0x0041, "BD Write"),
|
|
(0x0100, "Power Management"),
|
|
(0x0102, "Embedded Changer"),
|
|
(0x0107, "Real Time Streaming"),
|
|
(0x0108, "Serial Number"),
|
|
(0x010C, "Firmware Information"),
|
|
(0x010D, "AACS"),
|
|
];
|
|
|
|
/// Capture all available drive data via SCSI commands.
|
|
/// Returns raw responses — no formatting, no zipping, no presentation.
|
|
pub fn capture_drive_data(session: &mut Drive) -> Result<DriveCapture> {
|
|
let id = &session.drive_id;
|
|
|
|
// Already have INQUIRY from drive open
|
|
let inquiry = id.raw_inquiry.clone();
|
|
let gc_010c = id.raw_gc_010c.clone();
|
|
|
|
// Capture GET_CONFIG features using Drive's query methods
|
|
let mut features = Vec::new();
|
|
for &(code, name) in FEATURES {
|
|
if let Some(data) = session.get_config_feature(code) {
|
|
features.push(CapturedFeature { code, name, data });
|
|
}
|
|
}
|
|
|
|
// Vendor-specific READ_BUFFER queries
|
|
let rb_f1 = session.read_buffer(0x02, 0xF1, 48); // Pioneer
|
|
let rb_mode6 = session.read_buffer(0x06, 0x00, 32); // MTK
|
|
|
|
// Standard queries
|
|
let rpc_state = session.report_key_rpc_state();
|
|
let mode_2a = session.mode_sense_page(0x2A);
|
|
|
|
Ok(DriveCapture {
|
|
inquiry,
|
|
gc_010c,
|
|
features,
|
|
rpc_state,
|
|
mode_2a,
|
|
rb_f1,
|
|
rb_mode6,
|
|
})
|
|
}
|
|
|
|
/// Mask a string for privacy (letters->A, digits->0).
|
|
pub fn mask_string(s: &str) -> String {
|
|
s.chars()
|
|
.map(|c| {
|
|
if c.is_ascii_alphabetic() {
|
|
'A'
|
|
} else if c.is_ascii_digit() {
|
|
'0'
|
|
} else {
|
|
c
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Mask bytes for privacy.
|
|
pub fn mask_bytes(data: &[u8]) -> Vec<u8> {
|
|
data.iter()
|
|
.map(|&b| {
|
|
if b.is_ascii_alphabetic() {
|
|
b'A'
|
|
} else if b.is_ascii_digit() {
|
|
b'0'
|
|
} else {
|
|
b
|
|
}
|
|
})
|
|
.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_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 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"
|
|
);
|
|
}
|
|
}
|