winbrew_windows\deployment\msix/
install.rs

1use anyhow::{Context, Result};
2use std::fs;
3use std::path::Path;
4
5use super::installed_package_full_name;
6
7use windows::Management::Deployment::{AddPackageOptions, PackageManager};
8use windows::core::HSTRING;
9
10/// Install an MSIX package from a downloaded file and return the installed full name.
11///
12/// On Windows the input path is canonicalized, converted to a file URI, and
13/// handed to `PackageManager::AddPackageByUriAsync`. The returned string is the
14/// installed package full name, which is what WinBrew stores in an engine
15/// receipt for later removal.
16pub fn install(download_path: &Path, package_name: &str) -> Result<String> {
17    let package_manager = PackageManager::new().context("failed to create package manager")?;
18    let package_uri = file_uri_for_path(download_path)?;
19    let options = AddPackageOptions::new().context("failed to create add package options")?;
20
21    package_manager
22        .AddPackageByUriAsync(&package_uri, &options)
23        .context("failed to start msix installation")?
24        .join()
25        .context("msix install failed")?;
26
27    installed_package_full_name(package_name)
28}
29
30fn file_uri_for_path(path: &Path) -> Result<windows::Foundation::Uri> {
31    let absolute_path =
32        fs::canonicalize(path).with_context(|| format!("failed to resolve {}", path.display()))?;
33    let file_uri = file_uri_string(&absolute_path);
34    let file_uri = HSTRING::from(file_uri);
35
36    windows::Foundation::Uri::CreateUri(&file_uri)
37        .context("failed to create file URI for msix installer")
38}
39
40fn file_uri_string(path: &Path) -> String {
41    let path = path.to_string_lossy();
42    let (scheme, path) = if let Some(path) = path.strip_prefix(r"\\?\UNC\") {
43        ("file://", path)
44    } else if let Some(path) = path.strip_prefix(r"\\?\") {
45        ("file:///", path)
46    } else if let Some(path) = path.strip_prefix(r"\\") {
47        ("file://", path)
48    } else {
49        ("file:///", path.as_ref())
50    };
51
52    let mut file_uri = String::with_capacity(scheme.len() + path.len() + path.len() / 4);
53    file_uri.push_str(scheme);
54    encode_file_uri_path_into(path, &mut file_uri);
55
56    file_uri
57}
58
59#[cfg(test)]
60fn encode_file_uri_path(path: &str) -> String {
61    let mut encoded = String::with_capacity(path.len() + path.len() / 4);
62    encode_file_uri_path_into(path, &mut encoded);
63
64    encoded
65}
66
67fn encode_file_uri_path_into(path: &str, encoded: &mut String) {
68    const HEX: &[u8; 16] = b"0123456789ABCDEF";
69
70    for ch in path.chars() {
71        if ch == '\\' {
72            encoded.push('/');
73        } else if is_uri_path_char(ch) {
74            encoded.push(ch);
75        } else {
76            let mut buffer = [0u8; 4];
77            for &byte in ch.encode_utf8(&mut buffer).as_bytes() {
78                encoded.push('%');
79                encoded.push(HEX[(byte >> 4) as usize] as char);
80                encoded.push(HEX[(byte & 0x0F) as usize] as char);
81            }
82        }
83    }
84}
85
86fn is_uri_path_char(ch: char) -> bool {
87    ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '.' | '_' | '~' | ':')
88}
89
90#[cfg(test)]
91mod tests {
92    use super::encode_file_uri_path;
93    use super::file_uri_string;
94    use std::path::Path;
95
96    #[test]
97    fn encode_file_uri_path_escapes_special_characters() {
98        let encoded = encode_file_uri_path(r"C:\pkg\o'ne tool\app#.msix");
99
100        assert_eq!(encoded, "C:/pkg/o%27ne%20tool/app%23.msix");
101    }
102
103    #[test]
104    fn encode_file_uri_path_keeps_safe_segments() {
105        let encoded = encode_file_uri_path(r"C:\Packages\Contoso.App\tool-1.0.msix");
106
107        assert_eq!(encoded, "C:/Packages/Contoso.App/tool-1.0.msix");
108    }
109
110    #[test]
111    fn file_uri_string_strips_verbatim_path_prefix() {
112        let uri = file_uri_string(Path::new(r"\\?\C:\pkg\o'ne tool\app#.msix"));
113
114        assert_eq!(uri, "file:///C:/pkg/o%27ne%20tool/app%23.msix");
115    }
116
117    #[test]
118    fn file_uri_string_handles_unc_paths() {
119        let uri = file_uri_string(Path::new(r"\\server\share\pkg\tool msix.appx"));
120
121        assert_eq!(uri, "file://server/share/pkg/tool%20msix.appx");
122    }
123}