winreg_artifacts/shellbags.rs
1//! ShellBags registry artifact extractor.
2//!
3//! ShellBags record folder navigation history in Windows. `BagMRU` keys hold
4//! slot values (numeric names "0", "1", ...) containing binary `ShellItem` data,
5//! and a `MRUListEx` value encoding the access order.
6//!
7//! This implementation walks `BagMRU` keys recursively and emits one
8//! `ShellbagEntry` per key. Full `ShellItem` binary parsing is out of scope;
9//! slot data is represented as a human-readable size preview.
10
11use std::io::Cursor;
12
13use winreg_core::hive::Hive;
14use winreg_core::key::{filetime_to_datetime, Key};
15
16// ---------------------------------------------------------------------------
17// Output type
18// ---------------------------------------------------------------------------
19
20/// A single `BagMRU` entry from the ShellBags registry area.
21#[derive(Debug, Clone, serde::Serialize)]
22pub struct ShellbagEntry {
23 /// Reconstructed / descriptive folder path.
24 /// For this implementation, slot data is represented as
25 /// `"BagMRU[slot=N, size=M bytes]"` for each numeric slot value present,
26 /// or an empty string if no slot values exist.
27 pub path: String,
28 /// Registry path to this `BagMRU` key (relative to hive root).
29 pub key_path: String,
30 /// Key `LastWriteTime` as ISO 8601, or `None` if unavailable.
31 pub last_written: Option<String>,
32 /// Decoded MRU order from `MRUListEx` (slot index strings),
33 /// terminator (0xFFFFFFFF) is excluded. Empty if value is absent.
34 pub mru_order: Vec<String>,
35}
36
37// ---------------------------------------------------------------------------
38// BagMRU candidate paths to probe (NTUSER.DAT and USRCLASS.DAT variants)
39// ---------------------------------------------------------------------------
40
41const BAGMRU_PATHS: &[&str] = &[
42 "Software\\Microsoft\\Windows\\Shell\\BagMRU",
43 "Software\\Microsoft\\Windows\\ShellNoRoam\\BagMRU",
44 "Local Settings\\Software\\Microsoft\\Windows\\Shell\\BagMRU",
45];
46
47// ---------------------------------------------------------------------------
48// Public parse function
49// ---------------------------------------------------------------------------
50
51/// Extract all `ShellBag` entries from a hive.
52///
53/// Probes several well-known `BagMRU` key paths. For each that exists, walks
54/// the key tree recursively and emits one [`ShellbagEntry`] per key.
55///
56/// Returns an empty `Vec` if no `BagMRU` key is present.
57pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<ShellbagEntry> {
58 let mut entries = Vec::new();
59
60 for &path in BAGMRU_PATHS {
61 if let Ok(Some(root)) = hive.open_key(path) {
62 walk_key(&root, path, &mut entries);
63 }
64 }
65
66 entries
67}
68
69// ---------------------------------------------------------------------------
70// Recursive key walker
71// ---------------------------------------------------------------------------
72
73fn walk_key(key: &Key<'_>, key_path: &str, entries: &mut Vec<ShellbagEntry>) {
74 let last_written = filetime_to_datetime(key.last_written_raw())
75 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string());
76
77 // Decode MRUListEx value
78 let mru_order = decode_mrulistex(key);
79
80 // Build a path description from numeric slot values.
81 let path = build_slot_path(key);
82
83 entries.push(ShellbagEntry {
84 path,
85 key_path: key_path.to_string(),
86 last_written,
87 mru_order,
88 });
89
90 // Recurse into subkeys
91 if let Ok(subkeys) = key.subkeys() {
92 for subkey in subkeys {
93 let child_path = format!("{}\\{}", key_path, subkey.name());
94 walk_key(&subkey, &child_path, entries);
95 }
96 }
97}
98
99// ---------------------------------------------------------------------------
100// MRUListEx decoder
101// ---------------------------------------------------------------------------
102
103/// Decode `MRUListEx`: a `REG_BINARY` value holding an array of `u32` LE
104/// slot indices, terminated by `0xFFFF_FFFF`.
105fn decode_mrulistex(key: &Key<'_>) -> Vec<String> {
106 let Ok(Some(val)) = key.value("MRUListEx") else {
107 return Vec::new();
108 };
109 let Ok(raw) = val.raw_data() else {
110 return Vec::new();
111 };
112
113 let mut order = Vec::new();
114 let mut i = 0;
115 while i + 4 <= raw.len() {
116 let slot = u32::from_le_bytes([raw[i], raw[i + 1], raw[i + 2], raw[i + 3]]);
117 if slot == 0xFFFF_FFFF {
118 break;
119 }
120 order.push(slot.to_string());
121 i += 4;
122 }
123 order
124}
125
126// ---------------------------------------------------------------------------
127// Slot path builder
128// ---------------------------------------------------------------------------
129
130/// Build a descriptive path string from numeric slot values in this key.
131///
132/// Numeric value names ("0", "1", ...) each hold a binary `ShellItem` blob. Each
133/// slot is decoded with the [`shellitem`] primitive to its real folder name
134/// (volume, folder, file entry). When a slot does not decode to a named item
135/// (truncated or unrecognised class), it degrades to the `BagMRU[slot=N,
136/// size=M bytes]` preview so the slot is never silently dropped.
137fn build_slot_path(key: &Key<'_>) -> String {
138 let Ok(values) = key.values() else {
139 return String::new();
140 };
141
142 let mut parts: Vec<String> = Vec::new();
143 for val in values {
144 let name = val.name();
145 // Numeric names are slot entries (skip "MRUListEx" and others).
146 if name.chars().all(|c| c.is_ascii_digit()) {
147 parts.push(decode_slot(&name, &val));
148 }
149 }
150
151 parts.join("; ")
152}
153
154/// Decode one numeric slot value: its real shell-namespace folder name when the
155/// `ShellItem` blob decodes, otherwise a size preview (never silently dropped).
156fn decode_slot(slot: &str, val: &winreg_core::value::Value<'_>) -> String {
157 if let Ok(raw) = val.raw_data() {
158 let items = shellitem::parse_idlist(&raw);
159 let path = shellitem::reconstruct_path(&items);
160 if !path.is_empty() {
161 return path;
162 }
163 }
164 let size = val.data_size() as usize;
165 format!("BagMRU[slot={slot}, size={size} bytes]")
166}