winget_types/installer/
architecture.rs

1use std::str::FromStr;
2
3use derive_more::Display;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7#[derive(
8    Clone,
9    Copy,
10    Debug,
11    Default,
12    Display,
13    Eq,
14    Hash,
15    Ord,
16    PartialEq,
17    PartialOrd,
18    Serialize,
19    Deserialize,
20)]
21#[serde(rename_all = "lowercase")]
22pub enum Architecture {
23    X86,
24    X64,
25    Arm,
26    Arm64,
27    #[default]
28    Neutral,
29}
30
31#[derive(Error, Debug, Eq, PartialEq)]
32pub enum ArchitectureError {
33    #[error("Failed to parse as valid Architecture")]
34    Invalid,
35}
36
37pub const VALID_FILE_EXTENSIONS: [&str; 7] = [
38    "msix",
39    "msi",
40    "appx",
41    "exe",
42    "zip",
43    "msixbundle",
44    "appxbundle",
45];
46
47const DELIMITERS: [u8; 8] = [b',', b'/', b'\\', b'.', b'_', b'-', b'(', b')'];
48
49const ARCHITECTURES: [(&str, Architecture); 32] = [
50    ("x86-64", Architecture::X64),
51    ("x86_64", Architecture::X64),
52    ("x64", Architecture::X64),
53    ("64-bit", Architecture::X64),
54    ("64bit", Architecture::X64),
55    ("win64a", Architecture::Arm64),
56    ("win64", Architecture::X64),
57    ("winx64", Architecture::X64),
58    ("ia64", Architecture::X64),
59    ("amd64", Architecture::X64),
60    ("x86", Architecture::X86),
61    ("x32", Architecture::X86),
62    ("32-bit", Architecture::X86),
63    ("32bit", Architecture::X86),
64    ("win32", Architecture::X86),
65    ("winx86", Architecture::X86),
66    ("ia32", Architecture::X86),
67    ("i386", Architecture::X86),
68    ("i486", Architecture::X86),
69    ("i586", Architecture::X86),
70    ("i686", Architecture::X86),
71    ("386", Architecture::X86),
72    ("486", Architecture::X86),
73    ("586", Architecture::X86),
74    ("686", Architecture::X86),
75    ("arm64ec", Architecture::Arm64),
76    ("arm64", Architecture::Arm64),
77    ("aarch64", Architecture::Arm64),
78    ("arm", Architecture::Arm),
79    ("armv7", Architecture::Arm),
80    ("aarch", Architecture::Arm),
81    ("neutral", Architecture::Neutral),
82];
83
84impl Architecture {
85    #[must_use]
86    pub const fn is_64_bit(self) -> bool {
87        matches!(self, Self::X64 | Self::Arm64)
88    }
89
90    #[must_use]
91    pub fn from_url(url: &str) -> Option<Self> {
92        // Ignore the casing of the URL
93        let url = url.to_ascii_lowercase();
94
95        let url_bytes = url.as_bytes();
96
97        // Check for {delimiter}{architecture}{delimiter}
98        for (arch_name, arch) in ARCHITECTURES {
99            if let Some(arch_index) = url.rfind(arch_name) {
100                // Get characters before and after the architecture
101                if let (Some(char_before_arch), Some(char_after_arch)) = (
102                    url_bytes.get(arch_index - 1),
103                    url_bytes.get(arch_index + arch_name.len()),
104                ) {
105                    // If the architecture is surrounded by valid delimiters, return the architecture
106                    if DELIMITERS.contains(char_before_arch) && DELIMITERS.contains(char_after_arch)
107                    {
108                        return Some(arch);
109                    }
110                }
111            }
112        }
113
114        // If the architecture has not been found, check for {architecture}.{extension}
115        for extension in VALID_FILE_EXTENSIONS {
116            for (arch_name, arch) in ARCHITECTURES {
117                if url
118                    .rfind(extension)
119                    .map(|index| index - 1)
120                    .filter(|&index| url_bytes.get(index) == Some(&b'.'))
121                    .is_some_and(|end| url.get(end - arch_name.len()..end) == Some(arch_name))
122                {
123                    return Some(arch);
124                }
125            }
126        }
127
128        None
129    }
130}
131
132impl FromStr for Architecture {
133    type Err = ArchitectureError;
134
135    fn from_str(s: &str) -> Result<Self, Self::Err> {
136        match s {
137            "x86" => Ok(Self::X86),
138            "x64" => Ok(Self::X64),
139            "arm" => Ok(Self::Arm),
140            "arm64" => Ok(Self::Arm64),
141            "neutral" => Ok(Self::Neutral),
142            _ => Err(Self::Err::Invalid),
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use rstest::rstest;
150
151    use crate::installer::architecture::Architecture;
152
153    #[rstest]
154    fn x64_architectures_at_end(
155        #[values(
156            "x86-64", "x86_64", "x64", "64-bit", "64bit", "Win64", "Winx64", "ia64", "amd64"
157        )]
158        architecture: &str,
159    ) {
160        assert_eq!(
161            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
162            Some(Architecture::X64)
163        );
164    }
165
166    #[rstest]
167    fn x64_architectures_delimited(
168        #[values(
169            "x86-64", "x86_64", "x64", "64-bit", "64bit", "Win64", "Winx64", "ia64", "amd64"
170        )]
171        architecture: &str,
172        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
173    ) {
174        assert_eq!(
175            Architecture::from_url(&format!(
176                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
177            )),
178            Some(Architecture::X64)
179        );
180    }
181
182    #[rstest]
183    fn x86_architectures_at_end(
184        #[values(
185            "x86", "x32", "32-bit", "32bit", "win32", "winx86", "ia32", "i386", "i486", "i586",
186            "i686", "386", "486", "586", "686"
187        )]
188        architecture: &str,
189    ) {
190        assert_eq!(
191            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
192            Some(Architecture::X86)
193        );
194    }
195
196    #[rstest]
197    fn x86_architectures_delimited(
198        #[values(
199            "x86", "x32", "32-bit", "32bit", "win32", "winx86", "ia32", "i386", "i486", "i586",
200            "i686", "386", "486", "586", "686"
201        )]
202        architecture: &str,
203        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
204    ) {
205        assert_eq!(
206            Architecture::from_url(&format!(
207                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
208            )),
209            Some(Architecture::X86)
210        );
211    }
212
213    #[rstest]
214    fn arm64_architectures_at_end(
215        #[values("arm64ec", "arm64", "aarch64", "win64a")] architecture: &str,
216    ) {
217        assert_eq!(
218            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
219            Some(Architecture::Arm64)
220        );
221    }
222
223    #[rstest]
224    fn arm64_architectures_delimited(
225        #[values("arm64ec", "arm64", "aarch64", "win64a")] architecture: &str,
226        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
227    ) {
228        assert_eq!(
229            Architecture::from_url(&format!(
230                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
231            )),
232            Some(Architecture::Arm64)
233        );
234    }
235
236    #[rstest]
237    fn arm_architectures_at_end(#[values("arm", "armv7", "aarch")] architecture: &str) {
238        assert_eq!(
239            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
240            Some(Architecture::Arm)
241        );
242    }
243
244    #[rstest]
245    fn arm_architectures_delimited(
246        #[values("arm", "armv7", "aarch")] architecture: &str,
247        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
248    ) {
249        assert_eq!(
250            Architecture::from_url(&format!(
251                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
252            )),
253            Some(Architecture::Arm)
254        );
255    }
256
257    #[test]
258    fn no_architecture() {
259        assert_eq!(
260            Architecture::from_url("https://www.example.com/file.exe"),
261            None
262        );
263    }
264}