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.starts_with("Android") {
39                return create_res("Android", None);
40            }
41
42            if token.tag == "Chromecast" {
43                return create_res("Chromecast", None);
44            }
45
46            if token.tag == "Red Hat" {
47                return create_res("Red Hat", None);
48            }
49
50            if token.tag == "Kindle" {
51                return create_res("Kindle", token.get_version());
52            }
53
54            if token.tag == "Ubuntu" {
55                return create_res("Ubuntu", token.get_version());
56            }
57        }
58
59        if result.ios_hint {
60            return create_res("iOS", None);
61        }
62
63        if result.macos_hint {
64            return create_res("Mac OS X", None);
65        }
66
67        if result.windows_hint {
68            return create_res("Windows", None);
69        }
70
71        if result.linux_hint {
72            return create_res("Linux", None);
73        }
74
75        create_res("Other", None)
76    }
77
78    pub fn parse_browser(agent: &str) -> ParserResult<'_> {
79        let result = Tokenizer::run(agent);
80
81        if let Some(token) = &result.possible_browser_token {
82            return create_res(token.tag, token.get_version());
83        }
84
85        let mut android_token: Option<&Token> = None;
86        let mut chrome_token: Option<&Token> = None;
87        let mut version_token: Option<&Token> = None;
88
89        for token in &result.tokens {
90            if token.tag == "Firefox" {
91                if result.mobile_hint {
92                    return create_res("Firefox Mobile", token.get_version());
93                }
94
95                return create_res("Firefox", token.get_version());
96            }
97
98            if token.tag == "Android" {
99                android_token = Some(token);
100                continue;
101            }
102
103            if token.tag == "Version" {
104                version_token = Some(token);
105                continue;
106            }
107
108            if token.tag == "Yahoo! Slurp" {
109                return create_res("Yahoo! Slurp", None);
110            }
111
112            if token.tag == "Silk" {
113                if result.playstation_hint {
114                    return create_res("NetFront NX", None);
115                }
116
117                return create_res("Amazon Silk", token.get_version());
118            }
119
120            if token.tag == "NetFront NX" {
121                return create_res("NetFront NX", token.get_version());
122            }
123
124            if token.tag == "YaBrowser" {
125                return create_res("Yandex Browser", token.get_version());
126            }
127
128            if token.tag == "Edge" && result.mobile_hint {
129                return create_res("Edge Mobile", token.get_version());
130            }
131
132            if token.tag == "Edge" {
133                return create_res("Edge", token.get_version());
134            }
135
136            if token.tag == "Opera" {
137                if result.mobile_hint {
138                    return create_res("Opera Mobile", token.get_version());
139                }
140                return create_res("Opera", token.get_version());
141            }
142
143            if token.tag == "Chrome" {
144                chrome_token = Some(token);
145                continue;
146            }
147
148            if token.tag == "axios" {
149                return create_res("axios", token.get_version());
150            }
151
152            if token.tag == "HeadlessChrome" {
153                return create_res("HeadlessChrome", token.get_version());
154            }
155        }
156
157        if let Some(token) = chrome_token {
158            if version_token.is_some() {
159                return create_res("Chrome Mobile WebView", token.get_version());
160            }
161
162            if result.mobile_hint && token.version.is_some() && !result.huawei_hint {
163                return create_res("Chrome Mobile", token.get_version());
164            }
165
166            if token.version.is_none() {
167                if let Some(token) = android_token {
168                    return create_res("Android", token.get_version());
169                }
170            }
171
172            return create_res("Chrome", token.get_version());
173        }
174
175        if let Some(token) = android_token {
176            return create_res("Android", token.get_version());
177        }
178
179        if result.cfnetwork_hint && !result.tokens.is_empty() {
180            if result.tokens[0].tag == "NetworkingExtension" {
181                return create_res(
182                    "CFNetwork",
183                    result.tokens.get(1).and_then(|t| t.get_version()),
184                );
185            }
186            return create_res(result.tokens[0].tag, result.tokens[0].get_version());
187        }
188
189        if result.safari_hint {
190            let version = version_token.and_then(|t| t.get_version());
191
192            if result.mobile_hint && !result.macos_hint {
193                // UA string has this “Mobile” flag likely for compatibility or simulation purposes but it's running ins macos
194                return create_res("Mobile Safari", version);
195            }
196
197            return create_res("Safari", version);
198        }
199
200        if result.ios_hint {
201            return create_res(
202                "Mobile Safari UI/WKWebView",
203                result.possible_os_token.and_then(|o| o.get_version()),
204            );
205        }
206
207        if result.crawler_hint {
208            return create_res("crawler", None);
209        }
210        create_res("Other", None)
211    }
212}
213
214#[derive(Debug, Default)]
215pub struct Version<'a> {
216    pub major: Option<&'a str>,
217    pub minor: Option<&'a str>,
218    pub patch: Option<&'a str>,
219    pub patch_minor: Option<&'a str>,
220}
221
222impl<'a> Version<'a> {
223    pub fn major(major: &'a str) -> Self {
224        Self {
225            major: Some(major),
226            minor: None,
227            patch: None,
228            patch_minor: None,
229        }
230    }
231
232    pub fn get_version_string(&self) -> Option<String> {
233        let major = self.major?;
234
235        let mut version = String::new();
236
237        version.push_str(major);
238
239        if let Some(minor) = self.minor {
240            version.push('.');
241            version.push_str(minor);
242        }
243
244        if let Some(patch) = self.patch {
245            version.push('.');
246            version.push_str(patch);
247        }
248
249        if let Some(patch_minor) = self.patch_minor {
250            version.push('.');
251            version.push_str(patch_minor);
252        }
253
254        Some(version)
255    }
256}
257
258pub struct ParserResult<'a> {
259    pub name: &'a str,
260    pub version: Version<'a>,
261}
262
263fn create_res<'a>(name: &'a str, version: Option<Version<'a>>) -> ParserResult<'a> {
264    ParserResult {
265        name,
266        version: version.unwrap_or_default(),
267    }
268}