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 == "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}