winget_types/installer/
architecture.rs

1use core::{fmt, str::FromStr};
2
3use thiserror::Error;
4
5use super::VALID_FILE_EXTENSIONS;
6
7#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
10pub enum Architecture {
11    X86,
12    X64,
13    Arm,
14    Arm64,
15    #[default]
16    Neutral,
17}
18
19#[derive(Error, Debug, Eq, PartialEq)]
20#[error("Failed to parse as valid Architecture")]
21pub struct ParseArchitectureError;
22
23const DELIMITERS: [u8; 8] = [b',', b'/', b'\\', b'.', b'_', b'-', b'(', b')'];
24
25const ARCHITECTURES: [(&str, Architecture); 32] = [
26    ("x86-64", Architecture::X64),
27    ("x86_64", Architecture::X64),
28    ("x64", Architecture::X64),
29    ("64-bit", Architecture::X64),
30    ("64bit", Architecture::X64),
31    ("win64a", Architecture::Arm64),
32    ("win64", Architecture::X64),
33    ("winx64", Architecture::X64),
34    ("ia64", Architecture::X64),
35    ("amd64", Architecture::X64),
36    ("x86", Architecture::X86),
37    ("x32", Architecture::X86),
38    ("32-bit", Architecture::X86),
39    ("32bit", Architecture::X86),
40    ("win32", Architecture::X86),
41    ("winx86", Architecture::X86),
42    ("ia32", Architecture::X86),
43    ("i386", Architecture::X86),
44    ("i486", Architecture::X86),
45    ("i586", Architecture::X86),
46    ("i686", Architecture::X86),
47    ("386", Architecture::X86),
48    ("486", Architecture::X86),
49    ("586", Architecture::X86),
50    ("686", Architecture::X86),
51    ("arm64ec", Architecture::Arm64),
52    ("arm64", Architecture::Arm64),
53    ("aarch64", Architecture::Arm64),
54    ("arm", Architecture::Arm),
55    ("armv7", Architecture::Arm),
56    ("aarch", Architecture::Arm),
57    ("neutral", Architecture::Neutral),
58];
59
60impl Architecture {
61    /// Returns `true` if the architecture is a 64-bit architecture.
62    ///
63    /// # Examples
64    /// ```
65    /// use winget_types::installer::Architecture;
66    ///
67    /// assert!(Architecture::X64.is_64_bit());
68    /// assert!(Architecture::Arm64.is_64_bit());
69    /// assert!(!Architecture::X86.is_64_bit());
70    /// assert!(!Architecture::Arm.is_64_bit());
71    /// assert!(!Architecture::Neutral.is_64_bit());
72    /// ```
73    #[must_use]
74    #[inline]
75    pub const fn is_64_bit(self) -> bool {
76        matches!(self, Self::X64 | Self::Arm64)
77    }
78
79    /// Returns `true` if the architecture is a 32-bit architecture.
80    ///
81    /// # Examples
82    /// ```
83    /// use winget_types::installer::Architecture;
84    ///
85    /// assert!(Architecture::X86.is_32_bit());
86    /// assert!(Architecture::Arm.is_32_bit());
87    /// assert!(!Architecture::X64.is_32_bit());
88    /// assert!(!Architecture::Arm64.is_32_bit());
89    /// assert!(!Architecture::Neutral.is_32_bit());
90    /// ```
91    #[must_use]
92    #[inline]
93    pub const fn is_32_bit(self) -> bool {
94        matches!(self, Self::X86 | Self::Arm)
95    }
96
97    #[must_use]
98    pub fn from_url(url: &str) -> Option<Self> {
99        fn is_delimited_at(url_bytes: &[u8], start: usize, len: usize) -> bool {
100            url_bytes
101                .get(start - 1)
102                .is_some_and(|delimiter| DELIMITERS.contains(delimiter))
103                && url_bytes
104                    .get(start + len)
105                    .is_some_and(|delimiter| DELIMITERS.contains(delimiter))
106        }
107
108        // Ignore the casing of the URL
109        let url = url.to_ascii_lowercase();
110
111        let url_bytes = url.as_bytes();
112
113        // Check for {delimiter}{architecture}{delimiter}
114        if let Some(arch) = ARCHITECTURES
115            .into_iter()
116            // For each architecture name/type pair, try to find delimited matches in the URL
117            .filter_map(|(name, arch)| {
118                // Find all occurrences of this architecture name in the URL (from right to left)
119                url.rmatch_indices(name)
120                    // Find the first (rightmost) occurrence that is properly delimited
121                    .find(|&(index, _)| is_delimited_at(url_bytes, index, name.len()))
122                    // If found, return a tuple of (name, arch_type, index) for comparison
123                    .map(|(index, _)| (name, arch, index))
124            })
125            // Select the best match based on position and specificity
126            .max_by_key(|(name, _, index)| {
127                (
128                    *index,     // Primary: prefer matches found later in the URL
129                    name.len(), // Secondary: prefer longer names (e.g., x86_64 over x86)
130                )
131            })
132            // Extract just the architecture type from the winning match
133            .map(|(_, arch, _)| arch)
134        {
135            return Some(arch);
136        }
137
138        // If the architecture has not been found, check for {architecture}.{extension}
139        for extension in VALID_FILE_EXTENSIONS {
140            for (arch_name, arch) in ARCHITECTURES {
141                if url
142                    .rfind(extension)
143                    .map(|index| index - 1)
144                    .filter(|&index| url_bytes.get(index) == Some(&b'.'))
145                    .is_some_and(|end| url.get(end - arch_name.len()..end) == Some(arch_name))
146                {
147                    return Some(arch);
148                }
149            }
150        }
151
152        None
153    }
154
155    #[must_use]
156    pub const fn as_str(&self) -> &'static str {
157        match self {
158            Self::X86 => "x86",
159            Self::X64 => "x64",
160            Self::Arm => "arm",
161            Self::Arm64 => "arm64",
162            Self::Neutral => "neutral",
163        }
164    }
165}
166
167impl AsRef<str> for Architecture {
168    #[inline]
169    fn as_ref(&self) -> &str {
170        self.as_str()
171    }
172}
173
174impl fmt::Display for Architecture {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        self.as_str().fmt(f)
177    }
178}
179
180impl FromStr for Architecture {
181    type Err = ParseArchitectureError;
182
183    fn from_str(s: &str) -> Result<Self, Self::Err> {
184        match s {
185            "x86" => Ok(Self::X86),
186            "x64" => Ok(Self::X64),
187            "arm" => Ok(Self::Arm),
188            "arm64" => Ok(Self::Arm64),
189            "neutral" => Ok(Self::Neutral),
190            _ => Err(ParseArchitectureError),
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use alloc::format;
198
199    use rstest::rstest;
200
201    use super::Architecture;
202
203    #[rstest]
204    fn x64_architectures_at_end(
205        #[values(
206            "x86-64", "x86_64", "x64", "64-bit", "64bit", "Win64", "Winx64", "ia64", "amd64"
207        )]
208        architecture: &str,
209    ) {
210        assert_eq!(
211            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
212            Some(Architecture::X64)
213        );
214    }
215
216    #[rstest]
217    fn x64_architectures_delimited(
218        #[values(
219            "x86-64", "x86_64", "x64", "64-bit", "64bit", "Win64", "Winx64", "ia64", "amd64"
220        )]
221        architecture: &str,
222        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
223    ) {
224        assert_eq!(
225            Architecture::from_url(&format!(
226                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
227            )),
228            Some(Architecture::X64)
229        );
230    }
231
232    #[rstest]
233    fn x86_architectures_at_end(
234        #[values(
235            "x86", "x32", "32-bit", "32bit", "win32", "winx86", "ia32", "i386", "i486", "i586",
236            "i686", "386", "486", "586", "686"
237        )]
238        architecture: &str,
239    ) {
240        assert_eq!(
241            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
242            Some(Architecture::X86)
243        );
244    }
245
246    #[rstest]
247    fn x86_architectures_delimited(
248        #[values(
249            "x86", "x32", "32-bit", "32bit", "win32", "winx86", "ia32", "i386", "i486", "i586",
250            "i686", "386", "486", "586", "686"
251        )]
252        architecture: &str,
253        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
254    ) {
255        assert_eq!(
256            Architecture::from_url(&format!(
257                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
258            )),
259            Some(Architecture::X86)
260        );
261    }
262
263    #[rstest]
264    fn arm64_architectures_at_end(
265        #[values("arm64ec", "arm64", "aarch64", "win64a")] architecture: &str,
266    ) {
267        assert_eq!(
268            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
269            Some(Architecture::Arm64)
270        );
271    }
272
273    #[rstest]
274    fn arm64_architectures_delimited(
275        #[values("arm64ec", "arm64", "aarch64", "win64a")] architecture: &str,
276        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
277    ) {
278        assert_eq!(
279            Architecture::from_url(&format!(
280                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
281            )),
282            Some(Architecture::Arm64)
283        );
284    }
285
286    #[rstest]
287    fn arm_architectures_at_end(#[values("arm", "armv7", "aarch")] architecture: &str) {
288        assert_eq!(
289            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
290            Some(Architecture::Arm)
291        );
292    }
293
294    #[rstest]
295    fn arm_architectures_delimited(
296        #[values("arm", "armv7", "aarch")] architecture: &str,
297        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
298    ) {
299        assert_eq!(
300            Architecture::from_url(&format!(
301                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
302            )),
303            Some(Architecture::Arm)
304        );
305    }
306
307    #[test]
308    fn no_architecture() {
309        assert_eq!(
310            Architecture::from_url("https://www.example.com/file.exe"),
311            None
312        );
313    }
314
315    #[test]
316    fn win32_and_arm64_in_url() {
317        assert_eq!(
318            Architecture::from_url(
319                "https://github.com/vim/vim-win32-installer/releases/download/v9.1.1234/gvim_9.1.1234_arm64.exe"
320            ),
321            Some(Architecture::Arm64)
322        );
323    }
324}