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 let url = url.to_ascii_lowercase();
101
102 let url_bytes = url.as_bytes();
103
104 for (arch_name, arch) in ARCHITECTURES {
106 if let Some(arch_index) = url.rfind(arch_name) {
107 if let (Some(char_before_arch), Some(char_after_arch)) = (
109 url_bytes.get(arch_index - 1),
110 url_bytes.get(arch_index + arch_name.len()),
111 ) {
112 if DELIMITERS.contains(char_before_arch) && DELIMITERS.contains(char_after_arch)
114 {
115 return Some(arch);
116 }
117 }
118 }
119 }
120
121 for extension in VALID_FILE_EXTENSIONS {
123 for (arch_name, arch) in ARCHITECTURES {
124 if url
125 .rfind(extension)
126 .map(|index| index - 1)
127 .filter(|&index| url_bytes.get(index) == Some(&b'.'))
128 .is_some_and(|end| url.get(end - arch_name.len()..end) == Some(arch_name))
129 {
130 return Some(arch);
131 }
132 }
133 }
134
135 None
136 }
137
138 #[must_use]
139 pub const fn as_str(&self) -> &'static str {
140 match self {
141 Self::X86 => "x86",
142 Self::X64 => "x64",
143 Self::Arm => "arm",
144 Self::Arm64 => "arm64",
145 Self::Neutral => "neutral",
146 }
147 }
148}
149
150impl AsRef<str> for Architecture {
151 #[inline]
152 fn as_ref(&self) -> &str {
153 self.as_str()
154 }
155}
156
157impl fmt::Display for Architecture {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 self.as_str().fmt(f)
160 }
161}
162
163impl FromStr for Architecture {
164 type Err = ParseArchitectureError;
165
166 fn from_str(s: &str) -> Result<Self, Self::Err> {
167 match s {
168 "x86" => Ok(Self::X86),
169 "x64" => Ok(Self::X64),
170 "arm" => Ok(Self::Arm),
171 "arm64" => Ok(Self::Arm64),
172 "neutral" => Ok(Self::Neutral),
173 _ => Err(ParseArchitectureError),
174 }
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use alloc::format;
181
182 use rstest::rstest;
183
184 use super::Architecture;
185
186 #[rstest]
187 fn x64_architectures_at_end(
188 #[values(
189 "x86-64", "x86_64", "x64", "64-bit", "64bit", "Win64", "Winx64", "ia64", "amd64"
190 )]
191 architecture: &str,
192 ) {
193 assert_eq!(
194 Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
195 Some(Architecture::X64)
196 );
197 }
198
199 #[rstest]
200 fn x64_architectures_delimited(
201 #[values(
202 "x86-64", "x86_64", "x64", "64-bit", "64bit", "Win64", "Winx64", "ia64", "amd64"
203 )]
204 architecture: &str,
205 #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
206 ) {
207 assert_eq!(
208 Architecture::from_url(&format!(
209 "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
210 )),
211 Some(Architecture::X64)
212 );
213 }
214
215 #[rstest]
216 fn x86_architectures_at_end(
217 #[values(
218 "x86", "x32", "32-bit", "32bit", "win32", "winx86", "ia32", "i386", "i486", "i586",
219 "i686", "386", "486", "586", "686"
220 )]
221 architecture: &str,
222 ) {
223 assert_eq!(
224 Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
225 Some(Architecture::X86)
226 );
227 }
228
229 #[rstest]
230 fn x86_architectures_delimited(
231 #[values(
232 "x86", "x32", "32-bit", "32bit", "win32", "winx86", "ia32", "i386", "i486", "i586",
233 "i686", "386", "486", "586", "686"
234 )]
235 architecture: &str,
236 #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
237 ) {
238 assert_eq!(
239 Architecture::from_url(&format!(
240 "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
241 )),
242 Some(Architecture::X86)
243 );
244 }
245
246 #[rstest]
247 fn arm64_architectures_at_end(
248 #[values("arm64ec", "arm64", "aarch64", "win64a")] architecture: &str,
249 ) {
250 assert_eq!(
251 Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
252 Some(Architecture::Arm64)
253 );
254 }
255
256 #[rstest]
257 fn arm64_architectures_delimited(
258 #[values("arm64ec", "arm64", "aarch64", "win64a")] architecture: &str,
259 #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
260 ) {
261 assert_eq!(
262 Architecture::from_url(&format!(
263 "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
264 )),
265 Some(Architecture::Arm64)
266 );
267 }
268
269 #[rstest]
270 fn arm_architectures_at_end(#[values("arm", "armv7", "aarch")] architecture: &str) {
271 assert_eq!(
272 Architecture::from_url(&format!("https://www.example.com/file{architecture}.exe")),
273 Some(Architecture::Arm)
274 );
275 }
276
277 #[rstest]
278 fn arm_architectures_delimited(
279 #[values("arm", "armv7", "aarch")] architecture: &str,
280 #[values(',', '/', '\\', '.', '_', '-', '(', ')')] delimiter: char,
281 ) {
282 assert_eq!(
283 Architecture::from_url(&format!(
284 "https://www.example.com/file{delimiter}{architecture}{delimiter}app.exe"
285 )),
286 Some(Architecture::Arm)
287 );
288 }
289
290 #[test]
291 fn no_architecture() {
292 assert_eq!(
293 Architecture::from_url("https://www.example.com/file.exe"),
294 None
295 );
296 }
297}