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 let url = url.to_ascii_lowercase();
94
95 let url_bytes = url.as_bytes();
96
97 for (arch_name, arch) in ARCHITECTURES {
99 if let Some(arch_index) = url.rfind(arch_name) {
100 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 DELIMITERS.contains(char_before_arch) && DELIMITERS.contains(char_after_arch)
107 {
108 return Some(arch);
109 }
110 }
111 }
112 }
113
114 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}