use crate::Provenance;
use shellitem::{parse_idlist, reconstruct_path, ShellItem, ShellItemKind};
use std::collections::HashSet;
use std::io::Cursor;
use winreg_core::hive::Hive;
use winreg_core::key::Key;
const BAGMRU_PATHS: &[&str] = &[
"Software\\Microsoft\\Windows\\Shell\\BagMRU",
"Local Settings\\Software\\Microsoft\\Windows\\Shell\\BagMRU",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShellbagEntry {
pub path: String,
pub drive_letter: Option<char>,
pub last_write: Option<i64>,
pub source: Provenance,
}
#[must_use]
pub fn parse_shellbags(hive: &Hive<Cursor<Vec<u8>>>, file: &str) -> Vec<ShellbagEntry> {
let mut out = Vec::new();
for &root_path in BAGMRU_PATHS {
let Ok(Some(root)) = hive.open_key(root_path) else {
continue;
};
walk(root, root_path, file, &mut out);
}
out
}
fn walk(root: Key<'_>, root_path: &str, file: &str, out: &mut Vec<ShellbagEntry>) {
let mut visited: HashSet<u32> = HashSet::new();
let mut stack: Vec<(Key<'_>, Vec<ShellItem>, String)> =
vec![(root, Vec::new(), root_path.to_string())];
while let Some((key, parent_items, key_path)) = stack.pop() {
if !visited.insert(key.offset().0) {
continue; }
let Ok(subkeys) = key.subkeys() else {
continue; };
for sub in subkeys {
let name = sub.name();
if name.parse::<u32>().is_err() {
continue;
}
let bytes = key
.value(&name)
.ok()
.flatten()
.and_then(|v| v.raw_data().ok())
.unwrap_or_default();
let mut items = parent_items.clone();
items.extend(parse_idlist(&bytes));
let sub_path = format!("{key_path}\\{name}");
if let Some(entry) = volume_entry(&items, &sub, &sub_path, file) {
out.push(entry);
}
stack.push((sub, items, sub_path));
}
}
}
fn volume_entry(
items: &[ShellItem],
key: &Key<'_>,
key_path: &str,
file: &str,
) -> Option<ShellbagEntry> {
let volume = items.iter().find(|i| i.kind == ShellItemKind::Volume)?;
Some(ShellbagEntry {
path: reconstruct_path(items),
drive_letter: drive_letter(volume),
last_write: last_written_epoch(key),
source: Provenance {
file: file.to_string(),
line: 0,
key_path: Some(key_path.to_string()),
},
})
}
fn last_written_epoch(key: &Key<'_>) -> Option<i64> {
Some(key.last_written()?.as_second())
}
fn drive_letter(volume: &ShellItem) -> Option<char> {
let mut chars = volume.name.as_deref()?.chars();
let letter = chars.next()?;
(letter.is_ascii_alphabetic() && chars.next() == Some(':')).then(|| letter.to_ascii_uppercase())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::io::Cursor;
use winreg_core::hive::Hive;
fn hive() -> Hive<Cursor<Vec<u8>>> {
const BYTES: &[u8] = include_bytes!("../../tests/data/synthetic_bagmru.hive");
Hive::from_bytes(BYTES.to_vec()).expect("valid REGF")
}
#[test]
fn surfaces_the_volume_and_the_folder_browsed_on_it() {
let entries = parse_shellbags(&hive(), "NTUSER.DAT");
assert_eq!(entries.len(), 2);
assert!(entries.iter().all(|e| e.drive_letter == Some('E')));
let folder = entries
.iter()
.find(|e| e.path.contains("photos"))
.expect("the browsed E:\\photos folder is surfaced");
assert!(folder.path.contains("E:"));
assert_eq!(folder.last_write, Some(1_600_000_000));
assert_eq!(folder.source.file, "NTUSER.DAT");
assert!(folder
.source
.key_path
.as_deref()
.is_some_and(|k| k.contains("BagMRU")));
}
#[test]
fn a_hive_without_bagmru_yields_nothing() {
const SYS: &[u8] = include_bytes!("../../tests/data/synthetic_usb_system.hive");
let hive = Hive::from_bytes(SYS.to_vec()).expect("valid REGF");
assert!(parse_shellbags(&hive, "NTUSER.DAT").is_empty());
}
}