Skip to main content

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}