peripheral_core/
volume_info.rs1use crate::Provenance;
11use std::io::Cursor;
12use winreg_core::hive::Hive;
13use winreg_core::key::Key;
14
15const CACHE_PATH: &str = "Microsoft\\Windows Search\\VolumeInfoCache";
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct VolumeLabel {
21 pub drive_letter: char,
23 pub volume_label: String,
25 pub source: Provenance,
27}
28
29#[must_use]
35pub fn parse_volume_info_cache(hive: &Hive<Cursor<Vec<u8>>>, file: &str) -> Vec<VolumeLabel> {
36 let Ok(Some(cache)) = hive.open_key(CACHE_PATH) else {
37 return Vec::new();
38 };
39 let Ok(subkeys) = cache.subkeys() else {
40 return Vec::new(); };
42 let mut out = Vec::new();
43 for sub in subkeys {
44 let name = sub.name();
45 let Some(letter) = drive_letter(&name) else {
46 continue;
47 };
48 let Some(label) = string_value(&sub, "VolumeLabel") else {
49 continue;
50 };
51 out.push(VolumeLabel {
52 drive_letter: letter,
53 volume_label: label,
54 source: Provenance {
55 file: file.to_string(),
56 line: 0,
57 key_path: Some(format!("{CACHE_PATH}\\{name}")),
58 },
59 });
60 }
61 out
62}
63
64fn drive_letter(name: &str) -> Option<char> {
67 let mut chars = name.chars();
68 let letter = chars.next()?;
69 if letter.is_ascii_alphabetic() && chars.next() == Some(':') && chars.next().is_none() {
70 Some(letter.to_ascii_uppercase())
71 } else {
72 None
73 }
74}
75
76fn string_value(key: &Key<'_>, name: &str) -> Option<String> {
78 key.value(name)
79 .ok()
80 .flatten()
81 .and_then(|v| v.as_string().ok())
82 .filter(|s| !s.is_empty())
83}
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used, clippy::expect_used)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn synthetic_volume_info_cache_decodes_labelled_drives_only() {
92 const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_volume_info_cache.hive");
95 let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
96 let labels = parse_volume_info_cache(&hive, "SOFTWARE");
97 assert_eq!(labels.len(), 1);
98 assert_eq!(labels[0].drive_letter, 'E');
99 assert_eq!(labels[0].volume_label, "TESTLABEL");
100 assert!(labels[0].source.key_path.is_some());
101 }
102
103 #[test]
104 fn drive_letter_accepts_only_well_formed_names() {
105 assert_eq!(drive_letter("E:"), Some('E'));
106 assert_eq!(drive_letter("c:"), Some('C'));
107 assert_eq!(drive_letter("ZZ:"), None);
108 assert_eq!(drive_letter("E"), None);
109 assert_eq!(drive_letter("1:"), None);
110 assert_eq!(drive_letter(""), None);
111 }
112
113 #[test]
114 fn a_hive_without_the_cache_yields_nothing() {
115 const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_usb_system.hive");
117 let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
118 assert!(parse_volume_info_cache(&hive, "SYSTEM").is_empty());
119 }
120}