winreg_artifacts/
userassist.rs1use std::io::Cursor;
9
10use winreg_core::hive::Hive;
11use winreg_core::key::filetime_to_datetime;
12
13const GUID_EXE: &str = "{CEBFF5CD-ACE2-4F4F-9178-9926F41749EA}";
17
18const GUID_LNK: &str = "{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}";
20
21const KNOWN_GUIDS: &[&str] = &[GUID_EXE, GUID_LNK];
23
24const UA_DATA_SIZE: usize = 68; #[derive(Debug, Clone, serde::Serialize)]
33pub struct UserAssistEntry {
34 pub program: String,
36 pub run_count: u32,
38 pub focus_count: u32,
40 pub focus_duration_ms: u32,
42 pub last_run: Option<String>,
44 pub guid: String,
46}
47
48pub fn rot13_decode(s: &str) -> String {
52 s.chars()
53 .map(|c| match c {
54 'A'..='Z' => (b'A' + (c as u8 - b'A' + 13) % 26) as char,
55 'a'..='z' => (b'a' + (c as u8 - b'a' + 13) % 26) as char,
56 other => other,
57 })
58 .collect()
59}
60
61pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<UserAssistEntry> {
71 let mut entries = Vec::new();
72
73 for &guid in KNOWN_GUIDS {
74 let count_path = format!(
75 "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist\\{guid}\\Count"
76 );
77
78 let Ok(Some(count_key)) = hive.open_key(&count_path) else {
79 continue;
80 };
81
82 let Ok(values) = count_key.values() else {
83 continue;
84 };
85
86 for val in values {
87 let Ok(raw) = val.raw_data() else {
88 continue;
89 };
90
91 if raw.len() < UA_DATA_SIZE {
92 continue;
93 }
94
95 let run_count = winreg_core::bytes::le_u32(&raw[..], 4);
96 let focus_count = winreg_core::bytes::le_u32(&raw[..], 8);
97 let focus_duration_ms = winreg_core::bytes::le_u32(&raw[..], 12);
98 let filetime = winreg_core::bytes::le_u64(&raw[..], 60);
99
100 let last_run = filetime_to_datetime(filetime)
101 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string());
102
103 let program = rot13_decode(&val.name());
104
105 entries.push(UserAssistEntry {
106 program,
107 run_count,
108 focus_count,
109 focus_duration_ms,
110 last_run,
111 guid: guid.to_string(),
112 });
113 }
114 }
115
116 entries
117}
118
119#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn rot13_roundtrip_hello() {
127 let s = "Hello, World!";
128 assert_eq!(rot13_decode(&rot13_decode(s)), s);
129 }
130
131 #[test]
132 fn rot13_numbers_unchanged() {
133 assert_eq!(rot13_decode("12345"), "12345");
134 }
135
136 #[test]
137 fn rot13_special_chars_unchanged() {
138 assert_eq!(rot13_decode("\\:{}[]()"), "\\:{}[]()");
139 }
140
141 #[test]
142 fn rot13_uppercase() {
143 assert_eq!(rot13_decode("HELLO"), "URYYB");
144 }
145
146 #[test]
147 fn rot13_lowercase() {
148 assert_eq!(rot13_decode("hello"), "uryyb");
149 }
150}