use crate::Provenance;
use std::io::Cursor;
use winreg_core::hive::Hive;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MountedVolume {
pub drive_letter: Option<char>,
pub volume_guid: Option<String>,
pub disk_signature: u32,
pub partition_offset: u64,
pub source: Provenance,
}
#[must_use]
pub fn parse_mounted_volumes(hive: &Hive<Cursor<Vec<u8>>>, file: &str) -> Vec<MountedVolume> {
let Ok(Some(md)) = hive.open_key("MountedDevices") else {
return Vec::new();
};
let Ok(values) = md.values() else {
return Vec::new(); };
let mut out = Vec::new();
for value in values {
let name = value.name();
let drive_letter = dos_drive_letter(&name);
let volume_guid = volume_guid(&name);
if drive_letter.is_none() && volume_guid.is_none() {
continue;
}
let Ok(raw) = value.raw_data() else {
continue; };
let Some((disk_signature, partition_offset)) = decode_mbr(&raw) else {
continue;
};
out.push(MountedVolume {
drive_letter,
volume_guid,
disk_signature,
partition_offset,
source: Provenance {
file: file.to_string(),
line: 0,
key_path: Some(format!("MountedDevices\\{name}")),
},
});
}
out
}
fn dos_drive_letter(name: &str) -> Option<char> {
let tail = name.strip_prefix("\\DosDevices\\")?;
let mut chars = tail.chars();
let letter = chars.next()?;
(letter.is_ascii_alphabetic() && chars.next() == Some(':') && chars.next().is_none())
.then(|| letter.to_ascii_uppercase())
}
fn volume_guid(name: &str) -> Option<String> {
let g = name.strip_prefix("\\??\\Volume")?;
(g.starts_with('{') && g.ends_with('}')).then(|| g.to_ascii_lowercase())
}
fn decode_mbr(raw: &[u8]) -> Option<(u32, u64)> {
if raw.len() != 12 {
return None;
}
let sig = u32::from_le_bytes(raw.get(0..4)?.try_into().ok()?);
let offset = u64::from_le_bytes(raw.get(4..12)?.try_into().ok()?);
Some((sig, offset))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn dos_drive_letter_reads_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\\EE:"), None);
}
#[test]
fn volume_guid_reads_only_volume_names() {
assert_eq!(
volume_guid("\\??\\Volume{A2F2048E-D228-11E4-B630-000C29FF2429}").as_deref(),
Some("{a2f2048e-d228-11e4-b630-000c29ff2429}")
);
assert_eq!(volume_guid("\\DosDevices\\E:"), None);
assert_eq!(volume_guid("\\??\\Volumexyz"), None);
}
#[test]
fn decode_mbr_reads_signature_and_offset_or_rejects() {
assert_eq!(
decode_mbr(&[0x4c, 0x03, 0x21, 0xe2, 0, 0, 1, 0, 0, 0, 0, 0]),
Some((0xE221_034C, 0x1_0000))
);
assert_eq!(decode_mbr(&[0; 216]), None);
assert_eq!(decode_mbr(&[1, 2, 3]), None);
}
#[test]
fn synthetic_mounted_devices_hive_yields_the_mbr_volume() {
const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_mounted_devices.hive");
let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
let vols = parse_mounted_volumes(&hive, "SYNTHETIC");
let c = vols
.iter()
.find(|v| v.drive_letter == Some('C'))
.expect("C: MBR record present");
assert_eq!(c.disk_signature, 0x4433_2211);
assert!(c.source.key_path.is_some());
assert!(!vols
.iter()
.any(|v| v.drive_letter.is_none() && v.volume_guid.is_none()));
assert_eq!(vols.len(), 1);
}
#[test]
fn a_hive_without_mounted_devices_yields_nothing() {
const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_usb_system.hive");
let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
assert!(parse_mounted_volumes(&hive, "SYSTEM").is_empty());
}
}