Skip to main content

usb_forensic/sources/
volume_cache.rs

1//! Adapter: `peripheral-core` [`VolumeLabel`]s (Windows `VolumeInfoCache`) → USB-history
2//! [`Claim`]s.
3//!
4//! `VolumeInfoCache` records the label a user gave a volume against the drive letter it
5//! was mounted at. That label is a [`Attribute::VolumeName`] fact keyed by the **drive
6//! letter**; the correlation layer's drive-letter reconciliation then attributes it to the
7//! device that `MountedDevices` maps to that letter, so a stick's human-readable name
8//! ("Authorized USB") lands on the stick. A pure mapping over already-decoded records.
9
10use crate::{Attribute, Claim, DeviceKey, HistorySource, Provenance, SourceKind, Value};
11use peripheral_core::volume_info::VolumeLabel;
12
13/// A [`HistorySource`] over decoded [`VolumeLabel`]s.
14pub struct VolumeCacheSource<'a> {
15    labels: &'a [VolumeLabel],
16}
17
18impl<'a> VolumeCacheSource<'a> {
19    /// Wrap decoded volume labels (from `peripheral_core::volume_info::parse_volume_info_cache`).
20    #[must_use]
21    pub fn new(labels: &'a [VolumeLabel]) -> Self {
22        Self { labels }
23    }
24}
25
26impl HistorySource for VolumeCacheSource<'_> {
27    fn claims(&self) -> Vec<Claim> {
28        self.labels
29            .iter()
30            .map(|label| Claim {
31                // Keyed by the drive letter (`E:`) — the drive-letter reconciliation joins
32                // it to the device that MountedDevices maps to that letter.
33                device: DeviceKey(format!("{}:", label.drive_letter)),
34                attribute: Attribute::VolumeName,
35                value: Value::Text(label.volume_label.clone()),
36                provenance: Provenance {
37                    source: SourceKind::VolumeInfoCache,
38                    locator: label
39                        .source
40                        .key_path
41                        .clone()
42                        .unwrap_or_else(|| label.source.file.clone()),
43                },
44            })
45            .collect()
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use peripheral_core::Provenance as PcProvenance;
53
54    fn label(drive: char, name: &str) -> VolumeLabel {
55        VolumeLabel {
56            drive_letter: drive,
57            volume_label: name.to_string(),
58            source: PcProvenance {
59                file: "SOFTWARE".to_string(),
60                line: 0,
61                key_path: Some(format!(
62                    "Microsoft\\Windows Search\\VolumeInfoCache\\{drive}:"
63                )),
64            },
65        }
66    }
67
68    #[test]
69    fn volume_label_yields_a_volume_name_claim_keyed_by_drive_letter() {
70        let labels = [label('E', "Authorized USB")];
71        let claims = VolumeCacheSource::new(&labels).claims();
72        assert_eq!(claims.len(), 1);
73        assert_eq!(claims[0].device, DeviceKey("E:".to_string()));
74        assert_eq!(claims[0].attribute, Attribute::VolumeName);
75        assert_eq!(claims[0].value, Value::Text("Authorized USB".to_string()));
76        assert_eq!(claims[0].provenance.source, SourceKind::VolumeInfoCache);
77        assert!(claims[0].provenance.locator.contains("VolumeInfoCache"));
78    }
79
80    #[test]
81    fn multiple_labels_accumulate() {
82        let labels = [label('E', "Authorized USB"), label('F', "IAMAN")];
83        let claims = VolumeCacheSource::new(&labels).claims();
84        assert_eq!(claims.len(), 2);
85        assert_eq!(claims[1].device, DeviceKey("F:".to_string()));
86    }
87
88    #[test]
89    fn locator_falls_back_to_the_file_when_no_key_path() {
90        // A record without a key_path locates by the source file name (defensive branch).
91        let mut l = label('E', "Authorized USB");
92        l.source.key_path = None;
93        let labels = [l];
94        let claims = VolumeCacheSource::new(&labels).claims();
95        assert_eq!(claims[0].provenance.locator, "SOFTWARE");
96    }
97}