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    #[must_use]
62    pub fn from_url(url: &str) -> Option<Self> {
63        fn is_delimited_at(url_bytes: &[u8], start: usize, len: usize) -> bool {
64            url_bytes
65                .get(start - 1)
66                .is_some_and(|delimiter| DELIMITERS.contains(delimiter))
67                && url_bytes
68                    .get(start + len)
69                    .is_some_and(|delimiter| DELIMITERS.contains(delimiter))
70        }
71
72        // Ignore the casing of the URL
73        let url = url.to_ascii_lowercase();
74
75        let url_bytes = url.as_bytes();
76
77        // Check for {delimiter}{architecture}{delimiter}
78        if let Some(arch) = ARCHITECTURES
79            .into_iter()
80            // For each architecture name/type pair, try to find delimited matches in the URL
81            .filter_map(|(name, arch)| {
82                // Find all occurrences of this architecture name in the URL (from right to left)
83                url.rmatch_indices(name)
84                    // Find the first (rightmost) occurrence that is properly delimited
85                    .find(|&(index, _)| is_delimited_at(url_bytes, index, name.len()))
86                    // If found, return a tuple of (name, arch_type, index) for comparison
87                    .map(|(index, _)| (name, arch, index))
88            })
89            // Select the best match based on position and specificity
90            .max_by_key(|(name, _, index)| {
91                (
92                    *index,     // Primary: prefer matches found later in the URL
93                    name.len(), // Secondary: prefer longer names (e.g., x86_64 over x86)
94                )
95            })
96            // Extract just the architecture type from the winning match
97            .map(|(_, arch, _)| arch)
98        {
99            return Some(arch);
100        }
101
102        // If the architecture has not been found, check for {architecture}.{extension}
103        for extension in VALID_FILE_EXTENSIONS {
104            for (arch_name, arch) in ARCHITECTURES {
105                if url
106                    .rfind(extension)
107                    .map(|index| index - 1)
108                    .filter(|&index| url_bytes.get(index) == Some(&b'.'))
109                    .is_some_and(|end| url.get(end - arch_name.len()..end) == Some(arch_name))
110                {
111                    return Some(arch);
112                }
113            }
114        }
115
116        None
117    }
118
119    /// Returns `true` if the architecture is x86.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use winget_types::installer::Architecture;
125    ///
126    /// assert!(Architecture::X86.is_x86());
127    /// ```
128    #[must_use]
129    #[inline]
130    pub const fn is_x86(self) -> bool {
131        matches!(self, Self::X86)
132    }
133
134    /// Returns `true` if the architecture is x64.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use winget_types::installer::Architecture;
140    ///
141    /// assert!(Architecture::X64.is_x64());
142    /// ```
143    #[must_use]
144    #[inline]
145    pub const fn is_x64(self) -> bool {
146        matches!(self, Self::X64)
147    }
148
149    /// Returns `true` if the architecture is ARM.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use winget_types::installer::Architecture;
155    ///
156    /// assert!(Architecture::Arm.is_arm());
157    /// ```
158    #[must_use]
159    #[inline]
160    pub const fn is_arm(self) -> bool {
161        matches!(self, Self::Arm)
162    }
163
164    /// Returns `true` if the architecture is ARM64.
165    ///
166    /// # Examples
167    ///
168    /// ```
169    /// use winget_types::installer::Architecture;
170    ///
171    /// assert!(Architecture::Arm64.is_arm64());
172    /// ```
173    #[must_use]
174    #[inline]
175    pub const fn is_arm64(self) -> bool {
176        matches!(self, Self::Arm64)
177    }
178
179    /// Returns `true` if the architecture is neutral.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use winget_types::installer::Architecture;
185    ///
186    /// assert!(Architecture::Neutral.is_neutral());
187    /// ```
188    #[must_use]
189    #[inline]
190    pub const fn is_neutral(self) -> bool {
191        matches!(self, Self::Neutral)
192    }
193
194    /// Returns `true` if the architecture is a 64-bit architecture.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use winget_types::installer::Architecture;
200    ///
201    /// assert!(Architecture::X64.is_64_bit());
202    /// assert!(Architecture::Arm64.is_64_bit());
203    /// assert!(!Architecture::X86.is_64_bit());
204    /// assert!(!Architecture::Arm.is_64_bit());
205    /// assert!(!Architecture::Neutral.is_64_bit());
206    /// ```
207    #[must_use]
208    #[inline]
209    pub const fn is_64_bit(self) -> bool {
210        matches!(self, Self::X64 | Self::Arm64)
211    }
212
213    /// Returns `true` if the architecture is a 32-bit architecture.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// use winget_types::installer::Architecture;
219    ///
220    /// assert!(Architecture::X86.is_32_bit());
221    /// assert!(Architecture::Arm.is_32_bit());
222    /// assert!(!Architecture::X64.is_32_bit());
223    /// assert!(!Architecture::Arm64.is_32_bit());
224    /// assert!(!Architecture::Neutral.is_32_bit());
225    /// ```
226    #[must_use]
227    #[inline]
228    pub const fn is_32_bit(self) -> bool {
229        matches!(self, Self::X86 | Self::Arm)
230    }
231
232    #[must_use]
233    pub const fn as_str(&self) -> &'static str {
234        match self {
235            Self::X86 => "x86",
236            Self::X64 => "x64",
237            Self::Arm => "arm",
238            Self::Arm64 => "arm64",
239            Self::Neutral => "neutral",
240        }
241    }
242}
243
244impl AsRef<str> for Architecture {
245    #[inline]
246    fn as_ref(&self) -> &str {
247        self.as_str()
248    }
249}
250
251impl fmt::Display for Architecture {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        self.as_str().fmt(f)
254    }
255}
256
257impl FromStr for Architecture {
258    type Err = ParseArchitectureError;
259
260    fn from_str(s: &str) -> Result<Self, Self::Err> {
261        match s {
262            "x86" => Ok(Self::X86),
263            "x64" => Ok(Self::X64),
264            "arm" => Ok(Self::Arm),
265            "arm64" => Ok(Self::Arm64),
266            "neutral" => Ok(Self::Neutral),
267            _ => Err(ParseArchitectureError),
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use alloc::format;
275
276    use rstest::rstest;
277
278    use super::Architecture;
279
280    #[rstest]
281    fn x64_architectures_at_end(
282        #[values(
283            "x86-64", "x86_64", "x64", "64-bit", "64bit", "Win64", "Winx64", "ia64", "amd64"
284        )]
285        architecture: &str,
286    ) {
287        assert_eq!(
288            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
289            Some(Architecture::X64)
290        );
291    }
292
293    #[rstest]
294    fn x64_architectures_delimited(
295        #[values(
296            "x86-64", "x86_64", "x64", "64-bit", "64bit", "Win64", "Winx64", "ia64", "amd64"
297        )]
298        architecture: &str,
299        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
300    ) {
301        assert_eq!(
302            Architecture::from_url(&format!(
303                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
304            )),
305            Some(Architecture::X64)
306        );
307    }
308
309    #[rstest]
310    fn x86_architectures_at_end(
311        #[values(
312            "x86", "x32", "32-bit", "32bit", "win32", "winx86", "ia32", "i386", "i486", "i586",
313            "i686", "386", "486", "586", "686"
314        )]
315        architecture: &str,
316    ) {
317        assert_eq!(
318            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
319            Some(Architecture::X86)
320        );
321    }
322
323    #[rstest]
324    fn x86_architectures_delimited(
325        #[values(
326            "x86", "x32", "32-bit", "32bit", "win32", "winx86", "ia32", "i386", "i486", "i586",
327            "i686", "386", "486", "586", "686"
328        )]
329        architecture: &str,
330        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
331    ) {
332        assert_eq!(
333            Architecture::from_url(&format!(
334                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
335            )),
336            Some(Architecture::X86)
337        );
338    }
339
340    #[rstest]
341    fn arm64_architectures_at_end(
342        #[values("arm64ec", "arm64", "aarch64", "win64a")] architecture: &str,
343    ) {
344        assert_eq!(
345            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
346            Some(Architecture::Arm64)
347        );
348    }
349
350    #[rstest]
351    fn arm64_architectures_delimited(
352        #[values("arm64ec", "arm64", "aarch64", "win64a")] architecture: &str,
353        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
354    ) {
355        assert_eq!(
356            Architecture::from_url(&format!(
357                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
358            )),
359            Some(Architecture::Arm64)
360        );
361    }
362
363    #[rstest]
364    fn arm_architectures_at_end(#[values("arm", "armv7", "aarch")] architecture: &str) {
365        assert_eq!(
366            Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
367            Some(Architecture::Arm)
368        );
369    }
370
371    #[rstest]
372    fn arm_architectures_delimited(
373        #[values("arm", "armv7", "aarch")] architecture: &str,
374        #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
375    ) {
376        assert_eq!(
377            Architecture::from_url(&format!(
378                "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
379            )),
380            Some(Architecture::Arm)
381        );
382    }
383
384    #[test]
385    fn no_architecture() {
386        assert_eq!(
387            Architecture::from_url("https://www.example.com/file.exe"),
388            None
389        );
390    }
391
392    #[test]
393    fn win32_and_arm64_in_url() {
394        assert_eq!(
395            Architecture::from_url(
396                "https://github.com/vim/vim-win32-installer/releases/download/v9.1.1234/gvim_9.1.1234_arm64.exe"
397            ),
398            Some(Architecture::Arm64)
399        );
400    }
401}