alamofire_kit/
default_user_agent.rs

1//! [Ref](https://github.com/Alamofire/Alamofire/blob/5.6.4/Source/HTTPHeaders.swift#L370)
2
3use 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//
17#[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        //
61        //
62        //
63        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        //
81        //
82        //
83        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        //
110        //
111        //
112        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        //
118        //
119        //
120        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        //
139        //
140        //
141        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        //
147        //
148        //
149        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        //
166        //
167        //
168        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        //
174        //
175        //
176        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        //
193        //
194        //
195        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        //
212        //
213        //
214        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        //
220        //
221        //
222        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        //
242        //
243        //
244        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//
257#[derive(Debug, Clone)]
258pub enum DefaultUserAgentParseError {
259    //
260    ExecutableReadFailed(String),
261    ExecutableParseFailed(String),
262    //
263    AppVersionReadFailed(String),
264    AppVersionParseFailed(String),
265    //
266    BundleReadFailed(String),
267    BundleParseFailed(String),
268    //
269    AppBuildReadFailed(String),
270    AppBuildParseFailed(String),
271    //
272    OsNameReadFailed(String),
273    OsNameParseFailed(String),
274    //
275    OsVersionReadFailed(String),
276    OsVersionParseFailed(String),
277    //
278    AlamofireVersionReadFailed(String),
279    AlamofireVersionParseFailed(String),
280    //
281    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
290//
291impl 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//
325//
326//
327#[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//
374//
375//
376#[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}