user_agent_parser/lib.rs
1/*!
2# User Agent Parser
3
4A parser to get the product, OS, device, cpu, and engine information from a user agent, inspired by https://github.com/faisalman/ua-parser-js and https://github.com/ua-parser/uap-core
5
6## Usage
7
8You can make a **regexes.yaml** file or copy one from https://github.com/ua-parser/uap-core
9
10This is a simple example of **regexes.yaml**.
11
12```yaml
13user_agent_parsers:
14 - regex: '(ESPN)[%20| ]+Radio/(\d+)\.(\d+)\.(\d+) CFNetwork'
15 - regex: '(Namoroka|Shiretoko|Minefield)/(\d+)\.(\d+)\.(\d+(?:pre|))'
16 family_replacement: 'Firefox ($1)'
17 - regex: '(Android) Eclair'
18 v1_replacement: '2'
19 v2_replacement: '1'
20
21os_parsers:
22 - regex: 'Win(?:dows)? ?(95|98|3.1|NT|ME|2000|XP|Vista|7|CE)'
23 os_replacement: 'Windows'
24 os_v1_replacement: '$1'
25
26device_parsers:
27 - regex: '\bSmartWatch *\( *([^;]+) *; *([^;]+) *;'
28 device_replacement: '$1 $2'
29 brand_replacement: '$1'
30 model_replacement: '$2'
31```
32
33Then, use the `from_path` (or `from_str` if your YAML data is in-memory) associated function to create a `UserAgentParser` instance.
34
35
36```rust,no_run
37use user_agent_parser::UserAgentParser;
38
39let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap();
40```
41
42Use the `parse_*` methods and input a user-agent string to get information.
43
44```rust,no_run
45use user_agent_parser::UserAgentParser;
46
47let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap();
48
49let user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 [FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5]";
50
51let product = ua_parser.parse_product(user_agent);
52
53println!("{:#?}", product);
54
55// Product {
56// name: Some(
57// "Facebook",
58// ),
59// major: Some(
60// "8",
61// ),
62// minor: Some(
63// "0",
64// ),
65// patch: Some(
66// "0",
67// ),
68// }
69
70let os = ua_parser.parse_os(user_agent);
71
72println!("{:#?}", os);
73
74// OS {
75// name: Some(
76// "iOS",
77// ),
78// major: None,
79// minor: None,
80// patch: None,
81// patch_minor: None,
82// }
83
84let device = ua_parser.parse_device(user_agent);
85
86println!("{:#?}", device);
87
88// Device {
89// name: Some(
90// "iPhone",
91// ),
92// brand: Some(
93// "Apple",
94// ),
95// model: Some(
96// "iPhone4,1",
97// ),
98// }
99
100let cpu = ua_parser.parse_cpu(user_agent);
101
102println!("{:#?}", cpu);
103
104// CPU {
105// architecture: Some(
106// "amd64",
107// ),
108// }
109
110let engine = ua_parser.parse_engine(user_agent);
111
112println!("{:#?}", engine);
113
114// Engine {
115// name: Some(
116// "Gecko",
117// ),
118// major: Some(
119// "10",
120// ),
121// minor: Some(
122// "0",
123// ),
124// patch: None,
125// }
126```
127
128The lifetime of result instances of the `parse_*` methods depends on the user-agent string and the `UserAgentParser` instance. To make it independent, call the `into_owned` method.
129
130```rust,no_run
131use user_agent_parser::UserAgentParser;
132
133let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap();
134
135let product = ua_parser.parse_product("Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.04 (lucid) Firefox/3.6.12").into_owned();
136```
137
138## Rocket Support
139
140This crate supports the Rocket framework. All you have to do is enabling the `rocket` feature for this crate.
141
142```toml
143[dependencies.user-agent-parser]
144version = "*"
145features = ["rocket"]
146```
147
148Let `Rocket` manage a `UserAgentParser` instance, and the `Product`, `OS`, `Device`, `CPU`, `Engine` models of this crate (plus the `UserAgent` model) can be used as *Request Guards*.
149
150A request guard panics if no `UserAgentParser` is managed, because that is a mistake in the setup rather than something a single request can recover from. A request without a `User-Agent` header is fine and yields the default model.
151
152```rust,ignore
153#[macro_use]
154extern crate rocket;
155
156use user_agent_parser::{UserAgentParser, UserAgent, Product, OS, Device, CPU, Engine};
157
158#[get("/")]
159fn index(user_agent: UserAgent, product: Product, os: OS, device: Device, cpu: CPU, engine: Engine) -> String {
160 format!("{user_agent:#?}\n{product:#?}\n{os:#?}\n{device:#?}\n{cpu:#?}\n{engine:#?}",
161 user_agent = user_agent,
162 product = product,
163 os = os,
164 device = device,
165 cpu = cpu,
166 engine = engine,
167 )
168}
169
170#[launch]
171fn rocket() -> _ {
172 rocket::build()
173 .manage(UserAgentParser::from_path("/path/to/regexes.yaml").unwrap())
174 .mount("/", routes![index])
175}
176```
177
178## Axum Support
179
180This crate also supports the Axum framework. All you have to do is enabling the `axum` feature for this crate.
181
182```toml
183[dependencies.user-agent-parser]
184version = "*"
185features = ["axum"]
186```
187
188Share a `UserAgentParser` instance through an `Extension<Arc<UserAgentParser>>` layer, and the owned models `Product<'static>`, `OS<'static>`, `Device<'static>`, `CPU<'static>`, `Engine<'static>` (plus the `UserAgent<'static>` model) can be used as *extractors*.
189
190An extractor panics if that layer is missing, because that is a mistake in the setup rather than something a single request can recover from. A request without a `User-Agent` header is fine and yields the default model.
191
192```rust,ignore
193use std::sync::Arc;
194
195use axum::{routing::get, Extension, Router};
196use user_agent_parser::{UserAgentParser, UserAgent, Product, OS, Device, CPU, Engine};
197
198async fn index(
199 user_agent: UserAgent<'static>,
200 product: Product<'static>,
201 os: OS<'static>,
202 device: Device<'static>,
203 cpu: CPU<'static>,
204 engine: Engine<'static>,
205) -> String {
206 format!("{user_agent:#?}\n{product:#?}\n{os:#?}\n{device:#?}\n{cpu:#?}\n{engine:#?}")
207}
208
209#[tokio::main]
210async fn main() {
211 let app = Router::new()
212 .route("/", get(index))
213 .layer(Extension(Arc::new(UserAgentParser::from_path("/path/to/regexes.yaml").unwrap())));
214
215 let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();
216
217 axum::serve(listener, app).await.unwrap();
218}
219```
220
221## Testing
222
223```bash
224# git clone --recurse-submodules git://github.com/magiclen/user-agent-parser.git
225
226git clone git://github.com/magiclen/user-agent-parser.git
227
228cd user-agent-parser
229
230git submodule init
231git submodule update --recursive
232
233cargo test
234```
235*/
236
237#![cfg_attr(docsrs, feature(doc_cfg))]
238
239mod errors;
240mod models;
241mod regexes;
242
243#[cfg(feature = "rocket")]
244mod request_guards;
245
246#[cfg(feature = "axum")]
247mod request_guards_axum;
248
249use std::{borrow::Cow, fs, path::Path, str::FromStr};
250
251pub use errors::UserAgentParserError;
252pub use models::*;
253use regexes::*;
254use yaml_rust::{Yaml, YamlLoader};
255
256/// A set of regular expressions which can extract the product, OS, device, CPU and engine information out of a user-agent string.
257#[derive(Debug)]
258pub struct UserAgentParser {
259 product_regexes: Vec<ProductRegex>,
260 os_regexes: Vec<OSRegex>,
261 device_regexes: Vec<DeviceRegex>,
262 cpu_regexes: &'static [CPURegex],
263 engine_regexes: &'static [EngineRegex],
264}
265
266impl UserAgentParser {
267 /// Read the list of regular expressions (YAML data) from a file to create a `UserAgentParser` instance.
268 #[inline]
269 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<UserAgentParser, UserAgentParserError> {
270 let yaml = fs::read_to_string(path)?;
271
272 Self::from_str(yaml)
273 }
274
275 /// Read the list of regular expressions (YAML data) from a string to create a `UserAgentParser` instance.
276 #[allow(clippy::should_implement_trait)]
277 pub fn from_str<S: AsRef<str>>(yaml: S) -> Result<UserAgentParser, UserAgentParserError> {
278 let yamls = YamlLoader::load_from_str(yaml.as_ref())?;
279
280 if yamls.is_empty() {
281 Err(UserAgentParserError::IncorrectSource)
282 } else {
283 let yaml = &yamls[0];
284
285 match yaml.as_hash() {
286 Some(yaml) => {
287 let user_agent_parsers =
288 yaml.get(&Yaml::String("user_agent_parsers".to_string()));
289 let os_parsers = yaml.get(&Yaml::String("os_parsers".to_string()));
290 let device_parsers = yaml.get(&Yaml::String("device_parsers".to_string()));
291
292 let product_regexes = match user_agent_parsers {
293 Some(user_agent_parsers) => ProductRegex::from_yaml(user_agent_parsers)?,
294 None => Vec::new(),
295 };
296
297 let os_regexes = match os_parsers {
298 Some(os_parsers) => OSRegex::from_yaml(os_parsers)?,
299 None => Vec::new(),
300 };
301
302 let device_regexes = match device_parsers {
303 Some(device_parsers) => DeviceRegex::from_yaml(device_parsers)?,
304 None => Vec::new(),
305 };
306
307 Ok(UserAgentParser {
308 product_regexes,
309 os_regexes,
310 device_regexes,
311 cpu_regexes: CPURegex::built_in_regexes(),
312 engine_regexes: EngineRegex::built_in_regexes(),
313 })
314 },
315 None => Err(UserAgentParserError::IncorrectSource),
316 }
317 }
318 }
319}
320
321/// The results borrow from both the parser and the user-agent string, so call `into_owned` on them to break that tie.
322impl UserAgentParser {
323 /// Extracts the product information, falling back to a `name` of `Some("Other")` when no pattern matches.
324 pub fn parse_product<'a, S: AsRef<str> + ?Sized>(&'a self, user_agent: &'a S) -> Product<'a> {
325 let user_agent = user_agent.as_ref();
326
327 let mut product = Product::default();
328
329 // Do not replace this walk with a `RegexSet`, it was tried and it is much slower.
330 // A `RegexSet` has to report every pattern that matches, so it cannot stop at the first one and cannot use the fast leftmost engine.
331 // Trying the patterns one by one wins because each one carries its own literal prefilter, which rejects most of them almost for free.
332 // Measured against uap-core (433 product, 204 OS and 633 device patterns) with `cargo bench`:
333 // parse_product 94us -> 4852us, parse_os 20us -> 970us, parse_device 226us -> 4769us, and from_path 234ms -> 528ms.
334 for product_regex in self.product_regexes.iter() {
335 // `is_match` skips capture tracking, so the regexes that do not match cost much less than a `captures` call.
336 if !product_regex.regex.is_match(user_agent) {
337 continue;
338 }
339
340 let captures = product_regex.regex.captures(user_agent).unwrap();
341
342 product.name = resolve(1, product_regex.family_replacement.as_ref(), &captures);
343 product.major = resolve(2, product_regex.v1_replacement.as_ref(), &captures);
344 product.minor = resolve(3, product_regex.v2_replacement.as_ref(), &captures);
345 product.patch = resolve(4, product_regex.v3_replacement.as_ref(), &captures);
346
347 break;
348 }
349
350 if product.name.is_none() {
351 product.name = Some(Cow::from("Other"));
352 }
353
354 product
355 }
356
357 /// Extracts the OS information, falling back to a `name` of `Some("Other")` when no pattern matches.
358 pub fn parse_os<'a, S: AsRef<str> + ?Sized>(&'a self, user_agent: &'a S) -> OS<'a> {
359 let user_agent = user_agent.as_ref();
360
361 let mut os = OS::default();
362
363 for os_regex in self.os_regexes.iter() {
364 if !os_regex.regex.is_match(user_agent) {
365 continue;
366 }
367
368 let captures = os_regex.regex.captures(user_agent).unwrap();
369
370 os.name = resolve(1, os_regex.os_replacement.as_ref(), &captures);
371 os.major = resolve(2, os_regex.os_v1_replacement.as_ref(), &captures);
372 os.minor = resolve(3, os_regex.os_v2_replacement.as_ref(), &captures);
373 os.patch = resolve(4, os_regex.os_v3_replacement.as_ref(), &captures);
374 os.patch_minor = resolve(5, os_regex.os_v4_replacement.as_ref(), &captures);
375
376 break;
377 }
378
379 if os.name.is_none() {
380 os.name = Some(Cow::from("Other"));
381 }
382
383 os
384 }
385
386 /// Extracts the device information, falling back to a `name` of `Some("Other")` when no pattern matches.
387 pub fn parse_device<'a, S: AsRef<str> + ?Sized>(&'a self, user_agent: &'a S) -> Device<'a> {
388 let user_agent = user_agent.as_ref();
389
390 let mut device = Device::default();
391
392 for device_regex in self.device_regexes.iter() {
393 if !device_regex.regex.is_match(user_agent) {
394 continue;
395 }
396
397 let captures = device_regex.regex.captures(user_agent).unwrap();
398
399 device.name = resolve(1, device_regex.device_replacement.as_ref(), &captures);
400 // uap-core gives the brand no positional default; it is only set when a `brand_replacement` exists.
401 device.brand =
402 device_regex.brand_replacement.as_ref().and_then(|r| r.resolve(&captures));
403 device.model = resolve(1, device_regex.model_replacement.as_ref(), &captures);
404
405 break;
406 }
407
408 if device.name.is_none() {
409 device.name = Some(Cow::from("Other"));
410 }
411
412 device
413 }
414
415 /// Extracts the CPU architecture, which stays `None` when no pattern matches.
416 pub fn parse_cpu<'a, S: AsRef<str> + ?Sized>(&'a self, user_agent: &'a S) -> CPU<'a> {
417 let user_agent = user_agent.as_ref();
418
419 let mut cpu = CPU::default();
420
421 for cpu_regex in self.cpu_regexes.iter() {
422 if !cpu_regex.regex.is_match(user_agent) {
423 continue;
424 }
425
426 let captures = cpu_regex.regex.captures(user_agent).unwrap();
427
428 cpu.architecture = resolve(1, cpu_regex.architecture_replacement.as_ref(), &captures);
429
430 break;
431 }
432
433 cpu
434 }
435
436 /// Extracts the layout engine information, which stays `None` when no pattern matches.
437 pub fn parse_engine<'a, S: AsRef<str> + ?Sized>(&'a self, user_agent: &'a S) -> Engine<'a> {
438 let user_agent = user_agent.as_ref();
439
440 let mut engine = Engine::default();
441
442 for engine_regex in self.engine_regexes.iter() {
443 if !engine_regex.regex.is_match(user_agent) {
444 continue;
445 }
446
447 let captures = engine_regex.regex.captures(user_agent).unwrap();
448
449 engine.name = resolve(1, engine_regex.name_replacement.as_ref(), &captures);
450 engine.major = resolve(2, engine_regex.engine_v1_replacement.as_ref(), &captures);
451 engine.minor = resolve(3, engine_regex.engine_v2_replacement.as_ref(), &captures);
452 engine.patch = resolve(4, engine_regex.engine_v3_replacement.as_ref(), &captures);
453
454 break;
455 }
456
457 engine
458 }
459}
460
461impl FromStr for UserAgentParser {
462 type Err = UserAgentParserError;
463
464 #[inline]
465 fn from_str(s: &str) -> Result<Self, Self::Err> {
466 UserAgentParser::from_str(s)
467 }
468}