1use std::{
4 error, fmt,
5 io::{self, BufRead as _},
6 str::{self, FromStr},
7};
8
9use semver::Version;
10
11const UNKNOWN: &str = "Unknown";
12
13const OS_VERSION_DEFAULT: &(u64, u64, u64) = &(0, 0, 0);
14const ALAMOFIRE_VERSION_DEFAULT: &(u64, u64, u64) = &(5, 6, 4);
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct DefaultUserAgent {
19 pub executable: Option<String>,
20 pub app_version: Option<Version>,
21 pub bundle: Option<String>,
22 pub app_build: Option<DefaultUserAgentAppBuild>,
23 pub os_name: Option<DefaultUserAgentOsName>,
24 pub os_version: Version,
25 pub alamofire_version: Version,
26}
27impl Default for DefaultUserAgent {
28 fn default() -> Self {
29 Self {
30 executable: None,
31 app_version: None,
32 bundle: None,
33 app_build: None,
34 os_name: None,
35 os_version: Version {
36 major: OS_VERSION_DEFAULT.0,
37 minor: OS_VERSION_DEFAULT.1,
38 patch: OS_VERSION_DEFAULT.2,
39 pre: Default::default(),
40 build: Default::default(),
41 },
42 alamofire_version: Version {
43 major: ALAMOFIRE_VERSION_DEFAULT.0,
44 minor: ALAMOFIRE_VERSION_DEFAULT.1,
45 patch: ALAMOFIRE_VERSION_DEFAULT.2,
46 pre: Default::default(),
47 build: Default::default(),
48 },
49 }
50 }
51}
52
53impl DefaultUserAgent {
54 pub fn parse(bytes: impl AsRef<[u8]>) -> Result<Self, DefaultUserAgentParseError> {
55 let bytes = bytes.as_ref();
56
57 let mut cursor = io::Cursor::new(bytes);
58 let mut buf = vec![];
59
60 let n = cursor
64 .read_until(b'/', &mut buf)
65 .map_err(|err| DefaultUserAgentParseError::ExecutableReadFailed(err.to_string()))?;
66 if !buf.ends_with(&[b'/']) {
67 return Err(DefaultUserAgentParseError::ExecutableReadFailed(
68 "Invalid".to_owned(),
69 ));
70 }
71 let mut executable =
72 Some(String::from_utf8(buf[..n - 1].to_vec()).map_err(|err| {
73 DefaultUserAgentParseError::ExecutableParseFailed(err.to_string())
74 })?);
75 if executable.as_deref() == Some(UNKNOWN) {
76 executable = None;
77 }
78 buf.clear();
79
80 let n = cursor
84 .read_until(b' ', &mut buf)
85 .map_err(|err| DefaultUserAgentParseError::AppVersionReadFailed(err.to_string()))?;
86 if !buf.ends_with(&[b' ']) {
87 return Err(DefaultUserAgentParseError::AppVersionReadFailed(
88 "Invalid".to_owned(),
89 ));
90 }
91 let mut app_version =
92 Some(String::from_utf8(buf[..n - 1].to_vec()).map_err(|err| {
93 DefaultUserAgentParseError::AppVersionParseFailed(err.to_string())
94 })?);
95 if app_version.as_deref() == Some(UNKNOWN) {
96 app_version = None;
97 }
98
99 let app_version = if let Some(app_version) = app_version {
100 Some(Version::parse(&app_version).map_err(|err| {
101 DefaultUserAgentParseError::AppVersionParseFailed(err.to_string())
102 })?)
103 } else {
104 None
105 };
106
107 buf.clear();
108
109 if !cursor.get_ref()[cursor.position() as usize..].starts_with(&[b'(']) {
113 return Err(DefaultUserAgentParseError::Other("Mismatch AppVersion end"));
114 }
115 cursor.set_position(cursor.position() + 1);
116
117 let n = cursor
121 .read_until(b';', &mut buf)
122 .map_err(|err| DefaultUserAgentParseError::BundleReadFailed(err.to_string()))?;
123 if !buf.ends_with(&[b';']) {
124 return Err(DefaultUserAgentParseError::BundleReadFailed(
125 "Invalid".to_owned(),
126 ));
127 }
128 let mut bundle = Some(
129 String::from_utf8(buf[..n - 1].to_vec())
130 .map_err(|err| DefaultUserAgentParseError::BundleParseFailed(err.to_string()))?,
131 );
132 if bundle.as_deref() == Some(UNKNOWN) {
133 bundle = None;
134 }
135
136 buf.clear();
137
138 if !cursor.get_ref()[cursor.position() as usize..].starts_with(b" build:") {
142 return Err(DefaultUserAgentParseError::Other("Mismatch Bundle end"));
143 }
144 cursor.set_position(cursor.position() + 7);
145
146 let n = cursor
150 .read_until(b';', &mut buf)
151 .map_err(|err| DefaultUserAgentParseError::AppBuildReadFailed(err.to_string()))?;
152 if !buf.ends_with(&[b';']) {
153 return Err(DefaultUserAgentParseError::AppBuildReadFailed(
154 "Invalid".to_owned(),
155 ));
156 }
157 let app_build = String::from_utf8(buf[..n - 1].to_vec())
158 .map_err(|err| DefaultUserAgentParseError::AppBuildParseFailed(err.to_string()))?;
159
160 let app_build = DefaultUserAgentAppBuild::parse(app_build)
161 .map_err(DefaultUserAgentParseError::AppBuildParseFailed)?;
162
163 buf.clear();
164
165 if !cursor.get_ref()[cursor.position() as usize..].starts_with(&[b' ']) {
169 return Err(DefaultUserAgentParseError::Other("Mismatch AppBuild end"));
170 }
171 cursor.set_position(cursor.position() + 1);
172
173 let n = cursor
177 .read_until(b' ', &mut buf)
178 .map_err(|err| DefaultUserAgentParseError::OsNameReadFailed(err.to_string()))?;
179 if !buf.ends_with(&[b' ']) {
180 return Err(DefaultUserAgentParseError::OsNameReadFailed(
181 "Invalid".to_owned(),
182 ));
183 }
184 let os_name = String::from_utf8(buf[..n - 1].to_vec())
185 .map_err(|err| DefaultUserAgentParseError::OsNameParseFailed(err.to_string()))?;
186
187 let os_name = DefaultUserAgentOsName::parse(os_name)
188 .map_err(|err| DefaultUserAgentParseError::OsNameParseFailed(err.to_string()))?;
189
190 buf.clear();
191
192 let n = cursor
196 .read_until(b')', &mut buf)
197 .map_err(|err| DefaultUserAgentParseError::OsVersionReadFailed(err.to_string()))?;
198 if !buf.ends_with(&[b')']) {
199 return Err(DefaultUserAgentParseError::OsVersionReadFailed(
200 "Invalid".to_owned(),
201 ));
202 }
203 let os_version = String::from_utf8(buf[..n - 1].to_vec())
204 .map_err(|err| DefaultUserAgentParseError::OsVersionParseFailed(err.to_string()))?;
205
206 let os_version = Version::parse(&os_version)
207 .map_err(|err| DefaultUserAgentParseError::OsVersionParseFailed(err.to_string()))?;
208
209 buf.clear();
210
211 if !cursor.get_ref()[cursor.position() as usize..].starts_with(b" Alamofire/") {
215 return Err(DefaultUserAgentParseError::Other("Mismatch OsVersion end"));
216 }
217 cursor.set_position(cursor.position() + 11);
218
219 let n = cursor.read_until(b'\n', &mut buf).map_err(|err| {
223 DefaultUserAgentParseError::AlamofireVersionReadFailed(err.to_string())
224 })?;
225 if !cursor.get_ref()[cursor.position() as usize..].is_empty() {
226 return Err(DefaultUserAgentParseError::Other(
227 "Mismatch AlamofireVersion end",
228 ));
229 }
230
231 let alamofire_version =
232 String::from_utf8(buf[..if buf.ends_with(&[b'\n']) { n - 1 } else { n }].to_vec())
233 .map_err(|err| {
234 DefaultUserAgentParseError::AlamofireVersionParseFailed(err.to_string())
235 })?;
236
237 let alamofire_version = Version::parse(&alamofire_version).map_err(|err| {
238 DefaultUserAgentParseError::AlamofireVersionParseFailed(err.to_string())
239 })?;
240
241 Ok(DefaultUserAgent {
245 executable,
246 app_version,
247 bundle,
248 app_build,
249 os_name,
250 os_version,
251 alamofire_version,
252 })
253 }
254}
255
256#[derive(Debug, Clone)]
258pub enum DefaultUserAgentParseError {
259 ExecutableReadFailed(String),
261 ExecutableParseFailed(String),
262 AppVersionReadFailed(String),
264 AppVersionParseFailed(String),
265 BundleReadFailed(String),
267 BundleParseFailed(String),
268 AppBuildReadFailed(String),
270 AppBuildParseFailed(String),
271 OsNameReadFailed(String),
273 OsNameParseFailed(String),
274 OsVersionReadFailed(String),
276 OsVersionParseFailed(String),
277 AlamofireVersionReadFailed(String),
279 AlamofireVersionParseFailed(String),
280 Other(&'static str),
282}
283impl fmt::Display for DefaultUserAgentParseError {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 write!(f, "{self:?}")
286 }
287}
288impl error::Error for DefaultUserAgentParseError {}
289
290impl FromStr for DefaultUserAgent {
292 type Err = DefaultUserAgentParseError;
293
294 fn from_str(s: &str) -> Result<Self, Self::Err> {
295 Self::parse(s.to_string().as_str())
296 }
297}
298
299impl fmt::Display for DefaultUserAgent {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 write!(
302 f,
303 "{}/{} ({}; build:{}; {} {}) Alamofire/{}",
304 self.executable.as_ref().unwrap_or(&UNKNOWN.to_owned()),
305 self.app_version
306 .as_ref()
307 .map(|x| x.to_string())
308 .unwrap_or_else(|| UNKNOWN.to_owned()),
309 self.bundle.as_ref().unwrap_or(&UNKNOWN.to_owned()),
310 self.app_build
311 .as_ref()
312 .map(|x| x.to_string())
313 .unwrap_or_else(|| UNKNOWN.to_owned()),
314 self.os_name
315 .as_ref()
316 .map(|x| x.to_string())
317 .unwrap_or_else(|| UNKNOWN.to_owned()),
318 self.os_version,
319 self.alamofire_version,
320 )
321 }
322}
323
324#[derive(Debug, Clone, PartialEq, Eq)]
328pub struct DefaultUserAgentAppBuild(pub Version);
329impl DefaultUserAgentAppBuild {
330 pub fn parse(s: impl AsRef<str>) -> Result<Option<Self>, String> {
331 OptionDefaultUserAgentAppBuild::from_str(s.as_ref()).map(|x| x.0)
332 }
333}
334
335struct OptionDefaultUserAgentAppBuild(Option<DefaultUserAgentAppBuild>);
336impl FromStr for OptionDefaultUserAgentAppBuild {
337 type Err = String;
338
339 fn from_str(s: &str) -> Result<Self, Self::Err> {
340 match s {
341 UNKNOWN => Ok(Self(None)),
342 _ => {
343 let version = match s.split('.').count() {
344 1 => Version::parse(format!("{s}.0.0").as_str())
345 .map_err(|err| err.to_string())?,
346 2 => {
347 Version::parse(format!("{s}.0").as_str()).map_err(|err| err.to_string())?
348 }
349 3 => Version::parse(s).map_err(|err| err.to_string())?,
350 _ => {
351 return Err("Invalid".to_owned());
352 }
353 };
354
355 Ok(Self(Some(DefaultUserAgentAppBuild(version))))
356 }
357 }
358 }
359}
360
361impl fmt::Display for DefaultUserAgentAppBuild {
362 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 if self.0.patch == 0 && self.0.minor == 0 {
364 write!(f, "{}", self.0.major)
365 } else if self.0.minor == 0 {
366 write!(f, "{}.{}", self.0.major, self.0.minor)
367 } else {
368 write!(f, "{}.{}.{}", self.0.major, self.0.minor, self.0.patch)
369 }
370 }
371}
372
373#[allow(non_camel_case_types)]
377#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
378pub enum DefaultUserAgentOsName {
379 macOSCatalyst,
380 iOS,
381 watchOS,
382 tvOS,
383 macOS,
384 Linux,
385 Windows,
386}
387impl DefaultUserAgentOsName {
388 pub fn parse(s: impl AsRef<str>) -> Result<Option<Self>, &'static str> {
389 OptionDefaultUserAgentOsName::from_str(s.as_ref()).map(|x| x.0)
390 }
391}
392
393struct OptionDefaultUserAgentOsName(Option<DefaultUserAgentOsName>);
394impl FromStr for OptionDefaultUserAgentOsName {
395 type Err = &'static str;
396
397 fn from_str(s: &str) -> Result<Self, Self::Err> {
398 match s {
399 "macOS(Catalyst)" => Ok(Self(Some(DefaultUserAgentOsName::macOSCatalyst))),
400 "iOS" => Ok(Self(Some(DefaultUserAgentOsName::iOS))),
401 "watchOS" => Ok(Self(Some(DefaultUserAgentOsName::watchOS))),
402 "tvOS" => Ok(Self(Some(DefaultUserAgentOsName::tvOS))),
403 "macOS" => Ok(Self(Some(DefaultUserAgentOsName::macOS))),
404 "Linux" => Ok(Self(Some(DefaultUserAgentOsName::Linux))),
405 "Windows" => Ok(Self(Some(DefaultUserAgentOsName::Windows))),
406 UNKNOWN => Ok(Self(None)),
407 _ => Err("Mismatch"),
408 }
409 }
410}
411
412impl fmt::Display for DefaultUserAgentOsName {
413 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
414 match self {
415 Self::macOSCatalyst => write!(f, "macOS(Catalyst)"),
416 Self::iOS => write!(f, "iOS"),
417 Self::watchOS => write!(f, "watchOS"),
418 Self::tvOS => write!(f, "tvOS"),
419 Self::macOS => write!(f, "macOS"),
420 Self::Linux => write!(f, "Linux"),
421 Self::Windows => write!(f, "Windows"),
422 }
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn test_default() {
432 assert_eq!(
433 DefaultUserAgent::default(),
434 DefaultUserAgent {
435 executable: None,
436 app_version: None,
437 bundle: None,
438 app_build: None,
439 os_name: None,
440 os_version: "0.0.0".parse().unwrap(),
441 alamofire_version: "5.6.4".parse().unwrap()
442 }
443 );
444 }
445
446 #[test]
447 fn test_parse() {
448 assert_eq!(
449 DefaultUserAgent::parse(
450 "iOS Example/1.0.0 (org.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0"
451 )
452 .unwrap(),
453 DefaultUserAgent {
454 executable: Some("iOS Example".to_owned()),
455 app_version: Some("1.0.0".parse().unwrap()),
456 bundle: Some("org.alamofire.iOS-Example".to_owned()),
457 app_build: Some(DefaultUserAgentAppBuild("1.0.0".parse().unwrap())),
458 os_name: Some(DefaultUserAgentOsName::iOS),
459 os_version: "13.0.0".parse().unwrap(),
460 alamofire_version: "5.0.0".parse().unwrap()
461 }
462 );
463
464 assert_eq!(
465 DefaultUserAgent::parse(
466 "Unknown/Unknown (Unknown; build:Unknown; Unknown 13.0.0) Alamofire/5.0.0"
467 )
468 .unwrap(),
469 DefaultUserAgent {
470 executable: None,
471 app_version: None,
472 bundle: None,
473 app_build: None,
474 os_name: None,
475 os_version: "13.0.0".parse().unwrap(),
476 alamofire_version: "5.0.0".parse().unwrap()
477 }
478 );
479 }
480
481 #[test]
482 fn test_to_string() {
483 assert_eq!(
484 DefaultUserAgent {
485 executable: Some("iOS Example".to_owned()),
486 app_version: Some("1.0.0".parse().unwrap()),
487 bundle: Some("org.alamofire.iOS-Example".to_owned()),
488 app_build: Some(DefaultUserAgentAppBuild("1.0.0".parse().unwrap())),
489 os_name: Some(DefaultUserAgentOsName::iOS),
490 os_version: "13.0.0".parse().unwrap(),
491 alamofire_version: "5.0.0".parse().unwrap()
492 }
493 .to_string(),
494 "iOS Example/1.0.0 (org.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0"
495 );
496
497 assert_eq!(
498 DefaultUserAgent {
499 executable: None,
500 app_version: None,
501 bundle: None,
502 app_build: None,
503 os_name: None,
504 os_version: "13.0.0".parse().unwrap(),
505 alamofire_version: "5.0.0".parse().unwrap()
506 }
507 .to_string(),
508 "Unknown/Unknown (Unknown; build:Unknown; Unknown 13.0.0) Alamofire/5.0.0"
509 );
510 }
511}