use crate::drive::Drive;
use crate::error::Result;
#[derive(Debug, Clone)]
pub struct DriveCapture {
pub inquiry: Vec<u8>,
pub gc_010c: Vec<u8>,
pub features: Vec<CapturedFeature>,
pub rpc_state: Option<Vec<u8>>,
pub mode_2a: Option<Vec<u8>>,
pub rb_f1: Option<Vec<u8>>,
pub rb_mode6: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct CapturedFeature {
pub code: u16,
pub name: &'static str,
pub data: Vec<u8>,
}
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"),
];
pub fn capture_drive_data(session: &mut Drive) -> Result<DriveCapture> {
let id = &session.drive_id;
let inquiry = id.raw_inquiry.clone();
let gc_010c = id.raw_gc_010c.clone();
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 });
}
}
let rb_f1 = session.read_buffer(0x02, 0xF1, 48); let rb_mode6 = session.read_buffer(0x06, 0x00, 32);
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,
})
}
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()
}
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 {
use super::*;
#[test]
fn mask_string_letters_become_a_digits_become_zero() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
assert!(
FEATURES.iter().any(|&(c, _)| c == 0x010D),
"AACS feature 0x010D must be captured"
);
}
#[test]
fn feature_table_codes_are_sorted_ascending() {
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");
}
}