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 let url = url.to_ascii_lowercase();
74
75 let url_bytes = url.as_bytes();
76
77 if let Some(arch) = ARCHITECTURES
79 .into_iter()
80 .filter_map(|(name, arch)| {
82 url.rmatch_indices(name)
84 .find(|&(index, _)| is_delimited_at(url_bytes, index, name.len()))
86 .map(|(index, _)| (name, arch, index))
88 })
89 .max_by_key(|(name, _, index)| {
91 (
92 *index, name.len(), )
95 })
96 .map(|(_, arch, _)| arch)
98 {
99 return Some(arch);
100 }
101
102 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 #[must_use]
129 #[inline]
130 pub const fn is_x86(self) -> bool {
131 matches!(self, Self::X86)
132 }
133
134 #[must_use]
144 #[inline]
145 pub const fn is_x64(self) -> bool {
146 matches!(self, Self::X64)
147 }
148
149 #[must_use]
159 #[inline]
160 pub const fn is_arm(self) -> bool {
161 matches!(self, Self::Arm)
162 }
163
164 #[must_use]
174 #[inline]
175 pub const fn is_arm64(self) -> bool {
176 matches!(self, Self::Arm64)
177 }
178
179 #[must_use]
189 #[inline]
190 pub const fn is_neutral(self) -> bool {
191 matches!(self, Self::Neutral)
192 }
193
194 #[must_use]
208 #[inline]
209 pub const fn is_64_bit(self) -> bool {
210 matches!(self, Self::X64 | Self::Arm64)
211 }
212
213 #[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}