1use crate::{Bus, DeviceConnection, Provenance, Stamp};
11use std::io::Cursor;
12use winreg_core::hive::Hive;
13use winreg_core::key::{filetime_to_datetime, Key};
14
15const TS_GUID: &str = "{83da6326-97a6-4088-9453-a1923f573b29}";
18
19const ENUM_CLASSES: [(&str, Bus); 3] = [
23 ("USBSTOR", Bus::Usb),
24 ("USB", Bus::Usb),
25 ("SCSI", Bus::ScsiSas),
26];
27
28const CONTROL_SETS: [&str; 2] = ["ControlSet001", "ControlSet002"];
30
31#[must_use]
38pub fn parse_registry(hive: &Hive<Cursor<Vec<u8>>>, file: &str) -> Vec<DeviceConnection> {
39 let mut out = Vec::new();
40 for cs in CONTROL_SETS {
41 for (class, bus) in ENUM_CLASSES {
42 let base = format!("{cs}\\Enum\\{class}");
43 let Ok(Some(class_key)) = hive.open_key(&base) else {
44 continue;
45 };
46 let Ok(vendors) = class_key.subkeys() else {
47 continue; };
49 for vendor in vendors {
50 let ven_name = vendor.name();
51 let Ok(instances) = vendor.subkeys() else {
52 continue; };
54 for inst in instances {
55 let inst_name = inst.name();
56 let key_path = format!("{base}\\{ven_name}\\{inst_name}");
57 out.push(build_connection(
58 &inst, class, bus, &ven_name, &inst_name, file, key_path,
59 ));
60 }
61 }
62 }
63 }
64 apply_mounted_devices(hive, &mut out);
65 out
66}
67
68fn apply_mounted_devices(hive: &Hive<Cursor<Vec<u8>>>, conns: &mut [DeviceConnection]) {
79 let Ok(Some(md)) = hive.open_key("MountedDevices") else {
80 return;
81 };
82 let Ok(values) = md.values() else {
83 return; };
85 for value in values {
86 let Some(letter) = dos_drive_letter(&value.name()) else {
87 continue;
88 };
89 let Ok(raw) = value.raw_data() else {
90 continue; };
92 let Some(instance) = device_path_instance(&raw) else {
93 continue;
94 };
95 let suffix = format!("\\{instance}");
96 for conn in conns.iter_mut() {
97 if conn.device_instance_id == instance || conn.device_instance_id.ends_with(&suffix) {
98 conn.drive_letter = Some(letter);
99 }
100 }
101 }
102}
103
104fn dos_drive_letter(name: &str) -> Option<char> {
107 let tail = name.strip_prefix("\\DosDevices\\")?;
108 let mut chars = tail.chars();
109 let letter = chars.next()?;
110 if letter.is_ascii_alphabetic() && chars.next() == Some(':') && chars.next().is_none() {
111 Some(letter.to_ascii_uppercase())
112 } else {
113 None
114 }
115}
116
117fn device_path_instance(raw: &[u8]) -> Option<String> {
121 if raw.len() < 8 || raw.len() % 2 != 0 {
122 return None;
123 }
124 let units: Vec<u16> = raw
125 .chunks_exact(2)
126 .map(|c| u16::from_le_bytes([c[0], c[1]]))
127 .collect();
128 let decoded = String::from_utf16(&units).ok()?;
129 let path = decoded.strip_prefix("\\??\\")?;
130 let mut parts: Vec<&str> = path.split('#').collect();
131 if parts
133 .last()
134 .is_some_and(|p| p.starts_with('{') && p.ends_with('}'))
135 {
136 parts.pop();
137 }
138 let instance = *parts.last()?;
139 if instance.is_empty() || parts.len() < 2 {
140 return None;
141 }
142 Some(instance.to_string())
143}
144
145const MTP_SERVICE: &str = "WUDFWpdMtp";
149
150fn mtp_override(service: Option<&str>, bus: Bus) -> Bus {
154 if bus == Bus::Usb && service.is_some_and(|s| s.eq_ignore_ascii_case(MTP_SERVICE)) {
155 Bus::Mtp
156 } else {
157 bus
158 }
159}
160
161fn build_connection(
162 inst: &Key<'_>,
163 class: &str,
164 bus: Bus,
165 ven_name: &str,
166 inst_name: &str,
167 file: &str,
168 key_path: String,
169) -> DeviceConnection {
170 let (vid, pid) = parse_vid_pid(ven_name);
171 let bus = mtp_override(value_string(inst, "Service").as_deref(), bus);
174 let serial_is_os_generated = inst_name.as_bytes().get(1) == Some(&b'&');
177
178 let ts = inst
179 .subkey("Properties")
180 .ok()
181 .flatten()
182 .and_then(|p| p.subkey(TS_GUID).ok().flatten());
183 let filetime = |prop: u32| ts.as_ref().and_then(|k| read_filetime(k, prop));
184 let first_install = filetime(0x64)
187 .or_else(|| filetime(0x65))
188 .map(Stamp::authoritative);
189 let last_arrival = filetime(0x66).map(Stamp::inferred);
190 let last_removal = filetime(0x67).map(Stamp::inferred);
191
192 DeviceConnection {
193 bus,
194 device_class_guid: None,
195 vid,
196 pid,
197 device_serial: (!inst_name.is_empty()).then(|| inst_name.to_string()),
198 serial_is_os_generated,
199 friendly_name: value_string(inst, "FriendlyName"),
200 device_instance_id: format!("{class}\\{ven_name}\\{inst_name}"),
201 first_install,
202 last_install: None,
203 last_arrival,
204 last_removal,
205 parent_id_prefix: value_string(inst, "ParentIdPrefix"),
206 volume_guid: None,
207 drive_letter: None,
208 volume_serial: None,
209 disk_signature: None,
210 dma_capable: bus.is_dma_capable(),
211 mitre: Vec::new(),
212 source: Provenance {
213 file: file.to_string(),
214 line: 0,
215 key_path: Some(key_path),
216 },
217 }
218}
219
220fn value_string(key: &Key<'_>, name: &str) -> Option<String> {
222 key.value(name)
223 .ok()
224 .flatten()
225 .and_then(|v| v.as_string().ok())
226 .filter(|s| !s.is_empty())
227}
228
229fn read_filetime(guid_key: &Key<'_>, prop: u32) -> Option<i64> {
241 let prop_key = find_prop_subkey(guid_key, prop)?;
242 let raw = prop_key
243 .value("")
244 .ok()
245 .flatten()
246 .and_then(|v| v.raw_data().ok())
247 .or_else(|| {
248 let leaf = find_prop_subkey(&prop_key, 0)?;
250 leaf.value("Data")
251 .ok()
252 .flatten()
253 .and_then(|v| v.raw_data().ok())
254 })?;
255 let bytes: [u8; 8] = raw.get(..8)?.try_into().ok()?;
256 let ts = filetime_to_datetime(u64::from_le_bytes(bytes))?;
257 Some(ts.as_second())
258}
259
260fn find_prop_subkey<'a>(key: &Key<'a>, num: u32) -> Option<Key<'a>> {
263 key.subkeys()
264 .ok()?
265 .into_iter()
266 .find(|k| u32::from_str_radix(&k.name(), 16).ok() == Some(num))
267}
268
269fn parse_vid_pid(name: &str) -> (Option<u16>, Option<u16>) {
271 let hex4 = |tag: &str| {
272 name.split('&').find_map(|seg| {
273 let h = seg.strip_prefix(tag)?;
274 u16::from_str_radix(h.get(..4)?, 16).ok()
275 })
276 };
277 (hex4("VID_"), hex4("PID_"))
278}
279
280#[cfg(test)]
281#[allow(clippy::unwrap_used, clippy::expect_used)]
282mod tests {
283 use super::*;
284 use crate::Bus;
285
286 #[test]
296 fn synthetic_hive_exercises_every_branch() {
297 const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_usb_system.hive");
298 let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid synthetic REGF");
299 let conns = parse_registry(&hive, "SYNTHETIC");
300 let by = |needle: &str| {
301 conns
302 .iter()
303 .find(|c| c.device_instance_id.contains(needle))
304 .expect("device present")
305 };
306
307 let scsi = by("Disk&Ven_Test&Prod_Disk");
309 assert_eq!(scsi.bus, Bus::ScsiSas);
310 assert_eq!(scsi.friendly_name.as_deref(), Some("Test Virtual Disk"));
311 assert_eq!(
312 scsi.first_install.as_ref().map(|s| s.value),
313 Some(1_600_357_894)
314 );
315 assert_eq!(
316 scsi.last_arrival.as_ref().map(|s| s.value),
317 Some(1_600_478_558)
318 );
319 assert_eq!(scsi.last_removal, None);
320 assert!(scsi.serial_is_os_generated);
321 assert!(scsi.source.key_path.is_some());
322
323 let usbstor = by("Disk&Ven_Gen&Prod_Flash");
325 assert_eq!(usbstor.bus, Bus::Usb);
326 assert_eq!(
327 usbstor.first_install.as_ref().map(|s| s.value),
328 Some(1_500_000_000)
329 );
330 assert_eq!(
331 usbstor.last_removal.as_ref().map(|s| s.value),
332 Some(1_500_009_999)
333 );
334 assert_eq!(usbstor.friendly_name, None);
335
336 let usb = by("VID_0781&PID_5583");
338 assert_eq!(usb.bus, Bus::Usb);
339 assert_eq!(usb.vid, Some(0x0781));
340 assert_eq!(usb.pid, Some(0x5583));
341 assert_eq!(usb.device_serial.as_deref(), Some("0123456789AB"));
342 assert!(!usb.serial_is_os_generated);
343 }
344
345 #[test]
346 fn mtp_service_reclassifies_the_bus_to_mtp() {
347 assert_eq!(mtp_override(Some("WUDFWpdMtp"), Bus::Usb), Bus::Mtp);
352 assert_eq!(mtp_override(Some("wudfwpdmtp"), Bus::Usb), Bus::Mtp); assert_eq!(mtp_override(Some("USBSTOR"), Bus::Usb), Bus::Usb);
354 assert_eq!(mtp_override(None, Bus::Usb), Bus::Usb);
355 assert_eq!(mtp_override(Some("WUDFWpdMtp"), Bus::ScsiSas), Bus::ScsiSas);
357 }
358
359 #[test]
360 fn parse_vid_pid_handles_absent_and_malformed() {
361 assert_eq!(
362 parse_vid_pid("VID_0781&PID_5583"),
363 (Some(0x0781), Some(0x5583))
364 );
365 assert_eq!(parse_vid_pid("Disk&Ven_Gen&Prod_Flash"), (None, None));
366 assert_eq!(parse_vid_pid("VID_07&PID_ZZZZ"), (None, None));
368 }
369
370 fn u16le(s: &str) -> Vec<u8> {
372 s.encode_utf16().flat_map(u16::to_le_bytes).collect()
373 }
374
375 #[test]
376 fn dos_drive_letter_extracts_only_well_formed_names() {
377 assert_eq!(dos_drive_letter("\\DosDevices\\E:"), Some('E'));
378 assert_eq!(dos_drive_letter("\\DosDevices\\c:"), Some('C')); assert_eq!(dos_drive_letter("\\??\\Volume{1234}"), None);
381 assert_eq!(dos_drive_letter("\\DosDevices\\"), None);
383 assert_eq!(dos_drive_letter("\\DosDevices\\EE:"), None);
384 assert_eq!(dos_drive_letter("\\DosDevices\\1:"), None);
385 }
386
387 #[test]
388 fn device_path_instance_extracts_the_instance_or_rejects() {
389 let dev =
390 "\\??\\SCSI#Disk&Ven_Test&Prod_X#5&join123&0#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}";
391 assert_eq!(
392 device_path_instance(&u16le(dev)).as_deref(),
393 Some("5&join123&0")
394 );
395 assert_eq!(
397 device_path_instance(&u16le("\\??\\USBSTOR#Disk&Ven#INST42")).as_deref(),
398 Some("INST42")
399 );
400 assert_eq!(
402 device_path_instance(&[0x11, 0x22, 0x33, 0x44, 0, 0, 0, 0, 0, 0, 0, 0]),
403 None
404 );
405 assert_eq!(device_path_instance(&[0, 0]), None);
407 assert_eq!(device_path_instance(&[1, 2, 3]), None);
408 assert_eq!(device_path_instance(&u16le("C:\\not-a-device-path")), None);
409 assert_eq!(device_path_instance(&u16le("\\??\\onlyonepart")), None);
410 assert_eq!(
412 device_path_instance(&[0x00, 0xD8, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x00]),
413 None
414 );
415 }
416
417 #[test]
418 fn mounted_devices_join_sets_drive_letter_on_the_matching_device() {
419 const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_mounted_devices.hive");
423 let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
424 let conns = parse_registry(&hive, "SYNTHETIC");
425 let dev = conns
426 .iter()
427 .find(|c| c.device_instance_id.ends_with("5&join123&0"))
428 .expect("device present");
429 assert_eq!(dev.drive_letter, Some('E'));
430 }
431
432 #[test]
433 fn win7_nested_property_layout_filetime_is_decoded() {
434 const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_win7_props.hive");
440 let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
441 let conns = parse_registry(&hive, "SYNTHETIC");
442 let dev = conns
443 .iter()
444 .find(|c| c.device_instance_id.ends_with("7&win7serial&0"))
445 .expect("Win7 device present");
446 assert_eq!(
447 dev.first_install.as_ref().map(|s| s.value),
448 Some(1_427_135_471),
449 "install FILETIME decoded from the nested 00000064\\00000000\\Data leaf"
450 );
451 }
452}