1#![doc = include_str!("../README.md")]
8
9use regex::Regex;
10
11mod rules_data {
13 include!(concat!(env!("OUT_DIR"), "/rules_data.rs"));
14}
15
16struct Rules {
17 user_agent: Vec<UserAgentRule>,
18 os: Vec<OsRule>,
19}
20
21struct UserAgentRule {
22 regex: Regex,
23 family_replacement: Option<&'static str>,
24 v1_replacement: Option<&'static str>,
25 v2_replacement: Option<&'static str>,
26 v3_replacement: Option<&'static str>,
27}
28
29struct OsRule {
30 regex: Regex,
31 os_replacement: Option<&'static str>,
32 os_v1_replacement: Option<&'static str>,
33 os_v2_replacement: Option<&'static str>,
34 os_v3_replacement: Option<&'static str>,
35}
36
37impl Rules {
38 fn parse() -> Self {
39 Self {
40 user_agent: rules_data::USER_AGENT_RULES
41 .iter()
42 .map(|rule| UserAgentRule {
43 regex: Regex::new(rule.regex).expect("Invalid regex"),
44 family_replacement: rule.family_replacement,
45 v1_replacement: rule.v1_replacement,
46 v2_replacement: rule.v2_replacement,
47 v3_replacement: rule.v3_replacement,
48 })
49 .collect(),
50 os: rules_data::OS_RULES
51 .iter()
52 .map(|rule| OsRule {
53 regex: Regex::new(rule.regex).expect("Invalid regex"),
54 os_replacement: rule.os_replacement,
55 os_v1_replacement: rule.os_v1_replacement,
56 os_v2_replacement: rule.os_v2_replacement,
57 os_v3_replacement: rule.os_v3_replacement,
58 })
59 .collect(),
60 }
61 }
62}
63
64#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
67pub struct UserAgent {
68 pub client: Client,
70 pub os: OS,
72}
73
74#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
76pub struct Client {
77 pub family: String,
79 pub version: Option<String>,
81}
82
83#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
85pub struct OS {
86 pub family: String,
88 pub version: Option<String>,
90}
91
92pub struct UserAgentParser {
95 rules: Rules,
96}
97
98impl Default for UserAgentParser {
99 fn default() -> Self {
100 Self {
101 rules: Rules::parse(),
102 }
103 }
104}
105
106impl UserAgentParser {
107 pub fn new() -> Self {
109 Self::default()
110 }
111
112 pub fn parse(&self, user_agent: &str) -> UserAgent {
114 UserAgent {
115 client: self.parse_client(user_agent),
116 os: self.parse_os(user_agent),
117 }
118 }
119
120 fn parse_client(&self, user_agent: &str) -> Client {
122 for rule in &self.rules.user_agent {
123 if let Some(captures) = rule.regex.captures(user_agent) {
124 let family = rule
125 .family_replacement
126 .map(|s| Self::map_replacement(s, &captures))
127 .unwrap_or_else(|| captures[1].to_string());
128 let major = rule
129 .v1_replacement
130 .map(|s| Self::map_replacement(s, &captures))
131 .or_else(|| captures.get(2).map(|m| m.as_str().to_string()));
132 let minor = rule
133 .v2_replacement
134 .map(|s| Self::map_replacement(s, &captures))
135 .or_else(|| captures.get(3).map(|m| m.as_str().to_string()));
136 let patch = rule
137 .v3_replacement
138 .map(|s| Self::map_replacement(s, &captures))
139 .or_else(|| captures.get(4).map(|m| m.as_str().to_string()));
140 return Client {
141 family,
142 version: Self::concat_version(major, minor, patch),
143 };
144 }
145 }
146 Client {
147 family: "Other".to_string(),
148 version: None,
149 }
150 }
151
152 fn parse_os(&self, user_agent: &str) -> OS {
154 for rule in &self.rules.os {
155 if let Some(captures) = rule.regex.captures(user_agent) {
156 let family = rule
157 .os_replacement
158 .map(|s| Self::map_replacement(s, &captures))
159 .unwrap_or_else(|| captures[1].to_string());
160 let major = rule
161 .os_v1_replacement
162 .map(|s| Self::map_replacement(s, &captures))
163 .or_else(|| captures.get(2).map(|m| m.as_str().to_string()));
164 let minor = rule
165 .os_v2_replacement
166 .map(|s| Self::map_replacement(s, &captures))
167 .or_else(|| captures.get(3).map(|m| m.as_str().to_string()));
168 let patch = rule
169 .os_v3_replacement
170 .map(|s| Self::map_replacement(s, &captures))
171 .or_else(|| captures.get(4).map(|m| m.as_str().to_string()));
172 return OS {
173 family,
174 version: Self::concat_version(major, minor, patch),
175 };
176 }
177 }
178 OS {
179 family: "Other".to_string(),
180 version: None,
181 }
182 }
183
184 fn map_replacement(replacement: &str, captures: ®ex::Captures) -> String {
185 let mut result = replacement.to_string();
186 if result.contains("$1") {
187 result = result.replace("$1", &captures[1]);
188 }
189 if result.contains("$2") {
190 result = result.replace("$2", &captures[2]);
191 }
192 if result.contains("$3") {
193 result = result.replace("$3", &captures[3]);
194 }
195 result
196 }
197
198 fn concat_version(
199 major: Option<String>,
200 minor: Option<String>,
201 patch: Option<String>,
202 ) -> Option<String> {
203 let mut version = String::new();
204 if let Some(major) = major {
205 version.push_str(&major);
206 }
207 if let Some(minor) = minor {
208 version.push('.');
209 version.push_str(&minor);
210 }
211 if let Some(patch) = patch {
212 version.push('.');
213 version.push_str(&patch);
214 }
215 if version.is_empty() {
216 None
217 } else {
218 Some(version)
219 }
220 }
221}
222
223#[cfg(test)]
225mod test {
226 use super::*;
227
228 #[test]
229 fn test_parser() {
230 let parser = UserAgentParser::new();
231
232 let ua = parser.parse(
233 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0",
234 );
235 assert_eq!(ua.client.family, "Firefox");
236 assert_eq!(ua.client.version.as_deref(), Some("133.0"));
237 assert_eq!(ua.os.family, "Mac OS X");
238 assert_eq!(ua.os.version.as_deref(), Some("10.15"));
239
240 let ua = parser.parse(
241 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
242 );
243 assert_eq!(ua.client.family, "Chrome");
244 assert_eq!(ua.client.version.as_deref(), Some("91.0.4472"));
245 assert_eq!(ua.os.family, "Windows");
246 assert_eq!(ua.os.version.as_deref(), Some("10"));
247
248 let ua = parser.parse(
249 "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Mobile/15E148 Safari/604.1",
250 );
251 assert_eq!(ua.client.family, "Mobile Safari");
252 assert_eq!(ua.client.version.as_deref(), Some("14.0.1"));
253 assert_eq!(ua.os.family, "iOS");
254 assert_eq!(ua.os.version.as_deref(), Some("14.6"));
255
256 let ua = parser.parse(
257 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59",
258 );
259 assert_eq!(ua.client.family, "Edge");
260 assert_eq!(ua.client.version.as_deref(), Some("91.0.864"));
261 assert_eq!(ua.os.family, "Windows");
262 assert_eq!(ua.os.version.as_deref(), Some("10"));
263
264 let ua = parser.parse("Telegram-Android/11.4.2");
265 assert_eq!(ua.client.family, "Telegram");
266 assert_eq!(ua.client.version.as_deref(), Some("11.4.2"));
267
268 let ua = parser.parse("UnknownUserAgent/1.0");
269 assert_eq!(ua.client.family, "Other");
270 assert_eq!(ua.client.version, None);
271 assert_eq!(ua.os.family, "Other");
272 assert_eq!(ua.os.version, None);
273 }
274}