use crate::{Bus, DeviceConnection, Provenance, Stamp};
use std::io::Cursor;
use winreg_core::hive::Hive;
use winreg_core::key::{filetime_to_datetime, Key};
const TS_GUID: &str = "{83da6326-97a6-4088-9453-a1923f573b29}";
const ENUM_CLASSES: [(&str, Bus); 3] = [
("USBSTOR", Bus::Usb),
("USB", Bus::Usb),
("SCSI", Bus::ScsiSas),
];
const CONTROL_SETS: [&str; 2] = ["ControlSet001", "ControlSet002"];
#[must_use]
pub fn parse_registry(hive: &Hive<Cursor<Vec<u8>>>, file: &str) -> Vec<DeviceConnection> {
let mut out = Vec::new();
for cs in CONTROL_SETS {
for (class, bus) in ENUM_CLASSES {
let base = format!("{cs}\\Enum\\{class}");
let Ok(Some(class_key)) = hive.open_key(&base) else {
continue;
};
let Ok(vendors) = class_key.subkeys() else {
continue; };
for vendor in vendors {
let ven_name = vendor.name();
let Ok(instances) = vendor.subkeys() else {
continue; };
for inst in instances {
let inst_name = inst.name();
let key_path = format!("{base}\\{ven_name}\\{inst_name}");
out.push(build_connection(
&inst, class, bus, &ven_name, &inst_name, file, key_path,
));
}
}
}
}
apply_mounted_devices(hive, &mut out);
out
}
fn apply_mounted_devices(hive: &Hive<Cursor<Vec<u8>>>, conns: &mut [DeviceConnection]) {
let Ok(Some(md)) = hive.open_key("MountedDevices") else {
return;
};
let Ok(values) = md.values() else {
return; };
for value in values {
let Some(letter) = dos_drive_letter(&value.name()) else {
continue;
};
let Ok(raw) = value.raw_data() else {
continue; };
let Some(instance) = device_path_instance(&raw) else {
continue;
};
let suffix = format!("\\{instance}");
for conn in conns.iter_mut() {
if conn.device_instance_id == instance || conn.device_instance_id.ends_with(&suffix) {
conn.drive_letter = Some(letter);
}
}
}
}
fn dos_drive_letter(name: &str) -> Option<char> {
let tail = name.strip_prefix("\\DosDevices\\")?;
let mut chars = tail.chars();
let letter = chars.next()?;
if letter.is_ascii_alphabetic() && chars.next() == Some(':') && chars.next().is_none() {
Some(letter.to_ascii_uppercase())
} else {
None
}
}
fn device_path_instance(raw: &[u8]) -> Option<String> {
if raw.len() < 8 || raw.len() % 2 != 0 {
return None;
}
let units: Vec<u16> = raw
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let decoded = String::from_utf16(&units).ok()?;
let path = decoded.strip_prefix("\\??\\")?;
let mut parts: Vec<&str> = path.split('#').collect();
if parts
.last()
.is_some_and(|p| p.starts_with('{') && p.ends_with('}'))
{
parts.pop();
}
let instance = *parts.last()?;
if instance.is_empty() || parts.len() < 2 {
return None;
}
Some(instance.to_string())
}
const MTP_SERVICE: &str = "WUDFWpdMtp";
fn mtp_override(service: Option<&str>, bus: Bus) -> Bus {
if bus == Bus::Usb && service.is_some_and(|s| s.eq_ignore_ascii_case(MTP_SERVICE)) {
Bus::Mtp
} else {
bus
}
}
fn build_connection(
inst: &Key<'_>,
class: &str,
bus: Bus,
ven_name: &str,
inst_name: &str,
file: &str,
key_path: String,
) -> DeviceConnection {
let (vid, pid) = parse_vid_pid(ven_name);
let bus = mtp_override(value_string(inst, "Service").as_deref(), bus);
let serial_is_os_generated = inst_name.as_bytes().get(1) == Some(&b'&');
let ts = inst
.subkey("Properties")
.ok()
.flatten()
.and_then(|p| p.subkey(TS_GUID).ok().flatten());
let filetime = |prop: u32| ts.as_ref().and_then(|k| read_filetime(k, prop));
let first_install = filetime(0x64)
.or_else(|| filetime(0x65))
.map(Stamp::authoritative);
let last_arrival = filetime(0x66).map(Stamp::inferred);
let last_removal = filetime(0x67).map(Stamp::inferred);
DeviceConnection {
bus,
device_class_guid: None,
vid,
pid,
device_serial: (!inst_name.is_empty()).then(|| inst_name.to_string()),
serial_is_os_generated,
friendly_name: value_string(inst, "FriendlyName"),
device_instance_id: format!("{class}\\{ven_name}\\{inst_name}"),
first_install,
last_install: None,
last_arrival,
last_removal,
parent_id_prefix: value_string(inst, "ParentIdPrefix"),
volume_guid: None,
drive_letter: None,
volume_serial: None,
disk_signature: None,
dma_capable: bus.is_dma_capable(),
mitre: Vec::new(),
source: Provenance {
file: file.to_string(),
line: 0,
key_path: Some(key_path),
},
}
}
fn value_string(key: &Key<'_>, name: &str) -> Option<String> {
key.value(name)
.ok()
.flatten()
.and_then(|v| v.as_string().ok())
.filter(|s| !s.is_empty())
}
fn read_filetime(guid_key: &Key<'_>, prop: u32) -> Option<i64> {
let prop_key = find_prop_subkey(guid_key, prop)?;
let raw = prop_key
.value("")
.ok()
.flatten()
.and_then(|v| v.raw_data().ok())
.or_else(|| {
let leaf = find_prop_subkey(&prop_key, 0)?;
leaf.value("Data")
.ok()
.flatten()
.and_then(|v| v.raw_data().ok())
})?;
let bytes: [u8; 8] = raw.get(..8)?.try_into().ok()?;
let ts = filetime_to_datetime(u64::from_le_bytes(bytes))?;
Some(ts.as_second())
}
fn find_prop_subkey<'a>(key: &Key<'a>, num: u32) -> Option<Key<'a>> {
key.subkeys()
.ok()?
.into_iter()
.find(|k| u32::from_str_radix(&k.name(), 16).ok() == Some(num))
}
fn parse_vid_pid(name: &str) -> (Option<u16>, Option<u16>) {
let hex4 = |tag: &str| {
name.split('&').find_map(|seg| {
let h = seg.strip_prefix(tag)?;
u16::from_str_radix(h.get(..4)?, 16).ok()
})
};
(hex4("VID_"), hex4("PID_"))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::Bus;
#[test]
fn synthetic_hive_exercises_every_branch() {
const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_usb_system.hive");
let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid synthetic REGF");
let conns = parse_registry(&hive, "SYNTHETIC");
let by = |needle: &str| {
conns
.iter()
.find(|c| c.device_instance_id.contains(needle))
.expect("device present")
};
let scsi = by("Disk&Ven_Test&Prod_Disk");
assert_eq!(scsi.bus, Bus::ScsiSas);
assert_eq!(scsi.friendly_name.as_deref(), Some("Test Virtual Disk"));
assert_eq!(
scsi.first_install.as_ref().map(|s| s.value),
Some(1_600_357_894)
);
assert_eq!(
scsi.last_arrival.as_ref().map(|s| s.value),
Some(1_600_478_558)
);
assert_eq!(scsi.last_removal, None);
assert!(scsi.serial_is_os_generated);
assert!(scsi.source.key_path.is_some());
let usbstor = by("Disk&Ven_Gen&Prod_Flash");
assert_eq!(usbstor.bus, Bus::Usb);
assert_eq!(
usbstor.first_install.as_ref().map(|s| s.value),
Some(1_500_000_000)
);
assert_eq!(
usbstor.last_removal.as_ref().map(|s| s.value),
Some(1_500_009_999)
);
assert_eq!(usbstor.friendly_name, None);
let usb = by("VID_0781&PID_5583");
assert_eq!(usb.bus, Bus::Usb);
assert_eq!(usb.vid, Some(0x0781));
assert_eq!(usb.pid, Some(0x5583));
assert_eq!(usb.device_serial.as_deref(), Some("0123456789AB"));
assert!(!usb.serial_is_os_generated);
}
#[test]
fn mtp_service_reclassifies_the_bus_to_mtp() {
assert_eq!(mtp_override(Some("WUDFWpdMtp"), Bus::Usb), Bus::Mtp);
assert_eq!(mtp_override(Some("wudfwpdmtp"), Bus::Usb), Bus::Mtp); assert_eq!(mtp_override(Some("USBSTOR"), Bus::Usb), Bus::Usb);
assert_eq!(mtp_override(None, Bus::Usb), Bus::Usb);
assert_eq!(mtp_override(Some("WUDFWpdMtp"), Bus::ScsiSas), Bus::ScsiSas);
}
#[test]
fn parse_vid_pid_handles_absent_and_malformed() {
assert_eq!(
parse_vid_pid("VID_0781&PID_5583"),
(Some(0x0781), Some(0x5583))
);
assert_eq!(parse_vid_pid("Disk&Ven_Gen&Prod_Flash"), (None, None));
assert_eq!(parse_vid_pid("VID_07&PID_ZZZZ"), (None, None));
}
fn u16le(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(u16::to_le_bytes).collect()
}
#[test]
fn dos_drive_letter_extracts_only_well_formed_names() {
assert_eq!(dos_drive_letter("\\DosDevices\\E:"), Some('E'));
assert_eq!(dos_drive_letter("\\DosDevices\\c:"), Some('C')); assert_eq!(dos_drive_letter("\\??\\Volume{1234}"), None);
assert_eq!(dos_drive_letter("\\DosDevices\\"), None);
assert_eq!(dos_drive_letter("\\DosDevices\\EE:"), None);
assert_eq!(dos_drive_letter("\\DosDevices\\1:"), None);
}
#[test]
fn device_path_instance_extracts_the_instance_or_rejects() {
let dev =
"\\??\\SCSI#Disk&Ven_Test&Prod_X#5&join123&0#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}";
assert_eq!(
device_path_instance(&u16le(dev)).as_deref(),
Some("5&join123&0")
);
assert_eq!(
device_path_instance(&u16le("\\??\\USBSTOR#Disk&Ven#INST42")).as_deref(),
Some("INST42")
);
assert_eq!(
device_path_instance(&[0x11, 0x22, 0x33, 0x44, 0, 0, 0, 0, 0, 0, 0, 0]),
None
);
assert_eq!(device_path_instance(&[0, 0]), None);
assert_eq!(device_path_instance(&[1, 2, 3]), None);
assert_eq!(device_path_instance(&u16le("C:\\not-a-device-path")), None);
assert_eq!(device_path_instance(&u16le("\\??\\onlyonepart")), None);
assert_eq!(
device_path_instance(&[0x00, 0xD8, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x00]),
None
);
}
#[test]
fn mounted_devices_join_sets_drive_letter_on_the_matching_device() {
const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_mounted_devices.hive");
let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
let conns = parse_registry(&hive, "SYNTHETIC");
let dev = conns
.iter()
.find(|c| c.device_instance_id.ends_with("5&join123&0"))
.expect("device present");
assert_eq!(dev.drive_letter, Some('E'));
}
#[test]
fn win7_nested_property_layout_filetime_is_decoded() {
const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_win7_props.hive");
let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
let conns = parse_registry(&hive, "SYNTHETIC");
let dev = conns
.iter()
.find(|c| c.device_instance_id.ends_with("7&win7serial&0"))
.expect("Win7 device present");
assert_eq!(
dev.first_install.as_ref().map(|s| s.value),
Some(1_427_135_471),
"install FILETIME decoded from the nested 00000064\\00000000\\Data leaf"
);
}
}