winbrew_windows\registry/
mod.rs1use anyhow::Result;
2use regex::RegexBuilder;
3
4mod product_options;
5mod uninstall;
6
7#[cfg(any(test, feature = "testing"))]
8mod test_support;
9
10pub(crate) use product_options::read_product_type;
11use uninstall::uninstall_roots;
12
13#[cfg(any(test, feature = "testing"))]
14pub use test_support::{
15 UninstallEntryGuard, create_test_uninstall_entry,
16 create_test_uninstall_entry_with_install_location,
17};
18
19#[derive(Debug, Clone, Eq, PartialEq)]
21pub struct UninstallEntry {
22 pub display_name: String,
24 pub version: String,
26 pub publisher: String,
28 pub install_location: Option<String>,
30 pub quiet_uninstall_string: Option<String>,
32 pub uninstall_string: Option<String>,
34}
35
36#[derive(Debug, Eq, PartialEq)]
38pub struct AppInfo {
39 pub name: String,
41 pub version: String,
43 pub publisher: String,
45}
46
47pub fn installed_apps() -> Result<Vec<AppInfo>> {
58 installed_apps_with_filter(None)
59}
60
61pub fn installed_apps_matching(filter: &str) -> Result<Vec<AppInfo>> {
78 installed_apps_with_filter(Some(filter))
79}
80
81#[deprecated(note = "use installed_apps() or installed_apps_matching()")]
82#[doc(hidden)]
83pub fn collect_installed_apps(filter: Option<&str>) -> Result<Vec<AppInfo>> {
84 match filter {
85 Some(filter) => installed_apps_matching(filter),
86 None => installed_apps(),
87 }
88}
89
90fn installed_apps_with_filter(filter: Option<&str>) -> Result<Vec<AppInfo>> {
91 let mut apps = Vec::new();
92
93 visit_uninstall_entries(filter, |entry| {
94 apps.push(AppInfo {
95 name: entry.display_name,
96 version: entry.version,
97 publisher: entry.publisher,
98 });
99 })?;
100
101 apps.sort_unstable_by(|a, b| a.name.cmp(&b.name).then_with(|| b.version.cmp(&a.version)));
103
104 apps.dedup_by(|a, b| a.name == b.name);
106
107 Ok(apps)
108}
109
110pub fn uninstall_entries() -> Result<Vec<UninstallEntry>> {
118 uninstall_entries_with_filter(None)
119}
120
121pub fn uninstall_entries_matching(filter: &str) -> Result<Vec<UninstallEntry>> {
136 uninstall_entries_with_filter(Some(filter))
137}
138
139#[deprecated(note = "use uninstall_entries() or uninstall_entries_matching()")]
140#[doc(hidden)]
141pub fn collect_uninstall_entries(filter: Option<&str>) -> Result<Vec<UninstallEntry>> {
142 match filter {
143 Some(filter) => uninstall_entries_matching(filter),
144 None => uninstall_entries(),
145 }
146}
147
148fn uninstall_entries_with_filter(filter: Option<&str>) -> Result<Vec<UninstallEntry>> {
149 let mut entries = Vec::new();
150
151 visit_uninstall_entries(filter, |entry| entries.push(entry))?;
152
153 Ok(entries)
154}
155
156fn visit_uninstall_entries<F>(filter: Option<&str>, mut visit: F) -> Result<()>
157where
158 F: FnMut(UninstallEntry),
159{
160 let pattern = filter
161 .map(|f| {
162 RegexBuilder::new(®ex::escape(f))
163 .case_insensitive(true)
164 .build()
165 })
166 .transpose()?;
167
168 for root in uninstall_roots() {
169 for key_result in root.key().enum_keys() {
170 let Ok(key_name) = key_result else { continue };
171 let Ok(app_key) = root.key().open_subkey(&key_name) else {
172 continue;
173 };
174
175 let Ok(display_name) = app_key.get_value::<String, _>("DisplayName") else {
176 continue;
177 };
178
179 if pattern
180 .as_ref()
181 .is_some_and(|re| !re.is_match(&display_name))
182 {
183 continue;
184 }
185
186 visit(UninstallEntry {
187 display_name,
188 version: app_key
189 .get_value::<String, _>("DisplayVersion")
190 .unwrap_or_default(),
191 publisher: app_key
192 .get_value::<String, _>("Publisher")
193 .unwrap_or_default(),
194 install_location: read_optional_string(&app_key, "InstallLocation"),
195 quiet_uninstall_string: read_optional_string(&app_key, "QuietUninstallString"),
196 uninstall_string: read_optional_string(&app_key, "UninstallString"),
197 });
198 }
199 }
200
201 Ok(())
202}
203
204pub fn read_uninstall_registry_value(key_name: &str, value_name: &str) -> Option<String> {
209 for root in uninstall_roots() {
210 let Ok(app_key) = root.key().open_subkey(key_name) else {
211 continue;
212 };
213
214 let Ok(value) = app_key.get_value::<String, _>(value_name) else {
215 continue;
216 };
217
218 if !value.trim().is_empty() {
219 return Some(value);
220 }
221 }
222
223 None
224}
225
226#[deprecated(note = "use read_uninstall_registry_value")]
227#[doc(hidden)]
228pub fn uninstall_value(key_name: &str, value_name: &str) -> Option<String> {
229 read_uninstall_registry_value(key_name, value_name)
230}
231
232fn read_optional_string(app_key: &winreg::RegKey, value_name: &str) -> Option<String> {
233 let Ok(value) = app_key.get_value::<String, _>(value_name) else {
234 return None;
235 };
236
237 let value = value.trim();
238
239 if value.is_empty() {
240 None
241 } else {
242 Some(value.to_string())
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::{installed_apps_matching, uninstall_entries_matching};
249 use crate::registry::create_test_uninstall_entry_with_install_location;
250 use std::path::PathBuf;
251 use std::time::{SystemTime, UNIX_EPOCH};
252
253 fn temp_install_dir(name: &str) -> PathBuf {
254 std::env::temp_dir().join(format!(
255 "winbrew-registry-helper-{}-{}-{name}",
256 std::process::id(),
257 SystemTime::now()
258 .duration_since(UNIX_EPOCH)
259 .expect("time should be monotonic")
260 .as_nanos()
261 ))
262 }
263
264 #[test]
265 fn collects_uninstall_entries_and_projects_them_to_apps() {
266 let package_name = "WinBrew Registry Helper";
267 let install_dir = temp_install_dir("registry-helper");
268 let _guard = create_test_uninstall_entry_with_install_location(
269 package_name,
270 Some(&install_dir),
271 Some("/quiet"),
272 Some("/uninstall"),
273 )
274 .expect("test uninstall entry should be created");
275
276 let entries = uninstall_entries_matching(package_name)
277 .expect("uninstall entries should be collected");
278
279 assert_eq!(entries.len(), 1);
280 let entry = &entries[0];
281 assert_eq!(entry.display_name, package_name);
282 assert_eq!(
283 entry.install_location.as_deref(),
284 Some(install_dir.to_string_lossy().as_ref())
285 );
286 assert_eq!(entry.quiet_uninstall_string.as_deref(), Some("/quiet"));
287 assert_eq!(entry.uninstall_string.as_deref(), Some("/uninstall"));
288
289 let apps = installed_apps_matching(package_name).expect("apps should be collected");
290 assert_eq!(apps.len(), 1);
291 assert_eq!(apps[0].name, package_name);
292 }
293}