statsig_rust/evaluation/user_agent_parsing/statsig_uaparser/
ua_parser.rs1use 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 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}