peripheral_core/
mounted_volumes.rs1use crate::Provenance;
15use std::io::Cursor;
16use winreg_core::hive::Hive;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct MountedVolume {
22 pub drive_letter: Option<char>,
24 pub volume_guid: Option<String>,
26 pub disk_signature: u32,
28 pub partition_offset: u64,
30 pub source: Provenance,
32}
33
34#[must_use]
38pub fn parse_mounted_volumes(hive: &Hive<Cursor<Vec<u8>>>, file: &str) -> Vec<MountedVolume> {
39 let Ok(Some(md)) = hive.open_key("MountedDevices") else {
40 return Vec::new();
41 };
42 let Ok(values) = md.values() else {
43 return Vec::new(); };
45 let mut out = Vec::new();
46 for value in values {
47 let name = value.name();
48 let drive_letter = dos_drive_letter(&name);
49 let volume_guid = volume_guid(&name);
50 if drive_letter.is_none() && volume_guid.is_none() {
51 continue;
52 }
53 let Ok(raw) = value.raw_data() else {
54 continue; };
56 let Some((disk_signature, partition_offset)) = decode_mbr(&raw) else {
57 continue;
58 };
59 out.push(MountedVolume {
60 drive_letter,
61 volume_guid,
62 disk_signature,
63 partition_offset,
64 source: Provenance {
65 file: file.to_string(),
66 line: 0,
67 key_path: Some(format!("MountedDevices\\{name}")),
68 },
69 });
70 }
71 out
72}
73
74fn dos_drive_letter(name: &str) -> Option<char> {
76 let tail = name.strip_prefix("\\DosDevices\\")?;
77 let mut chars = tail.chars();
78 let letter = chars.next()?;
79 (letter.is_ascii_alphabetic() && chars.next() == Some(':') && chars.next().is_none())
80 .then(|| letter.to_ascii_uppercase())
81}
82
83fn volume_guid(name: &str) -> Option<String> {
85 let g = name.strip_prefix("\\??\\Volume")?;
86 (g.starts_with('{') && g.ends_with('}')).then(|| g.to_ascii_lowercase())
87}
88
89fn decode_mbr(raw: &[u8]) -> Option<(u32, u64)> {
92 if raw.len() != 12 {
93 return None;
94 }
95 let sig = u32::from_le_bytes(raw.get(0..4)?.try_into().ok()?);
96 let offset = u64::from_le_bytes(raw.get(4..12)?.try_into().ok()?);
97 Some((sig, offset))
98}
99
100#[cfg(test)]
101#[allow(clippy::unwrap_used, clippy::expect_used)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn dos_drive_letter_reads_only_well_formed_names() {
107 assert_eq!(dos_drive_letter("\\DosDevices\\E:"), Some('E'));
108 assert_eq!(dos_drive_letter("\\DosDevices\\c:"), Some('C'));
109 assert_eq!(dos_drive_letter("\\??\\Volume{1234}"), None);
110 assert_eq!(dos_drive_letter("\\DosDevices\\EE:"), None);
111 }
112
113 #[test]
114 fn volume_guid_reads_only_volume_names() {
115 assert_eq!(
116 volume_guid("\\??\\Volume{A2F2048E-D228-11E4-B630-000C29FF2429}").as_deref(),
117 Some("{a2f2048e-d228-11e4-b630-000c29ff2429}")
118 );
119 assert_eq!(volume_guid("\\DosDevices\\E:"), None);
120 assert_eq!(volume_guid("\\??\\Volumexyz"), None);
121 }
122
123 #[test]
124 fn decode_mbr_reads_signature_and_offset_or_rejects() {
125 assert_eq!(
127 decode_mbr(&[0x4c, 0x03, 0x21, 0xe2, 0, 0, 1, 0, 0, 0, 0, 0]),
128 Some((0xE221_034C, 0x1_0000))
129 );
130 assert_eq!(decode_mbr(&[0; 216]), None);
132 assert_eq!(decode_mbr(&[1, 2, 3]), None);
133 }
134
135 #[test]
136 fn synthetic_mounted_devices_hive_yields_the_mbr_volume() {
137 const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_mounted_devices.hive");
139 let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
140 let vols = parse_mounted_volumes(&hive, "SYNTHETIC");
141 let c = vols
142 .iter()
143 .find(|v| v.drive_letter == Some('C'))
144 .expect("C: MBR record present");
145 assert_eq!(c.disk_signature, 0x4433_2211);
146 assert!(c.source.key_path.is_some());
147 assert!(!vols
151 .iter()
152 .any(|v| v.drive_letter.is_none() && v.volume_guid.is_none()));
153 assert_eq!(vols.len(), 1);
154 }
155
156 #[test]
157 fn a_hive_without_mounted_devices_yields_nothing() {
158 const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_usb_system.hive");
160 let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
161 assert!(parse_mounted_volumes(&hive, "SYSTEM").is_empty());
162 }
163}