Skip to main content

peripheral_core/
emdmgmt.rs

1//! Volume label + serial history: decode `EMDMgmt` from a Windows `SOFTWARE` hive.
2//!
3//! `SOFTWARE\Microsoft\Windows NT\CurrentVersion\EMDMgmt` is the ReadyBoost device cache.
4//! Windows evaluated every volume it saw for ReadyBoost and left one subkey per volume,
5//! named `<device-prefix>__<VolumeLabel>_<VolumeSerialNumber>` (the serial in decimal).
6//! It is a durable record of a removable volume's **label and 4-byte volume serial** — the
7//! same serial a Shell Link's `DriveSerialNumber` stores — so it ties a `.lnk` file-access
8//! to a named volume even after the device is gone. This reader emits those `(label,
9//! volume_serial)` facts.
10
11use crate::Provenance;
12use std::io::Cursor;
13use winreg_core::hive::Hive;
14
15/// The `EMDMgmt` (ReadyBoost) cache path in a `SOFTWARE` hive.
16const EMD_PATH: &str = "Microsoft\\Windows NT\\CurrentVersion\\EMDMgmt";
17
18/// One cached volume: its label and 4-byte volume serial number.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct EmdVolume {
21    /// The volume label (may be empty — an unlabelled volume still has a serial).
22    pub volume_label: String,
23    /// The 4-byte volume serial number (`BS_VolID` / the `vol`-displayed serial).
24    pub volume_serial: u32,
25    /// Where the record was decoded from.
26    pub source: Provenance,
27}
28
29/// Parse `EMDMgmt` volume records from an already-opened `SOFTWARE` hive. Total over a
30/// valid hive: a subkey whose name has no trailing `_<decimal>` serial is skipped, never
31/// panicked on. `file` is recorded as each record's [`Provenance`] file.
32#[must_use]
33pub fn parse_emdmgmt(hive: &Hive<Cursor<Vec<u8>>>, file: &str) -> Vec<EmdVolume> {
34    let Ok(Some(emd)) = hive.open_key(EMD_PATH) else {
35        return Vec::new();
36    };
37    let Ok(subkeys) = emd.subkeys() else {
38        return Vec::new(); // cov:unreachable: subkeys() only errors on hive corruption
39    };
40    let mut out = Vec::new();
41    for sub in subkeys {
42        let name = sub.name();
43        let Some((label, serial)) = parse_emd_name(&name) else {
44            continue;
45        };
46        out.push(EmdVolume {
47            volume_label: label,
48            volume_serial: serial,
49            source: Provenance {
50                file: file.to_string(),
51                line: 0,
52                key_path: Some(format!("{EMD_PATH}\\{name}")),
53            },
54        });
55    }
56    out
57}
58
59/// Decode an `EMDMgmt` subkey name `<prefix>__<label>_<serial>` into `(label, serial)`.
60///
61/// The volume serial is the trailing `_<decimal>` group; the label is the text after the
62/// **last** `__` (double underscore) in the remainder, which cleanly separates the varying
63/// device prefix from a label that may itself contain single underscores (e.g. `IAMAN $_@`)
64/// or be empty. `None` when there is no trailing decimal serial.
65fn parse_emd_name(name: &str) -> Option<(String, u32)> {
66    let (rest, serial_str) = name.rsplit_once('_')?;
67    let serial: u32 = serial_str.parse().ok()?;
68    let label = match rest.rfind("__") {
69        Some(i) => &rest[i + 2..],
70        None => rest,
71    };
72    Some((label.to_string(), serial))
73}
74
75#[cfg(test)]
76#[allow(clippy::unwrap_used, clippy::expect_used)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn parse_emd_name_splits_prefix_label_and_serial() {
82        // Real CFReDS EMDMgmt key names (answer-key labels).
83        assert_eq!(
84            parse_emd_name("JOJ___Authorized USB_1551191358"),
85            Some(("Authorized USB".to_string(), 1_551_191_358))
86        );
87        assert_eq!(
88            parse_emd_name("TO_B__IAMAN $_@_2657770370"),
89            Some(("IAMAN $_@".to_string(), 2_657_770_370))
90        );
91        // An unlabelled volume → empty label, serial intact.
92        assert_eq!(
93            parse_emd_name("ED_Q___3389815368"),
94            Some((String::new(), 3_389_815_368))
95        );
96        // A label containing internal double underscores keeps everything after the last.
97        assert_eq!(
98            parse_emd_name("PFX__My__Disk_42"),
99            Some(("Disk".to_string(), 42))
100        );
101        // A name with a serial but no `__` boundary → the whole remainder is the label.
102        assert_eq!(parse_emd_name("Label_42"), Some(("Label".to_string(), 42)));
103        // No trailing decimal serial → None, no panic.
104        assert_eq!(parse_emd_name("NoSerialHere"), None);
105        assert_eq!(parse_emd_name("Prefix__Label_notanumber"), None);
106    }
107
108    #[test]
109    fn synthetic_emdmgmt_hive_decodes_the_volume() {
110        const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_emdmgmt.hive");
111        let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
112        let vols = parse_emdmgmt(&hive, "SOFTWARE");
113        let v = vols
114            .iter()
115            .find(|v| v.volume_label == "TESTUSB")
116            .expect("labelled volume present");
117        assert_eq!(v.volume_serial, 1_234_567_890);
118        assert!(v.source.key_path.is_some());
119    }
120
121    #[test]
122    fn a_hive_without_emdmgmt_yields_nothing() {
123        const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_usb_system.hive");
124        let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
125        assert!(parse_emdmgmt(&hive, "SYSTEM").is_empty());
126    }
127}