winbrew_windows\deployment\msix/
remove.rs

1use anyhow::{Context, Result};
2
3use windows::ApplicationModel::Package;
4use windows::Management::Deployment::PackageManager;
5use windows::core::HSTRING;
6
7/// Remove an installed MSIX package by its full package name.
8///
9/// The caller is expected to pass the exact full name stored in the install
10/// receipt. This keeps removal deterministic and avoids ambiguous package name
11/// lookups at uninstall time.
12pub fn remove(package_full_name: &str) -> Result<()> {
13    let package_manager = PackageManager::new().context("failed to create package manager")?;
14
15    package_manager
16        .RemovePackageAsync(&HSTRING::from(package_full_name))
17        .with_context(|| format!("failed to start uninstall for {package_full_name}"))?
18        .join()
19        .with_context(|| format!("msix uninstall failed for {package_full_name}"))?;
20
21    Ok(())
22}
23
24pub(crate) fn matching_package_full_names(
25    package_manager: &PackageManager,
26    package_name: &str,
27) -> Result<Vec<HSTRING>> {
28    let normalized_name = package_name.trim().to_ascii_lowercase();
29    let mut matching_full_names = Vec::new();
30
31    if let Ok(package) = package_manager.FindPackageByPackageFullName(&HSTRING::from(package_name))
32        && package_matches(&package, &normalized_name)?
33    {
34        matching_full_names.push(package_full_name(&package)?);
35    }
36
37    for package in package_manager
38        .FindPackagesByPackageFamilyName(&HSTRING::from(package_name))
39        .context("failed to enumerate installed packages")?
40    {
41        if package_matches(&package, &normalized_name)? {
42            matching_full_names.push(package_full_name(&package)?);
43        }
44    }
45
46    matching_full_names.sort_by_key(|value| value.to_string());
47    matching_full_names.dedup();
48
49    Ok(matching_full_names)
50}
51
52fn package_matches(package: &Package, expected_name: &str) -> Result<bool> {
53    let package_id = package.Id().context("failed to read package identity")?;
54
55    Ok(identity_matches(
56        &package_id
57            .Name()
58            .context("failed to read package name")?
59            .to_string(),
60        &package_id
61            .FamilyName()
62            .context("failed to read package family name")?
63            .to_string(),
64        &package_id
65            .FullName()
66            .context("failed to read package full name")?
67            .to_string(),
68        expected_name,
69    ))
70}
71
72fn package_full_name(package: &Package) -> Result<HSTRING> {
73    package
74        .Id()
75        .context("failed to read package identity")?
76        .FullName()
77        .context("failed to read package full name")
78}
79
80fn identity_matches(name: &str, family_name: &str, full_name: &str, expected_name: &str) -> bool {
81    [name, family_name, full_name]
82        .into_iter()
83        .any(|value| value.eq_ignore_ascii_case(expected_name))
84}
85
86#[cfg(test)]
87mod tests {
88    use super::identity_matches;
89
90    #[test]
91    fn identity_matches_name_family_or_full_name() {
92        assert!(identity_matches(
93            "Contoso.App",
94            "Contoso.App_123abc",
95            "Contoso.App_123abc!App",
96            "contoso.app"
97        ));
98        assert!(identity_matches(
99            "Contoso.App",
100            "Contoso.App_123abc",
101            "Contoso.App_123abc!App",
102            "contoso.app_123abc"
103        ));
104        assert!(identity_matches(
105            "Contoso.App",
106            "Contoso.App_123abc",
107            "Contoso.App_123abc!App",
108            "contoso.app_123abc!app"
109        ));
110    }
111
112    #[test]
113    fn identity_matches_rejects_other_names() {
114        assert!(!identity_matches(
115            "Contoso.App",
116            "Contoso.App_123abc",
117            "Contoso.App_123abc!App",
118            "fabrikam.tool"
119        ));
120    }
121}