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]
74 #[inline]
75 pub const fn is_64_bit(self) -> bool {
76 matches!(self, Self::X64 | Self::Arm64)
77 }
78
79 #[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 let url = url.to_ascii_lowercase();
110
111 let url_bytes = url.as_bytes();
112
113 if let Some(arch) = ARCHITECTURES
115 .into_iter()
116 .filter_map(|(name, arch)| {
118 url.rmatch_indices(name)
120 .find(|&(index, _)| is_delimited_at(url_bytes, index, name.len()))
122 .map(|(index, _)| (name, arch, index))
124 })
125 .max_by_key(|(name, _, index)| {
127 (
128 *index, name.len(), )
131 })
132 .map(|(_, arch, _)| arch)
134 {
135 return Some(arch);
136 }
137
138 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}