use crate::Provenance;
use std::io::Cursor;
use winreg_core::hive::Hive;
use winreg_core::key::Key;
const CACHE_PATH: &str = "Microsoft\\Windows Search\\VolumeInfoCache";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VolumeLabel {
pub drive_letter: char,
pub volume_label: String,
pub source: Provenance,
}
#[must_use]
pub fn parse_volume_info_cache(hive: &Hive<Cursor<Vec<u8>>>, file: &str) -> Vec<VolumeLabel> {
let Ok(Some(cache)) = hive.open_key(CACHE_PATH) else {
return Vec::new();
};
let Ok(subkeys) = cache.subkeys() else {
return Vec::new(); };
let mut out = Vec::new();
for sub in subkeys {
let name = sub.name();
let Some(letter) = drive_letter(&name) else {
continue;
};
let Some(label) = string_value(&sub, "VolumeLabel") else {
continue;
};
out.push(VolumeLabel {
drive_letter: letter,
volume_label: label,
source: Provenance {
file: file.to_string(),
line: 0,
key_path: Some(format!("{CACHE_PATH}\\{name}")),
},
});
}
out
}
fn drive_letter(name: &str) -> Option<char> {
let mut chars = name.chars();
let letter = chars.next()?;
if letter.is_ascii_alphabetic() && chars.next() == Some(':') && chars.next().is_none() {
Some(letter.to_ascii_uppercase())
} else {
None
}
}
fn string_value(key: &Key<'_>, name: &str) -> Option<String> {
key.value(name)
.ok()
.flatten()
.and_then(|v| v.as_string().ok())
.filter(|s| !s.is_empty())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn synthetic_volume_info_cache_decodes_labelled_drives_only() {
const HIVE: &[u8] = include_bytes!("../../tests/data/synthetic_volume_info_cache.hive");
let hive = Hive::from_bytes(HIVE.to_vec()).expect("valid REGF");
let labels = parse_volume_info_cache(&hive, "SOFTWARE");
assert_eq!(labels.len(), 1);
assert_eq!(labels[0].drive_letter, 'E');
assert_eq!(labels[0].volume_label, "TESTLABEL");
assert!(labels[0].source.key_path.is_some());
}
#[test]
fn drive_letter_accepts_only_well_formed_names() {
assert_eq!(drive_letter("E:"), Some('E'));
assert_eq!(drive_letter("c:"), Some('C'));
assert_eq!(drive_letter("ZZ:"), None);
assert_eq!(drive_letter("E"), None);
assert_eq!(drive_letter("1:"), None);
assert_eq!(drive_letter(""), None);
}
#[test]
fn a_hive_without_the_cache_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_volume_info_cache(&hive, "SYSTEM").is_empty());
}
}