winbrew_engines\windows\exe/
metadata.rs1use anyhow::Result;
2use std::fs;
3use std::path::Path;
4use tracing::warn;
5
6use crate::windows_dep::installed::{UninstallEntry, uninstall_entries_matching};
7
8pub(super) enum NativeExeInstallMetadata {
9 QuietOnly(String),
10 QuietAndStandard {
11 quiet_uninstall_command: String,
12 uninstall_command: String,
13 },
14 StandardOnly(String),
15}
16
17pub(super) fn capture_native_exe_metadata(
18 package_name: &str,
19 install_dir: &Path,
20) -> Option<NativeExeInstallMetadata> {
21 capture_native_exe_metadata_with(package_name, install_dir, uninstall_entries_matching)
22}
23
24pub(super) fn capture_native_exe_metadata_with(
25 package_name: &str,
26 install_dir: &Path,
27 collect_entries: impl FnOnce(&str) -> Result<Vec<UninstallEntry>>,
28) -> Option<NativeExeInstallMetadata> {
29 let package_name = package_name.trim();
30 let mut best_match: Option<(u8, NativeExeInstallMetadata)> = None;
31 let mut saw_ambiguous_match = false;
32
33 let Ok(entries) = collect_entries(package_name) else {
34 return None;
35 };
36
37 for entry in entries {
38 if !entry.display_name.trim().eq_ignore_ascii_case(package_name) {
39 continue;
40 }
41
42 let install_location_exact = match entry.install_location.as_deref() {
43 Some(value) if !value.trim().is_empty() => {
44 if !same_install_location(Path::new(value), install_dir) {
45 continue;
46 }
47
48 true
49 }
50 _ => false,
51 };
52
53 let candidate = match (
54 entry.quiet_uninstall_string.as_deref(),
55 entry.uninstall_string.as_deref(),
56 ) {
57 (Some(quiet_uninstall_command), Some(uninstall_command)) => Some((
58 native_exe_metadata_priority(install_location_exact, 3),
59 NativeExeInstallMetadata::QuietAndStandard {
60 quiet_uninstall_command: quiet_uninstall_command.to_string(),
61 uninstall_command: uninstall_command.to_string(),
62 },
63 )),
64 (Some(quiet_uninstall_command), None) => Some((
65 native_exe_metadata_priority(install_location_exact, 2),
66 NativeExeInstallMetadata::QuietOnly(quiet_uninstall_command.to_string()),
67 )),
68 (None, Some(uninstall_command)) => Some((
69 native_exe_metadata_priority(install_location_exact, 1),
70 NativeExeInstallMetadata::StandardOnly(uninstall_command.to_string()),
71 )),
72 (None, None) => None,
73 };
74
75 let Some((priority, metadata)) = candidate else {
76 continue;
77 };
78
79 match best_match.as_mut() {
80 Some((best_priority, best_metadata)) => {
81 if priority > *best_priority {
82 *best_priority = priority;
83 *best_metadata = metadata;
84 } else if priority == *best_priority {
85 saw_ambiguous_match = true;
86 }
87 }
88 None => {
89 best_match = Some((priority, metadata));
90 }
91 }
92 }
93
94 if saw_ambiguous_match {
95 warn!(
96 package = package_name,
97 install_dir = %install_dir.display(),
98 "multiple native executable uninstall registry entries matched; using the best available metadata"
99 );
100 }
101
102 best_match.map(|(_, metadata)| metadata)
103}
104
105fn native_exe_metadata_priority(install_location_exact: bool, metadata_priority: u8) -> u8 {
106 if install_location_exact {
107 10 + metadata_priority
108 } else {
109 metadata_priority
110 }
111}
112
113pub(super) fn same_install_location(left: &Path, right: &Path) -> bool {
114 match (fs::canonicalize(left), fs::canonicalize(right)) {
115 (Ok(left), Ok(right)) => left == right,
116 _ => normalize_path_text(left) == normalize_path_text(right),
117 }
118}
119
120fn normalize_path_text(path: &Path) -> String {
121 path.to_string_lossy()
122 .replace('/', "\\")
123 .trim_end_matches('\\')
124 .to_ascii_lowercase()
125}