statsig_rust/evaluation/user_agent_parsing/statsig_uaparser/
ua_parser.rs

1use super::tokenizer::{Token, Tokenizer};
2
3pub struct UaParser;
4
5impl UaParser {
6    pub fn parse_os(agent: &str) -> ParserResult<'_> {
7        let result = Tokenizer::run(agent);
8
9        if let Some(token) = &result.possible_os_token {
10            return create_res(token.tag, token.get_version());
11        }
12
13        for token in &result.tokens {
14            if token.tag == "ATV OS X" {
15                return create_res("ATV OS X", None);
16            }
17
18            if token.tag == "iPhone OS" || token.tag == "iOS" {
19                return create_res("iOS", token.get_version());
20            }
21
22            if token.tag == "CPU OS" && result.ios_hint {
23                return create_res("iOS", token.get_version());
24            }
25
26            if token.tag == "Version" && result.ios_hint && result.macos_hint {
27                return create_res("iOS", token.get_version());
28            }
29
30            if token.tag == "CFNetwork" {
31                return create_res("iOS", None);
32            }
33
34            if token.tag == "Android" {
35                return create_res(token.tag, token.get_version());
36            }
37
38            if token.tag == "Chromecast" {
39                return create_res("Chromecast", None);
40            }
41
42            if token.tag == "Red Hat" {
43                return create_res("Red Hat", None);
44            }
45
46            if token.tag == "Kindle" {
47                return create_res("Kindle", token.get_version());
48            }
49
50            if token.tag == "Ubuntu" {
51                return create_res("Ubuntu", token.get_version());
52            }
53        }
54
55        if result.ios_hint {
56            return create_res("iOS", None);
57        }
58
59        if result.macos_hint {
60            return create_res("Mac OS X", None);
61        }
62
63        if result.windows_hint {
64            return create_res("Windows", None);
65        }
66
67        if result.linux_hint {
68            return create_res("Linux", None);
69        }
70
71        create_res("Other", None)
72    }
73
74    pub fn parse_browser(agent: &str) -> ParserResult<'_> {
75        let result = Tokenizer::run(agent);
76
77        if let Some(token) = &result.possible_browser_token {
78            return create_res(token.tag, token.get_version());
79        }
80
81        let mut android_token: Option<&Token> = None;
82        let mut chrome_token: Option<&Token> = None;
83        let mut version_token: Option<&Token> = None;
84
85        for token in &result.tokens {
86            if token.tag == "Firefox" {
87                if result.mobile_hint {
88                    return create_res("Firefox Mobile", token.get_version());
89                }
90
91                return create_res("Firefox", token.get_version());
92            }
93
94            if token.tag == "Android" {
95                android_token = Some(token);
96                continue;
97            }
98
99            if token.tag == "Version" {
100                version_token = Some(token);
101                continue;
102            }
103
104            if token.tag == "Yahoo! Slurp" {
105                return create_res("Yahoo! Slurp", None);
106            }
107
108            if token.tag == "Silk" {
109                if result.playstation_hint {
110                    return create_res("NetFront NX", None);
111                }
112
113                return create_res("Amazon Silk", token.get_version());
114            }
115
116            if token.tag == "NetFront NX" {
117                return create_res("NetFront NX", token.get_version());
118            }
119
120            if token.tag == "YaBrowser" {
121                return create_res("Yandex Browser", token.get_version());
122            }
123
124            if token.tag == "Edge" && result.mobile_hint {
125                return create_res("Edge Mobile", token.get_version());
126            }
127
128            if token.tag == "Edge" {
129                return create_res("Edge", token.get_version());
130            }
131
132            if token.tag == "Chrome" {
133                chrome_token = Some(token);
134                continue;
135            }
136
137            if token.tag == "axios" {
138                return create_res("axios", token.get_version());
139            }
140
141            if token.tag == "HeadlessChrome" {
142                return create_res("HeadlessChrome", token.get_version());
143            }
144        }
145
146        if let Some(token) = chrome_token {
147            if version_token.is_some() {
148                return create_res("Chrome Mobile WebView", token.get_version());
149            }
150
151            if result.mobile_hint && token.version.is_some() && !result.huawei_hint {
152                return create_res("Chrome Mobile", token.get_version());
153            }
154
155            if token.version.is_none() {
156                if let Some(token) = android_token {
157                    return create_res("Android", token.get_version());
158                }
159            }
160
161            return create_res("Chrome", token.get_version());
162        }
163
164        if let Some(token) = android_token {
165            return create_res("Android", token.get_version());
166        }
167
168        if result.cfnetwork_hint {
169            if result.tokens[0].tag == "NetworkingExtension" {
170                return create_res("CFNetwork", result.tokens[1].get_version());
171            }
172            return create_res(result.tokens[0].tag, result.tokens[0].get_version());
173        }
174
175        if result.safari_hint {
176            let version = version_token.and_then(|t| t.get_version());
177
178            if result.mobile_hint {
179                return create_res("Mobile Safari", version);
180            }
181
182            return create_res("Safari", version);
183        }
184
185        if result.ios_hint {
186            return create_res("Mobile Safari UI/WKWebView", None);
187        }
188
189        if result.crawler_hint {
190            return create_res("crawler", None);
191        }
192        create_res("Other", None)
193    }
194}
195
196#[derive(Debug, Default)]
197pub struct Version<'a> {
198    pub major: Option<&'a str>,
199    pub minor: Option<&'a str>,
200    pub patch: Option<&'a str>,
201    pub patch_minor: Option<&'a str>,
202}
203
204impl<'a> Version<'a> {
205    pub fn major(major: &'a str) -> Self {
206        Self {
207            major: Some(major),
208            minor: None,
209            patch: None,
210            patch_minor: None,
211        }
212    }
213
214    pub fn get_version_string(&self) -> Option<String> {
215        let major = self.major?;
216
217        let mut version = String::new();
218
219        version.push_str(major);
220
221        if let Some(minor) = self.minor {
222            version.push('.');
223            version.push_str(minor);
224        }
225
226        if let Some(patch) = self.patch {
227            version.push('.');
228            version.push_str(patch);
229        }
230
231        if let Some(patch_minor) = self.patch_minor {
232            version.push('.');
233            version.push_str(patch_minor);
234        }
235
236        Some(version)
237    }
238}
239
240pub struct ParserResult<'a> {
241    pub name: &'a str,
242    pub version: Version<'a>,
243}
244
245fn create_res<'a>(name: &'a str, version: Option<Version<'a>>) -> ParserResult<'a> {
246    ParserResult {
247        name,
248        version: version.unwrap_or_default(),
249    }
250}