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<'a>(agent: &'a str) -> ParserResult<'a> {
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" {
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<'a>(agent: &'a str) -> ParserResult<'a> {
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 == "ChatGPT" {
109                return create_res("ChatGPT", token.get_version());
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 == "Chrome" {
137                chrome_token = Some(token);
138                continue;
139            }
140        }
141
142        if let Some(token) = chrome_token {
143            if version_token.is_some() {
144                return create_res("Chrome Mobile WebView", token.get_version());
145            }
146
147            if result.mobile_hint && token.version.is_some() && !result.huawei_hint {
148                return create_res("Chrome Mobile", token.get_version());
149            }
150
151            if token.version.is_none() {
152                if let Some(token) = android_token {
153                    return create_res("Android", token.get_version());
154                }
155            }
156
157            return create_res("Chrome", token.get_version());
158        }
159
160        if let Some(token) = android_token {
161            return create_res("Android", token.get_version());
162        }
163
164        if result.safari_hint {
165            let version = version_token.and_then(|t| t.get_version());
166
167            if result.mobile_hint {
168                return create_res("Mobile Safari", version);
169            }
170
171            return create_res("Safari", version);
172        }
173
174        if result.ios_hint {
175            return create_res("Mobile Safari UI/WKWebView", None);
176        }
177
178        create_res("Other", None)
179    }
180}
181
182#[derive(Debug, Default)]
183pub struct Version<'a> {
184    pub major: Option<&'a str>,
185    pub minor: Option<&'a str>,
186    pub patch: Option<&'a str>,
187    pub patch_minor: Option<&'a str>,
188}
189
190impl<'a> Version<'a> {
191    pub fn major(major: &'a str) -> Self {
192        Self {
193            major: Some(major),
194            minor: None,
195            patch: None,
196            patch_minor: None,
197        }
198    }
199
200    pub fn get_version_string(&self) -> Option<String> {
201        let major = self.major?;
202
203        let mut version = String::new();
204
205        version.push_str(major);
206
207        if let Some(minor) = self.minor {
208            version.push('.');
209            version.push_str(minor);
210        }
211
212        if let Some(patch) = self.patch {
213            version.push('.');
214            version.push_str(patch);
215        }
216
217        if let Some(patch_minor) = self.patch_minor {
218            version.push('.');
219            version.push_str(patch_minor);
220        }
221
222        Some(version)
223    }
224}
225
226pub struct ParserResult<'a> {
227    pub name: &'a str,
228    pub version: Version<'a>,
229}
230
231fn create_res<'a>(name: &'a str, version: Option<Version<'a>>) -> ParserResult<'a> {
232    ParserResult {
233        name,
234        version: version.unwrap_or_default(),
235    }
236}