uaparser/
lib.rs

1//! This crate is an implementation of a User Agent Parser, similar to those
2//! found as part of the [UA-Parser Community](https://github.com/ua-parser). It tries to remain as
3//! consistent with the other implementations as possible while remaining simple
4//! and legible.
5//!
6//! ```rust
7//! # use uaparser::*;
8//! let ua_parser = UserAgentParser::from_yaml("./src/core/regexes.yaml").expect("Parser creation failed");
9//! let user_agent_string =
10//!     String::from("Mozilla/5.0 (X11; Linux x86_64; rv:2.0b8pre) Gecko/20101031 Firefox-4.0/4.0b8pre");
11//! let client = ua_parser.parse(&user_agent_string);
12//!
13//! let device = ua_parser.parse_device(&user_agent_string);
14//! let os = ua_parser.parse_os(&user_agent_string);
15//! let user_agent = ua_parser.parse_user_agent(&user_agent_string);
16//!
17//! assert_eq!(client.device, device);
18//! assert_eq!(client.os, os);
19//! assert_eq!(client.user_agent, user_agent);
20//! ```
21//!
22//! Alternatively you can use the `UserAgentParserBuilder` to create a parser:
23//! ```rust
24//! # use uaparser::*;
25//! let ua_parser = UserAgentParser::builder().build_from_yaml("./src/core/regexes.yaml").expect("Parser creation failed");
26//! let user_agent_string =
27//!     String::from("Mozilla/5.0 (X11; Linux x86_64; rv:2.0b8pre) Gecko/20101031 Firefox-4.0/4.0b8pre");
28//! let client = ua_parser.parse(&user_agent_string);
29//!
30//! let device = ua_parser.parse_device(&user_agent_string);
31//! let os = ua_parser.parse_os(&user_agent_string);
32//! let user_agent = ua_parser.parse_user_agent(&user_agent_string);
33//!
34//! assert_eq!(client.device, device);
35//! assert_eq!(client.os, os);
36//! assert_eq!(client.user_agent, user_agent);
37//! ```
38
39#![deny(clippy::all)]
40#![deny(clippy::pedantic)]
41#![allow(clippy::missing_errors_doc)]
42#![allow(clippy::wildcard_imports)]
43#![allow(clippy::module_name_repetitions)]
44
45use serde_derive::{Deserialize, Serialize};
46
47mod client;
48mod device;
49pub use device::Device;
50
51mod os;
52pub use os::OS;
53
54mod user_agent;
55pub use user_agent::UserAgent;
56
57mod file;
58mod parser;
59
60pub use parser::{Error, UserAgentParser};
61
62pub use client::Client;
63
64pub trait Parser {
65    fn parse<'a>(&self, user_agent: &'a str) -> Client<'a>;
66    fn parse_device<'a>(&self, user_agent: &'a str) -> Device<'a>;
67    fn parse_os<'a>(&self, user_agent: &'a str) -> OS<'a>;
68    fn parse_user_agent<'a>(&self, user_agent: &'a str) -> UserAgent<'a>;
69}
70
71pub(crate) trait SubParser<'a> {
72    type Item;
73    fn try_parse(&self, text: &'a str) -> Option<Self::Item>;
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use std::{borrow::Cow, fmt::Debug};
80
81    #[test]
82    fn parse_os_with_unicode() {
83        let parser = UserAgentParser::builder()
84            .with_unicode_support(true)
85            .build_from_yaml("./src/core/regexes.yaml")
86            .expect("Parser creation failed");
87        do_parse_os_test_with_parser(&parser)
88    }
89
90    #[test]
91    fn parse_os_without_unicode() {
92        let parser = UserAgentParser::builder()
93            .with_unicode_support(false)
94            .build_from_yaml("./src/core/regexes.yaml")
95            .expect("Parser creation failed");
96        do_parse_os_test_with_parser(&parser)
97    }
98
99    fn do_parse_os_test_with_parser(parser: &UserAgentParser) {
100        #[derive(Deserialize, Debug)]
101        struct OSTestCases<'a> {
102            test_cases: Vec<OSTestCase<'a>>,
103        }
104
105        #[derive(Deserialize, Debug)]
106        struct OSTestCase<'a> {
107            user_agent_string: Cow<'a, str>,
108            family: Cow<'a, str>,
109            major: Option<Cow<'a, str>>,
110            minor: Option<Cow<'a, str>>,
111            patch: Option<Cow<'a, str>>,
112            patch_minor: Option<Cow<'a, str>>,
113        }
114
115        let test_os = std::fs::File::open("./src/core/tests/test_os.yaml")
116            .expect("test_device.yaml failed to load");
117
118        let additional_os_tests =
119            std::fs::File::open("./src/core/test_resources/additional_os_tests.yaml")
120                .expect("additional_os_tests.yaml failed to load");
121
122        let test_cases: OSTestCases = serde_yaml::from_reader(test_os)
123            .expect("Failed to deserialize device test cases");
124
125        let additional_cases: OSTestCases = serde_yaml::from_reader(additional_os_tests)
126            .expect("Failed to deserialize additional test cases");
127
128        let mut total_passed = 0;
129        let mut failed = Vec::new();
130
131        for test_case in test_cases
132            .test_cases
133            .iter()
134            .chain(additional_cases.test_cases.iter())
135        {
136            let os = parser.parse_os(&test_case.user_agent_string);
137
138            if test_eq(&os, &test_case) {
139                total_passed += 1;
140            } else {
141                failed.push((os.clone(), test_case));
142            }
143        }
144
145        println!(
146            "parse_os - Test Summary: {} out of {} test cases passed",
147            total_passed,
148            total_passed + failed.len()
149        );
150
151        if !failed.is_empty() {
152            for fail in failed.iter() {
153                print_failure(&fail.0, &fail.1);
154            }
155        }
156
157        assert!(failed.is_empty());
158
159        fn test_eq(os: &OS, test_case: &OSTestCase) -> bool {
160            os.family == test_case.family
161                && os.major == test_case.major
162                && os.minor == test_case.minor
163                && os.patch == test_case.patch
164                && os.patch_minor == test_case.patch_minor
165        }
166    }
167
168    #[test]
169    fn parse_device_with_unicode() {
170        let parser = UserAgentParser::builder()
171            .with_unicode_support(true)
172            .build_from_yaml("./src/core/regexes.yaml")
173            .expect("Parser creation failed");
174        do_parse_device_test_with_parser(&parser)
175    }
176
177    #[test]
178    fn parse_device_without_unicode() {
179        let parser = UserAgentParser::builder()
180            .with_unicode_support(false)
181            .build_from_yaml("./src/core/regexes.yaml")
182            .expect("Parser creation failed");
183        do_parse_device_test_with_parser(&parser)
184    }
185
186    fn do_parse_device_test_with_parser(parser: &UserAgentParser) {
187        #[derive(Deserialize, Debug)]
188        struct DeviceTestCases<'a> {
189            test_cases: Vec<DeviceTestCase<'a>>,
190        }
191
192        #[derive(Deserialize, Debug)]
193        struct DeviceTestCase<'a> {
194            user_agent_string: Cow<'a, str>,
195            family: Cow<'a, str>,
196            brand: Option<Cow<'a, str>>,
197            model: Option<Cow<'a, str>>,
198        }
199
200        let file = std::fs::File::open("./src/core/tests/test_device.yaml")
201            .expect("test_device.yaml failed to load");
202
203        let test_cases: DeviceTestCases = serde_yaml::from_reader(file)
204            .expect("Failed to deserialize device test cases");
205
206        let mut total_passed = 0;
207        let mut failed = Vec::new();
208
209        for test_case in &test_cases.test_cases {
210            let dev = parser.parse_device(&test_case.user_agent_string);
211
212            if test_eq(&dev, &test_case) {
213                total_passed += 1;
214            } else {
215                failed.push((dev, test_case));
216            }
217        }
218
219        println!(
220            "parse_device - Test Summary: {} out of {} test cases passed",
221            total_passed,
222            total_passed + failed.len()
223        );
224
225        if !failed.is_empty() {
226            for fail in failed.iter() {
227                print_failure(&fail.0, &fail.1);
228            }
229        }
230
231        assert!(failed.is_empty());
232
233        fn test_eq(dev: &Device, test_case: &DeviceTestCase) -> bool {
234            dev.family == test_case.family
235                && dev.brand == test_case.brand
236                && dev.model == test_case.model
237        }
238    }
239
240    #[test]
241    fn parse_user_agent_with_unicode() {
242        let parser = UserAgentParser::builder()
243            .with_unicode_support(true)
244            .build_from_yaml("./src/core/regexes.yaml")
245            .expect("Parser creation failed");
246        do_parse_user_agent_test_with_parser(&parser)
247    }
248
249    #[test]
250    fn parse_user_agent_without_unicode() {
251        let parser = UserAgentParser::builder()
252            .with_unicode_support(false)
253            .build_from_yaml("./src/core/regexes.yaml")
254            .expect("Parser creation failed");
255        do_parse_user_agent_test_with_parser(&parser)
256    }
257    fn do_parse_user_agent_test_with_parser(parser: &UserAgentParser) {
258        #[derive(Deserialize, Debug)]
259        struct UserAgentTestCases<'a> {
260            test_cases: Vec<UserAgentTestCase<'a>>,
261        }
262
263        #[derive(Deserialize, Debug)]
264        struct UserAgentTestCase<'a> {
265            user_agent_string: Cow<'a, str>,
266            family: Cow<'a, str>,
267            major: Option<Cow<'a, str>>,
268            minor: Option<Cow<'a, str>>,
269            patch: Option<Cow<'a, str>>,
270        }
271
272        let test_ua = std::fs::File::open("./src/core/tests/test_ua.yaml")
273            .expect("test_device.yaml failed to load");
274
275        let firefox_user_agent_strings = std::fs::File::open(
276            "./src/core/test_resources/firefox_user_agent_strings.yaml",
277        )
278        .expect("firefox_user_agent_strings.yaml failed to load");
279
280        let opera_mini_user_agent_strings = std::fs::File::open(
281            "./src/core/test_resources/opera_mini_user_agent_strings.yaml",
282        )
283        .expect("opera_mini_user_agent_strings.yaml failed to open");
284
285        let test_cases: UserAgentTestCases = serde_yaml::from_reader(test_ua)
286            .expect("Failed to deserialize device test cases");
287
288        let firefox_user_agent_test_cases: UserAgentTestCases =
289            serde_yaml::from_reader(firefox_user_agent_strings)
290                .expect("Failed deserialize firefox test cases");
291
292        let opera_mini_test_cases: UserAgentTestCases =
293            serde_yaml::from_reader(opera_mini_user_agent_strings)
294                .expect("Failed to deserialized opera mini test cases");
295
296        let mut total_passed = 0;
297        let mut failed = Vec::new();
298
299        for test_case in test_cases
300            .test_cases
301            .iter()
302            .chain(firefox_user_agent_test_cases.test_cases.iter())
303            .chain(opera_mini_test_cases.test_cases.iter())
304        {
305            let ua = parser.parse_user_agent(&test_case.user_agent_string);
306
307            if test_eq(&ua, &test_case) {
308                total_passed += 1;
309            } else {
310                failed.push((ua, test_case));
311            }
312        }
313
314        println!(
315            "parse_user_agent - Test Summary: {} out of {} test cases passed",
316            total_passed,
317            total_passed + failed.len()
318        );
319
320        if !failed.is_empty() {
321            for fail in failed.iter() {
322                print_failure(&fail.0, &fail.1);
323            }
324        }
325
326        assert!(failed.is_empty());
327
328        fn test_eq(ua: &UserAgent, test_case: &UserAgentTestCase) -> bool {
329            ua.family == test_case.family
330                && ua.major == test_case.major
331                && ua.minor == test_case.minor
332                && ua.patch == test_case.patch
333        }
334    }
335
336    fn print_failure<T: Debug, F: Debug>(got: &T, expected: &F) {
337        println!(
338            r" --- Failed Test Case ----
339Expected {:?}
340Got {:?}
341",
342            expected, got
343        );
344    }
345}