├── .github ├── dependabot.yml └── workflows │ ├── ci-version.yml │ └── ci.yml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── basic.rs └── rocket.rs ├── rustfmt.toml ├── src ├── errors.rs ├── lib.rs ├── models │ ├── cpu.rs │ ├── device.rs │ ├── engine.rs │ ├── mod.rs │ ├── os.rs │ ├── product.rs │ └── user_agent.rs ├── regexes │ ├── cpu_regex.rs │ ├── device_regex.rs │ ├── engine_regex.rs │ ├── mod.rs │ ├── os_regex.rs │ └── product_regex.rs └── request_guards.rs └── tests └── uap_core.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/ci-version.yml: -------------------------------------------------------------------------------- 1 | name: CI-version 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | tests: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | - macos-latest 19 | toolchain: 20 | - stable 21 | - nightly 22 | features: 23 | - 24 | - --features rocket 25 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | submodules: recursive 31 | - uses: actions-rust-lang/setup-rust-toolchain@v1 32 | with: 33 | toolchain: ${{ matrix.toolchain }} 34 | - run: cargo test --release ${{ matrix.features }} 35 | - run: cargo doc --release ${{ matrix.features }} 36 | 37 | MSRV: 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | os: 42 | - ubuntu-latest 43 | - macos-latest 44 | toolchain: 45 | - 1.69 46 | features: 47 | - 48 | - --features rocket 49 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 50 | runs-on: ${{ matrix.os }} 51 | steps: 52 | - uses: actions/checkout@v4 53 | with: 54 | submodules: recursive 55 | - uses: actions-rust-lang/setup-rust-toolchain@v1 56 | with: 57 | toolchain: ${{ matrix.toolchain }} 58 | - run: cargo test --release --lib --bins ${{ matrix.features }} -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | rustfmt: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions-rust-lang/setup-rust-toolchain@v1 14 | with: 15 | toolchain: nightly 16 | components: rustfmt 17 | - uses: actions-rust-lang/rustfmt@v1 18 | 19 | clippy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions-rust-lang/setup-rust-toolchain@v1 24 | with: 25 | components: clippy 26 | - run: cargo clippy --all-targets --all-features -- -D warnings 27 | 28 | tests: 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | os: 33 | - ubuntu-latest 34 | - macos-latest 35 | toolchain: 36 | - stable 37 | - nightly 38 | features: 39 | - 40 | - --features rocket 41 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 42 | runs-on: ${{ matrix.os }} 43 | steps: 44 | - uses: actions/checkout@v4 45 | with: 46 | submodules: recursive 47 | - uses: actions-rust-lang/setup-rust-toolchain@v1 48 | with: 49 | toolchain: ${{ matrix.toolchain }} 50 | - run: cargo test ${{ matrix.features }} 51 | - run: cargo doc ${{ matrix.features }} 52 | 53 | MSRV: 54 | strategy: 55 | fail-fast: false 56 | matrix: 57 | os: 58 | - ubuntu-latest 59 | - macos-latest 60 | toolchain: 61 | - 1.69 62 | features: 63 | - 64 | - --features rocket 65 | name: Test ${{ matrix.toolchain }} on ${{ matrix.os }} (${{ matrix.features }}) 66 | runs-on: ${{ matrix.os }} 67 | steps: 68 | - uses: actions/checkout@v4 69 | with: 70 | submodules: recursive 71 | - uses: actions-rust-lang/setup-rust-toolchain@v1 72 | with: 73 | toolchain: ${{ matrix.toolchain }} 74 | - run: cargo test --lib --bins ${{ matrix.features }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/intellij+all 2 | 3 | ### Intellij+all ### 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | # User-specific stuff 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/**/usage.statistics.xml 11 | .idea/**/dictionaries 12 | .idea/**/shelf 13 | 14 | # Sensitive or high-churn files 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.ids 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | .idea/**/dbnavigator.xml 22 | 23 | # Gradle 24 | .idea/**/gradle.xml 25 | .idea/**/libraries 26 | 27 | # Gradle and Maven with auto-import 28 | # When using Gradle or Maven with auto-import, you should exclude module files, 29 | # since they will be recreated, and may cause churn. Uncomment if using 30 | # auto-import. 31 | # .idea/modules.xml 32 | # .idea/*.iml 33 | # .idea/modules 34 | 35 | # CMake 36 | cmake-build-*/ 37 | 38 | # Mongo Explorer plugin 39 | .idea/**/mongoSettings.xml 40 | 41 | # File-based project format 42 | *.iws 43 | 44 | # IntelliJ 45 | out/ 46 | 47 | # mpeltonen/sbt-idea plugin 48 | .idea_modules/ 49 | 50 | # JIRA plugin 51 | atlassian-ide-plugin.xml 52 | 53 | # Cursive Clojure plugin 54 | .idea/replstate.xml 55 | 56 | # Crashlytics plugin (for Android Studio and IntelliJ) 57 | com_crashlytics_export_strings.xml 58 | crashlytics.properties 59 | crashlytics-build.properties 60 | fabric.properties 61 | 62 | # Editor-based Rest Client 63 | .idea/httpRequests 64 | 65 | ### Intellij+all Patch ### 66 | # Ignores the whole .idea folder and all .iml files 67 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 68 | 69 | .idea/ 70 | 71 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 72 | 73 | *.iml 74 | modules.xml 75 | .idea/misc.xml 76 | *.ipr 77 | 78 | 79 | # End of https://www.gitignore.io/api/intellij+all 80 | 81 | 82 | ### Rust ### 83 | # Generated by Cargo 84 | # will have compiled files and executables 85 | /target/ 86 | 87 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 88 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 89 | Cargo.lock 90 | 91 | # These are backup files generated by rustfmt 92 | **/*.rs.bk 93 | 94 | 95 | # End of https://www.gitignore.io/api/rust 96 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "uap-core"] 2 | path = uap-core 3 | url = git@github.com:ua-parser/uap-core.git 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "user-agent-parser" 3 | version = "0.3.6" 4 | authors = ["Magic Len "] 5 | edition = "2021" 6 | rust-version = "1.69" 7 | repository = "https://github.com/magiclen/user-agent-parser" 8 | homepage = "https://magiclen.org/user-agent-parser" 9 | keywords = ["useragent", "user-agent", "uap", "rocket", "uap-core"] 10 | categories = ["parser-implementations"] 11 | description = "A 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" 12 | license = "MIT" 13 | include = ["src/**/*", "Cargo.toml", "README.md", "LICENSE", "examples/rocket.rs"] 14 | 15 | [dependencies] 16 | yaml-rust = "0.4" 17 | onig = { version = "6", default-features = false } 18 | rocket = { version = "0.5.0-rc.4", optional = true } 19 | 20 | [[example]] 21 | name = "rocket" 22 | required-features = ["rocket"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 magiclen.org (Ron Li) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | User Agent Parser 2 | ==================== 3 | 4 | [![CI](https://github.com/magiclen/user-agent-parser/actions/workflows/ci.yml/badge.svg)](https://github.com/magiclen/user-agent-parser/actions/workflows/ci.yml) 5 | 6 | A 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 7 | 8 | ## Usage 9 | 10 | You can make a **regexes.yaml** file or copy one from https://github.com/ua-parser/uap-core 11 | 12 | This is a simple example of **regexes.yaml**. 13 | 14 | ```yaml 15 | user_agent_parsers: 16 | - regex: '(ESPN)[%20| ]+Radio/(\d+)\.(\d+)\.(\d+) CFNetwork' 17 | - regex: '(Namoroka|Shiretoko|Minefield)/(\d+)\.(\d+)\.(\d+(?:pre|))' 18 | family_replacement: 'Firefox ($1)' 19 | - regex: '(Android) Eclair' 20 | v1_replacement: '2' 21 | v2_replacement: '1' 22 | 23 | os_parsers: 24 | - regex: 'Win(?:dows)? ?(95|98|3.1|NT|ME|2000|XP|Vista|7|CE)' 25 | os_replacement: 'Windows' 26 | os_v1_replacement: '$1' 27 | 28 | device_parsers: 29 | - regex: '\bSmartWatch *\( *([^;]+) *; *([^;]+) *;' 30 | device_replacement: '$1 $2' 31 | brand_replacement: '$1' 32 | model_replacement: '$2' 33 | ``` 34 | 35 | Then, use the `from_path` (or `from_str` if your YAML data is in-memory) associated function to create a `UserAgentParser` instance. 36 | 37 | 38 | ```rust 39 | use user_agent_parser::UserAgentParser; 40 | 41 | let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap(); 42 | ``` 43 | 44 | Use the `parse_*` methods and input a user-agent string to get information. 45 | 46 | ```rust 47 | use user_agent_parser::UserAgentParser; 48 | 49 | let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap(); 50 | 51 | let 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]"; 52 | 53 | let product = ua_parser.parse_product(user_agent); 54 | 55 | println!("{:#?}", product); 56 | 57 | // Product { 58 | // name: Some( 59 | // "Facebook", 60 | // ), 61 | // major: Some( 62 | // "8", 63 | // ), 64 | // minor: Some( 65 | // "0", 66 | // ), 67 | // patch: Some( 68 | // "0", 69 | // ), 70 | // } 71 | 72 | let os = ua_parser.parse_os(user_agent); 73 | 74 | println!("{:#?}", os); 75 | 76 | // OS { 77 | // name: Some( 78 | // "iOS", 79 | // ), 80 | // major: None, 81 | // minor: None, 82 | // patch: None, 83 | // patch_minor: None, 84 | // } 85 | 86 | let device = ua_parser.parse_device(user_agent); 87 | 88 | println!("{:#?}", device); 89 | 90 | // Device { 91 | // name: Some( 92 | // "iPhone", 93 | // ), 94 | // brand: Some( 95 | // "Apple", 96 | // ), 97 | // model: Some( 98 | // "iPhone4,1", 99 | // ), 100 | // } 101 | 102 | let cpu = ua_parser.parse_cpu(user_agent); 103 | 104 | println!("{:#?}", cpu); 105 | 106 | // CPU { 107 | // architecture: Some( 108 | // "amd64", 109 | // ), 110 | // } 111 | 112 | let engine = ua_parser.parse_engine(user_agent); 113 | 114 | println!("{:#?}", engine); 115 | 116 | // Engine { 117 | // name: Some( 118 | // "Gecko", 119 | // ), 120 | // major: Some( 121 | // "10", 122 | // ), 123 | // minor: Some( 124 | // "0", 125 | // ), 126 | // patch: None, 127 | // } 128 | ``` 129 | 130 | The 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. 131 | 132 | ```rust 133 | use user_agent_parser::UserAgentParser; 134 | 135 | let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap(); 136 | 137 | let 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(); 138 | ``` 139 | 140 | ## Rocket Support 141 | 142 | This crate supports the Rocket framework. All you have to do is enabling the `rocket` feature for this crate. 143 | 144 | ```toml 145 | [dependencies.user-agent-parser] 146 | version = "*" 147 | features = ["rocket"] 148 | ``` 149 | 150 | Let `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*. 151 | 152 | ```rust 153 | #[macro_use] 154 | extern crate rocket; 155 | 156 | use user_agent_parser::{UserAgentParser, UserAgent, Product, OS, Device, CPU, Engine}; 157 | 158 | #[get("/")] 159 | fn 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] 171 | fn rocket() -> _ { 172 | rocket::build() 173 | .manage(UserAgentParser::from_path("/path/to/regexes.yaml").unwrap()) 174 | .mount("/", routes![index]) 175 | } 176 | ``` 177 | 178 | ## Testing 179 | 180 | ```bash 181 | # git clone --recurse-submodules git://github.com/magiclen/user-agent-parser.git 182 | 183 | git clone git://github.com/magiclen/user-agent-parser.git 184 | 185 | cd user-agent-parser 186 | 187 | git submodule init 188 | git submodule update --recursive 189 | 190 | cargo test 191 | ``` 192 | 193 | ## Crates.io 194 | 195 | https://crates.io/crates/user-agent-parser 196 | 197 | ## Documentation 198 | 199 | https://docs.rs/user-agent-parser 200 | 201 | ## License 202 | 203 | [MIT](LICENSE) -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use user_agent_parser::UserAgentParser; 2 | 3 | fn main() { 4 | let ua_parser = UserAgentParser::from_path("uap-core/regexes.yaml").unwrap(); 5 | 6 | let user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 \ 7 | [FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/\ 8 | iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5]"; 9 | 10 | let product = ua_parser.parse_product(user_agent); 11 | 12 | println!("{:#?}", product); 13 | 14 | // Product { 15 | // name: Some( 16 | // "Facebook", 17 | // ), 18 | // major: Some( 19 | // "8", 20 | // ), 21 | // minor: Some( 22 | // "0", 23 | // ), 24 | // patch: Some( 25 | // "0", 26 | // ), 27 | // } 28 | 29 | let os = ua_parser.parse_os(user_agent); 30 | 31 | println!("{:#?}", os); 32 | 33 | // OS { 34 | // name: Some( 35 | // "iOS", 36 | // ), 37 | // major: None, 38 | // minor: None, 39 | // patch: None, 40 | // patch_minor: None, 41 | // } 42 | 43 | let device = ua_parser.parse_device(user_agent); 44 | 45 | println!("{:#?}", device); 46 | 47 | // Device { 48 | // name: Some( 49 | // "iPhone", 50 | // ), 51 | // brand: Some( 52 | // "Apple", 53 | // ), 54 | // model: Some( 55 | // "iPhone4,1", 56 | // ), 57 | // } 58 | 59 | let cpu = ua_parser.parse_cpu(user_agent); 60 | 61 | println!("{:#?}", cpu); 62 | 63 | // CPU { 64 | // architecture: Some( 65 | // "amd64", 66 | // ), 67 | // } 68 | 69 | let engine = ua_parser.parse_engine(user_agent); 70 | 71 | println!("{:#?}", engine); 72 | 73 | // Engine { 74 | // name: Some( 75 | // "Gecko", 76 | // ), 77 | // major: Some( 78 | // "10", 79 | // ), 80 | // minor: Some( 81 | // "0", 82 | // ), 83 | // patch: None, 84 | // } 85 | } 86 | -------------------------------------------------------------------------------- /examples/rocket.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate rocket; 3 | 4 | use user_agent_parser::{Device, Engine, Product, UserAgent, UserAgentParser, CPU, OS}; 5 | 6 | #[get("/")] 7 | fn index( 8 | user_agent: UserAgent, 9 | product: Product, 10 | os: OS, 11 | device: Device, 12 | cpu: CPU, 13 | engine: Engine, 14 | ) -> String { 15 | format!( 16 | "{user_agent:#?}\n{product:#?}\n{os:#?}\n{device:#?}\n{cpu:#?}\n{engine:#?}", 17 | user_agent = user_agent, 18 | product = product, 19 | os = os, 20 | device = device, 21 | cpu = cpu, 22 | engine = engine, 23 | ) 24 | } 25 | 26 | #[launch] 27 | fn rocket() -> _ { 28 | rocket::build() 29 | .manage(UserAgentParser::from_path("uap-core/regexes.yaml").unwrap()) 30 | .mount("/", routes![index]) 31 | } 32 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # array_width = 60 2 | # attr_fn_like_width = 70 3 | binop_separator = "Front" 4 | blank_lines_lower_bound = 0 5 | blank_lines_upper_bound = 1 6 | brace_style = "PreferSameLine" 7 | # chain_width = 60 8 | color = "Auto" 9 | # comment_width = 100 10 | condense_wildcard_suffixes = true 11 | control_brace_style = "AlwaysSameLine" 12 | empty_item_single_line = true 13 | enum_discrim_align_threshold = 80 14 | error_on_line_overflow = false 15 | error_on_unformatted = false 16 | # fn_call_width = 60 17 | fn_params_layout = "Tall" 18 | fn_single_line = false 19 | force_explicit_abi = true 20 | force_multiline_blocks = false 21 | format_code_in_doc_comments = true 22 | doc_comment_code_block_width = 80 23 | format_generated_files = true 24 | format_macro_matchers = true 25 | format_macro_bodies = true 26 | skip_macro_invocations = [] 27 | format_strings = true 28 | hard_tabs = false 29 | hex_literal_case = "Upper" 30 | imports_indent = "Block" 31 | imports_layout = "Mixed" 32 | indent_style = "Block" 33 | inline_attribute_width = 0 34 | match_arm_blocks = true 35 | match_arm_leading_pipes = "Never" 36 | match_block_trailing_comma = true 37 | max_width = 100 38 | merge_derives = true 39 | imports_granularity = "Crate" 40 | newline_style = "Unix" 41 | normalize_comments = false 42 | normalize_doc_attributes = true 43 | overflow_delimited_expr = true 44 | remove_nested_parens = true 45 | reorder_impl_items = true 46 | reorder_imports = true 47 | group_imports = "StdExternalCrate" 48 | reorder_modules = true 49 | short_array_element_width_threshold = 10 50 | # single_line_if_else_max_width = 50 51 | space_after_colon = true 52 | space_before_colon = false 53 | spaces_around_ranges = false 54 | struct_field_align_threshold = 80 55 | struct_lit_single_line = false 56 | # struct_lit_width = 18 57 | # struct_variant_width = 35 58 | tab_spaces = 4 59 | trailing_comma = "Vertical" 60 | trailing_semicolon = true 61 | type_punctuation_density = "Wide" 62 | use_field_init_shorthand = true 63 | use_small_heuristics = "Max" 64 | use_try_shorthand = true 65 | where_single_line = false 66 | wrap_comments = false -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | fmt::{Display, Error as FmtError, Formatter}, 4 | io::Error as IOError, 5 | }; 6 | 7 | use onig::Error as RegexError; 8 | use yaml_rust::ScanError; 9 | 10 | #[derive(Debug)] 11 | /// Possible errors of `UserAgentParser`. 12 | pub enum UserAgentParserError { 13 | ScanError(ScanError), 14 | IOError(IOError), 15 | RegexError(RegexError), 16 | IncorrectSource, 17 | } 18 | 19 | impl Display for UserAgentParserError { 20 | #[inline] 21 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> { 22 | match self { 23 | UserAgentParserError::ScanError(err) => Display::fmt(&err, f), 24 | UserAgentParserError::IOError(err) => Display::fmt(&err, f), 25 | UserAgentParserError::RegexError(err) => Display::fmt(&err, f), 26 | UserAgentParserError::IncorrectSource => { 27 | f.write_str("The source of regular expressions is incorrect.") 28 | }, 29 | } 30 | } 31 | } 32 | 33 | impl Error for UserAgentParserError {} 34 | 35 | impl From for UserAgentParserError { 36 | #[inline] 37 | fn from(error: ScanError) -> UserAgentParserError { 38 | UserAgentParserError::ScanError(error) 39 | } 40 | } 41 | 42 | impl From for UserAgentParserError { 43 | #[inline] 44 | fn from(error: IOError) -> UserAgentParserError { 45 | UserAgentParserError::IOError(error) 46 | } 47 | } 48 | 49 | impl From for UserAgentParserError { 50 | #[inline] 51 | fn from(error: RegexError) -> UserAgentParserError { 52 | UserAgentParserError::RegexError(error) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | # User Agent Parser 3 | 4 | A 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 | 8 | You can make a **regexes.yaml** file or copy one from https://github.com/ua-parser/uap-core 9 | 10 | This is a simple example of **regexes.yaml**. 11 | 12 | ```yaml 13 | user_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 | 21 | os_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 | 26 | device_parsers: 27 | - regex: '\bSmartWatch *\( *([^;]+) *; *([^;]+) *;' 28 | device_replacement: '$1 $2' 29 | brand_replacement: '$1' 30 | model_replacement: '$2' 31 | ``` 32 | 33 | Then, 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,ignore 37 | use user_agent_parser::UserAgentParser; 38 | 39 | let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap(); 40 | ``` 41 | 42 | Use the `parse_*` methods and input a user-agent string to get information. 43 | 44 | ```rust,ignore 45 | use user_agent_parser::UserAgentParser; 46 | 47 | let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap(); 48 | 49 | let 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 | 51 | let product = ua_parser.parse_product(user_agent); 52 | 53 | println!("{:#?}", 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 | 70 | let os = ua_parser.parse_os(user_agent); 71 | 72 | println!("{:#?}", 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 | 84 | let device = ua_parser.parse_device(user_agent); 85 | 86 | println!("{:#?}", 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 | 100 | let cpu = ua_parser.parse_cpu(user_agent); 101 | 102 | println!("{:#?}", cpu); 103 | 104 | // CPU { 105 | // architecture: Some( 106 | // "amd64", 107 | // ), 108 | // } 109 | 110 | let engine = ua_parser.parse_engine(user_agent); 111 | 112 | println!("{:#?}", 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 | 128 | The 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,ignore 131 | use user_agent_parser::UserAgentParser; 132 | 133 | let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap(); 134 | 135 | let 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 | 140 | This 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] 144 | version = "*" 145 | features = ["rocket"] 146 | ``` 147 | 148 | Let `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 | 150 | ```rust,ignore 151 | #[macro_use] 152 | extern crate rocket; 153 | 154 | use user_agent_parser::{UserAgentParser, UserAgent, Product, OS, Device, CPU, Engine}; 155 | 156 | #[get("/")] 157 | fn index(user_agent: UserAgent, product: Product, os: OS, device: Device, cpu: CPU, engine: Engine) -> String { 158 | format!("{user_agent:#?}\n{product:#?}\n{os:#?}\n{device:#?}\n{cpu:#?}\n{engine:#?}", 159 | user_agent = user_agent, 160 | product = product, 161 | os = os, 162 | device = device, 163 | cpu = cpu, 164 | engine = engine, 165 | ) 166 | } 167 | 168 | #[launch] 169 | fn rocket() -> _ { 170 | rocket::build() 171 | .manage(UserAgentParser::from_path("/path/to/regexes.yaml").unwrap()) 172 | .mount("/", routes![index]) 173 | } 174 | ``` 175 | 176 | ## Testing 177 | 178 | ```bash 179 | # git clone --recurse-submodules git://github.com/magiclen/user-agent-parser.git 180 | 181 | git clone git://github.com/magiclen/user-agent-parser.git 182 | 183 | cd user-agent-parser 184 | 185 | git submodule init 186 | git submodule update --recursive 187 | 188 | cargo test 189 | ``` 190 | */ 191 | 192 | mod errors; 193 | mod models; 194 | mod regexes; 195 | 196 | #[cfg(feature = "rocket")] 197 | mod request_guards; 198 | 199 | use std::{borrow::Cow, fs, path::Path, str::FromStr}; 200 | 201 | pub use errors::UserAgentParserError; 202 | pub use models::*; 203 | use onig::Regex; 204 | use regexes::*; 205 | use yaml_rust::{Yaml, YamlLoader}; 206 | 207 | #[derive(Debug)] 208 | pub struct UserAgentParser { 209 | replacement_regex: Regex, 210 | product_regexes: Vec, 211 | os_regexes: Vec, 212 | device_regexes: Vec, 213 | cpu_regexes: Vec, 214 | engine_regexes: Vec, 215 | } 216 | 217 | impl UserAgentParser { 218 | /// Read the list of regular expressions (YAML data) from a file to create a `UserAgentParser` instance. 219 | #[inline] 220 | pub fn from_path>(path: P) -> Result { 221 | let yaml = fs::read_to_string(path)?; 222 | 223 | Self::from_str(yaml) 224 | } 225 | 226 | /// Read the list of regular expressions (YAML data) from a string to create a `UserAgentParser` instance. 227 | #[allow(clippy::should_implement_trait)] 228 | pub fn from_str>(yaml: S) -> Result { 229 | let yamls = YamlLoader::load_from_str(yaml.as_ref())?; 230 | 231 | if yamls.is_empty() { 232 | Err(UserAgentParserError::IncorrectSource) 233 | } else { 234 | let yaml = &yamls[0]; 235 | 236 | match yaml.as_hash() { 237 | Some(yaml) => { 238 | let user_agent_parsers = 239 | yaml.get(&Yaml::String("user_agent_parsers".to_string())); 240 | let os_parsers = yaml.get(&Yaml::String("os_parsers".to_string())); 241 | let device_parsers = yaml.get(&Yaml::String("device_parsers".to_string())); 242 | 243 | let user_agent_regexes = match user_agent_parsers { 244 | Some(user_agent_parsers) => ProductRegex::from_yaml(user_agent_parsers)?, 245 | None => Vec::new(), 246 | }; 247 | 248 | let os_regexes = match os_parsers { 249 | Some(os_parsers) => OSRegex::from_yaml(os_parsers)?, 250 | None => Vec::new(), 251 | }; 252 | 253 | let device_regexes = match device_parsers { 254 | Some(device_parsers) => DeviceRegex::from_yaml(device_parsers)?, 255 | None => Vec::new(), 256 | }; 257 | 258 | Ok(UserAgentParser { 259 | replacement_regex: Regex::new(r"\$(\d){1,9}").unwrap(), 260 | product_regexes: user_agent_regexes, 261 | os_regexes, 262 | device_regexes, 263 | cpu_regexes: CPURegex::built_in_regexes(), 264 | engine_regexes: EngineRegex::built_in_regexes(), 265 | }) 266 | }, 267 | None => Err(UserAgentParserError::IncorrectSource), 268 | } 269 | } 270 | } 271 | } 272 | 273 | macro_rules! get_string { 274 | ($index:expr, $replacement:expr, $replacement_regex:expr, $captures:expr) => { 275 | match $replacement.as_ref() { 276 | Some(replacement) => { 277 | let replacement_captures_vec: Vec<_> = 278 | $replacement_regex.captures_iter(replacement).collect(); 279 | 280 | if replacement_captures_vec.is_empty() { 281 | Some(Cow::from(replacement)) 282 | } else { 283 | let mut replacement = replacement.to_string(); 284 | 285 | let captures_len = $captures.len(); 286 | 287 | for replacement_captures in replacement_captures_vec.into_iter().rev() { 288 | let index = replacement_captures.at(1).unwrap().parse::().unwrap(); 289 | 290 | let pos = replacement_captures.pos(0).unwrap(); 291 | 292 | if index < captures_len { 293 | replacement.replace_range( 294 | pos.0..pos.1, 295 | $captures.at(index).unwrap_or_default(), 296 | ); 297 | } else { 298 | replacement.replace_range(pos.0..pos.1, ""); 299 | } 300 | } 301 | 302 | let start_trimmed_replacement = replacement.trim_start(); 303 | 304 | if start_trimmed_replacement.len() != replacement.len() { 305 | replacement = start_trimmed_replacement.trim_end().to_string(); 306 | } else { 307 | replacement.truncate(replacement.trim_end().len()); 308 | } 309 | 310 | if replacement.is_empty() { 311 | None 312 | } else { 313 | Some(Cow::from(replacement)) 314 | } 315 | } 316 | }, 317 | None => match $captures.at($index) { 318 | Some(s) => { 319 | let s = s.trim(); 320 | 321 | if s.is_empty() { 322 | None 323 | } else { 324 | Some(Cow::from(s)) 325 | } 326 | }, 327 | None => None, 328 | }, 329 | } 330 | }; 331 | 332 | ($index:expr, $captures:expr) => { 333 | match $captures.at($index) { 334 | Some(s) => { 335 | let s = s.trim(); 336 | 337 | if s.is_empty() { 338 | None 339 | } else { 340 | Some(Cow::from(s)) 341 | } 342 | }, 343 | None => None, 344 | } 345 | }; 346 | } 347 | 348 | impl UserAgentParser { 349 | pub fn parse_product<'a, S: AsRef + ?Sized>(&'a self, user_agent: &'a S) -> Product<'a> { 350 | let mut product = Product::default(); 351 | 352 | for product_regex in self.product_regexes.iter() { 353 | if let Some(captures) = product_regex.regex.captures(user_agent.as_ref()) { 354 | product.name = get_string!( 355 | 1, 356 | product_regex.family_replacement, 357 | self.replacement_regex, 358 | captures 359 | ); 360 | product.major = 361 | get_string!(2, product_regex.v1_replacement, self.replacement_regex, captures); 362 | product.minor = 363 | get_string!(3, product_regex.v2_replacement, self.replacement_regex, captures); 364 | product.patch = 365 | get_string!(4, product_regex.v3_replacement, self.replacement_regex, captures); 366 | 367 | break; 368 | } 369 | } 370 | 371 | if product.name.is_none() { 372 | product.name = Some(Cow::from("Other")); 373 | } 374 | 375 | product 376 | } 377 | 378 | pub fn parse_os<'a, S: AsRef + ?Sized>(&'a self, user_agent: &'a S) -> OS<'a> { 379 | let mut os = OS::default(); 380 | 381 | for os_regex in self.os_regexes.iter() { 382 | if let Some(captures) = os_regex.regex.captures(user_agent.as_ref()) { 383 | os.name = get_string!(1, os_regex.os_replacement, self.replacement_regex, captures); 384 | os.major = 385 | get_string!(2, os_regex.os_v1_replacement, self.replacement_regex, captures); 386 | os.minor = 387 | get_string!(3, os_regex.os_v2_replacement, self.replacement_regex, captures); 388 | os.patch = 389 | get_string!(4, os_regex.os_v3_replacement, self.replacement_regex, captures); 390 | os.patch_minor = 391 | get_string!(5, os_regex.os_v4_replacement, self.replacement_regex, captures); 392 | 393 | break; 394 | } 395 | } 396 | 397 | if os.name.is_none() { 398 | os.name = Some(Cow::from("Other")); 399 | } 400 | 401 | os 402 | } 403 | 404 | pub fn parse_device<'a, S: AsRef + ?Sized>(&'a self, user_agent: &'a S) -> Device<'a> { 405 | let mut device = Device::default(); 406 | 407 | for device_regex in self.device_regexes.iter() { 408 | if let Some(captures) = device_regex.regex.captures(user_agent.as_ref()) { 409 | device.name = get_string!( 410 | 1, 411 | device_regex.device_replacement, 412 | self.replacement_regex, 413 | captures 414 | ); 415 | device.brand = get_string!( 416 | 2, 417 | device_regex.brand_replacement, 418 | self.replacement_regex, 419 | captures 420 | ); 421 | device.model = get_string!( 422 | 1, 423 | device_regex.model_replacement, 424 | self.replacement_regex, 425 | captures 426 | ); 427 | 428 | break; 429 | } 430 | } 431 | 432 | if device.name.is_none() { 433 | device.name = Some(Cow::from("Other")); 434 | } 435 | 436 | device 437 | } 438 | 439 | pub fn parse_cpu<'a, S: AsRef + ?Sized>(&'a self, user_agent: &'a S) -> CPU<'a> { 440 | let mut cpu = CPU::default(); 441 | 442 | for cpu_regex in self.cpu_regexes.iter() { 443 | if let Some(captures) = cpu_regex.regex.captures(user_agent.as_ref()) { 444 | cpu.architecture = get_string!( 445 | 1, 446 | cpu_regex.architecture_replacement, 447 | self.replacement_regex, 448 | captures 449 | ); 450 | 451 | break; 452 | } 453 | } 454 | 455 | cpu 456 | } 457 | 458 | pub fn parse_engine<'a, S: AsRef + ?Sized>(&'a self, user_agent: &'a S) -> Engine<'a> { 459 | let mut engine = Engine::default(); 460 | 461 | for engine_regex in self.engine_regexes.iter() { 462 | if let Some(captures) = engine_regex.regex.captures(user_agent.as_ref()) { 463 | engine.name = 464 | get_string!(1, engine_regex.name_replacement, self.replacement_regex, captures); 465 | engine.major = get_string!( 466 | 2, 467 | engine_regex.engine_v1_replacement, 468 | self.replacement_regex, 469 | captures 470 | ); 471 | engine.minor = get_string!( 472 | 3, 473 | engine_regex.engine_v2_replacement, 474 | self.replacement_regex, 475 | captures 476 | ); 477 | engine.patch = get_string!( 478 | 4, 479 | engine_regex.engine_v3_replacement, 480 | self.replacement_regex, 481 | captures 482 | ); 483 | 484 | break; 485 | } 486 | } 487 | 488 | engine 489 | } 490 | } 491 | 492 | impl FromStr for UserAgentParser { 493 | type Err = UserAgentParserError; 494 | 495 | #[inline] 496 | fn from_str(s: &str) -> Result { 497 | UserAgentParser::from_str(s) 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /src/models/cpu.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | #[derive(Debug, Clone, Default)] 4 | #[allow(clippy::upper_case_acronyms)] 5 | pub struct CPU<'a> { 6 | pub architecture: Option>, 7 | } 8 | 9 | impl<'a> CPU<'a> { 10 | /// Extracts the owned data. 11 | #[inline] 12 | pub fn into_owned(self) -> CPU<'static> { 13 | let architecture = self.architecture.map(|c| Cow::from(c.into_owned())); 14 | 15 | CPU { 16 | architecture, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/models/device.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | #[derive(Debug, Clone, Default)] 4 | pub struct Device<'a> { 5 | pub name: Option>, 6 | pub brand: Option>, 7 | pub model: Option>, 8 | } 9 | 10 | impl<'a> Device<'a> { 11 | /// Extracts the owned data. 12 | #[inline] 13 | pub fn into_owned(self) -> Device<'static> { 14 | let device = self.name.map(|c| Cow::from(c.into_owned())); 15 | let brand = self.brand.map(|c| Cow::from(c.into_owned())); 16 | let model = self.model.map(|c| Cow::from(c.into_owned())); 17 | 18 | Device { 19 | name: device, 20 | brand, 21 | model, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/models/engine.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | #[derive(Debug, Clone, Default)] 4 | pub struct Engine<'a> { 5 | pub name: Option>, 6 | pub major: Option>, 7 | pub minor: Option>, 8 | pub patch: Option>, 9 | } 10 | 11 | impl<'a> Engine<'a> { 12 | /// Extracts the owned data. 13 | #[inline] 14 | pub fn into_owned(self) -> Engine<'static> { 15 | let name = self.name.map(|c| Cow::from(c.into_owned())); 16 | let major = self.major.map(|c| Cow::from(c.into_owned())); 17 | let minor = self.minor.map(|c| Cow::from(c.into_owned())); 18 | let patch = self.patch.map(|c| Cow::from(c.into_owned())); 19 | 20 | Engine { 21 | name, 22 | major, 23 | minor, 24 | patch, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | mod cpu; 2 | mod device; 3 | mod engine; 4 | mod os; 5 | mod product; 6 | 7 | #[cfg(feature = "rocket")] 8 | mod user_agent; 9 | 10 | pub use cpu::CPU; 11 | pub use device::Device; 12 | pub use engine::Engine; 13 | pub use os::OS; 14 | pub use product::Product; 15 | #[cfg(feature = "rocket")] 16 | pub use user_agent::UserAgent; 17 | -------------------------------------------------------------------------------- /src/models/os.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | #[derive(Debug, Clone, Default)] 4 | pub struct OS<'a> { 5 | pub name: Option>, 6 | pub major: Option>, 7 | pub minor: Option>, 8 | pub patch: Option>, 9 | pub patch_minor: Option>, 10 | } 11 | 12 | impl<'a> OS<'a> { 13 | /// Extracts the owned data. 14 | #[inline] 15 | pub fn into_owned(self) -> OS<'static> { 16 | let os = self.name.map(|c| Cow::from(c.into_owned())); 17 | let major = self.major.map(|c| Cow::from(c.into_owned())); 18 | let minor = self.minor.map(|c| Cow::from(c.into_owned())); 19 | let patch = self.patch.map(|c| Cow::from(c.into_owned())); 20 | let patch_minor = self.patch_minor.map(|c| Cow::from(c.into_owned())); 21 | 22 | OS { 23 | name: os, 24 | major, 25 | minor, 26 | patch, 27 | patch_minor, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/models/product.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | #[derive(Debug, Clone, Default)] 4 | pub struct Product<'a> { 5 | pub name: Option>, 6 | pub major: Option>, 7 | pub minor: Option>, 8 | pub patch: Option>, 9 | } 10 | 11 | impl<'a> Product<'a> { 12 | /// Extracts the owned data. 13 | #[inline] 14 | pub fn into_owned(self) -> Product<'static> { 15 | let product = self.name.map(|c| Cow::from(c.into_owned())); 16 | let major = self.major.map(|c| Cow::from(c.into_owned())); 17 | let minor = self.minor.map(|c| Cow::from(c.into_owned())); 18 | let patch = self.patch.map(|c| Cow::from(c.into_owned())); 19 | 20 | Product { 21 | name: product, 22 | major, 23 | minor, 24 | patch, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/models/user_agent.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | #[derive(Debug, Clone, Default)] 4 | pub struct UserAgent<'a> { 5 | pub user_agent: Option>, 6 | } 7 | 8 | impl<'a> UserAgent<'a> { 9 | /// Extracts the owned data. 10 | #[inline] 11 | pub fn into_owned(self) -> UserAgent<'static> { 12 | let user_agent = self.user_agent.map(|c| Cow::from(c.into_owned())); 13 | 14 | UserAgent { 15 | user_agent, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/regexes/cpu_regex.rs: -------------------------------------------------------------------------------- 1 | use onig::Regex; 2 | 3 | #[derive(Debug)] 4 | pub struct CPURegex { 5 | pub(crate) regex: Regex, 6 | pub(crate) architecture_replacement: Option, 7 | } 8 | 9 | impl CPURegex { 10 | pub fn built_in_regexes() -> Vec { 11 | vec![ 12 | { 13 | let regex = Regex::new(r"(?i)(?:(amd|x(?:(?:86|64)[_-])?|wow|win)64)[;)]").unwrap(); 14 | 15 | CPURegex { 16 | regex, 17 | architecture_replacement: Some("amd64".to_string()), 18 | } 19 | }, 20 | { 21 | let regex = Regex::new(r"(?i)(ia32(?=;))").unwrap(); 22 | 23 | CPURegex { 24 | regex, 25 | architecture_replacement: Some("ia32".to_string()), 26 | } 27 | }, 28 | { 29 | let regex = Regex::new(r"(?i)((?:i[346]|x)86)[;)]").unwrap(); 30 | 31 | CPURegex { 32 | regex, 33 | architecture_replacement: Some("ia32".to_string()), 34 | } 35 | }, 36 | { 37 | let regex = Regex::new(r"(?i)windows\s(ce|mobile);\sppc;").unwrap(); 38 | 39 | CPURegex { 40 | regex, 41 | architecture_replacement: Some("arm".to_string()), 42 | } 43 | }, 44 | { 45 | let regex = Regex::new(r"(?i)((?:ppc|powerpc)(?:64)?)(?:\smac|;|\))").unwrap(); 46 | 47 | CPURegex { 48 | regex, 49 | architecture_replacement: Some("ppc".to_string()), 50 | } 51 | }, 52 | { 53 | let regex = Regex::new(r"(sun4\w)[;)]").unwrap(); 54 | 55 | CPURegex { 56 | regex, 57 | architecture_replacement: Some("sparc".to_string()), 58 | } 59 | }, 60 | { 61 | let regex = Regex::new(r"(?i)(?:ia64;)").unwrap(); 62 | 63 | CPURegex { 64 | regex, 65 | architecture_replacement: Some("ia64".to_string()), 66 | } 67 | }, 68 | { 69 | let regex = Regex::new(r"(?i)(?:68k\))").unwrap(); 70 | 71 | CPURegex { 72 | regex, 73 | architecture_replacement: Some("68k".to_string()), 74 | } 75 | }, 76 | { 77 | let regex = Regex::new(r"(?i)arm(?:64|(?=v\d+[;l]))").unwrap(); 78 | 79 | CPURegex { 80 | regex, 81 | architecture_replacement: Some("arm".to_string()), 82 | } 83 | }, 84 | { 85 | let regex = Regex::new(r"(?i)(?=atmel\s)avr").unwrap(); 86 | 87 | CPURegex { 88 | regex, 89 | architecture_replacement: Some("avr".to_string()), 90 | } 91 | }, 92 | { 93 | let regex = Regex::new(r"(?i)irix(?:64)?").unwrap(); 94 | 95 | CPURegex { 96 | regex, 97 | architecture_replacement: Some("irix".to_string()), 98 | } 99 | }, 100 | { 101 | let regex = Regex::new(r"(?i)mips(?:64)?").unwrap(); 102 | 103 | CPURegex { 104 | regex, 105 | architecture_replacement: Some("mips".to_string()), 106 | } 107 | }, 108 | { 109 | let regex = Regex::new(r"(?i)sparc(?:64)?").unwrap(); 110 | 111 | CPURegex { 112 | regex, 113 | architecture_replacement: Some("sparc".to_string()), 114 | } 115 | }, 116 | { 117 | let regex = Regex::new(r"(?i)pa-risc").unwrap(); 118 | 119 | CPURegex { 120 | regex, 121 | architecture_replacement: Some("pa-risc".to_string()), 122 | } 123 | }, 124 | ] 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/regexes/device_regex.rs: -------------------------------------------------------------------------------- 1 | use onig::{Regex, RegexOptions, Syntax}; 2 | use yaml_rust::Yaml; 3 | 4 | use crate::UserAgentParserError; 5 | 6 | #[derive(Debug)] 7 | pub struct DeviceRegex { 8 | pub(crate) regex: Regex, 9 | pub(crate) device_replacement: Option, 10 | pub(crate) brand_replacement: Option, 11 | pub(crate) model_replacement: Option, 12 | } 13 | 14 | impl DeviceRegex { 15 | pub fn from_yaml(yaml: &Yaml) -> Result, UserAgentParserError> { 16 | let yamls = yaml.as_vec().ok_or(UserAgentParserError::IncorrectSource)?; 17 | 18 | let yamls_len = yamls.len(); 19 | 20 | if yamls_len == 0 { 21 | Err(UserAgentParserError::IncorrectSource) 22 | } else { 23 | let mut device_regexes = Vec::with_capacity(yamls_len); 24 | 25 | let yaml_regex = Yaml::String("regex".to_string()); 26 | let yaml_device_replacement = Yaml::String("device_replacement".to_string()); 27 | let yaml_brand_replacement = Yaml::String("brand_replacement".to_string()); 28 | let yaml_model_replacement = Yaml::String("model_replacement".to_string()); 29 | let yaml_regex_flag = Yaml::String("regex_flag".to_string()); 30 | 31 | for yaml in yamls { 32 | let yaml = yaml.as_hash().ok_or(UserAgentParserError::IncorrectSource)?; 33 | 34 | let device_replacement = match yaml.get(&yaml_device_replacement) { 35 | Some(yaml) => yaml 36 | .as_str() 37 | .map(|s| Some(s.to_string())) 38 | .ok_or(UserAgentParserError::IncorrectSource)?, 39 | None => None, 40 | }; 41 | 42 | let brand_replacement = match yaml.get(&yaml_brand_replacement) { 43 | Some(yaml) => yaml 44 | .as_str() 45 | .map(|s| Some(s.to_string())) 46 | .ok_or(UserAgentParserError::IncorrectSource)?, 47 | None => None, 48 | }; 49 | 50 | let model_replacement = match yaml.get(&yaml_model_replacement) { 51 | Some(yaml) => yaml 52 | .as_str() 53 | .map(|s| Some(s.to_string())) 54 | .ok_or(UserAgentParserError::IncorrectSource)?, 55 | None => None, 56 | }; 57 | 58 | let regex_options = if let Some(yaml) = yaml.get(&yaml_regex_flag) { 59 | let regex_flag = yaml.as_str().ok_or(UserAgentParserError::IncorrectSource)?; 60 | 61 | if regex_flag == "i" { 62 | RegexOptions::REGEX_OPTION_IGNORECASE 63 | } else { 64 | RegexOptions::REGEX_OPTION_NONE 65 | } 66 | } else { 67 | RegexOptions::REGEX_OPTION_NONE 68 | }; 69 | 70 | let regex = Regex::with_options( 71 | yaml.get(&yaml_regex) 72 | .ok_or(UserAgentParserError::IncorrectSource)? 73 | .as_str() 74 | .ok_or(UserAgentParserError::IncorrectSource)?, 75 | regex_options, 76 | Syntax::default(), 77 | )?; 78 | 79 | let device_regex = DeviceRegex { 80 | regex, 81 | device_replacement, 82 | brand_replacement, 83 | model_replacement, 84 | }; 85 | 86 | device_regexes.push(device_regex); 87 | } 88 | 89 | Ok(device_regexes) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/regexes/engine_regex.rs: -------------------------------------------------------------------------------- 1 | use onig::Regex; 2 | 3 | #[derive(Debug)] 4 | pub struct EngineRegex { 5 | pub(crate) regex: Regex, 6 | pub(crate) name_replacement: Option, 7 | pub(crate) engine_v1_replacement: Option, 8 | pub(crate) engine_v2_replacement: Option, 9 | pub(crate) engine_v3_replacement: Option, 10 | } 11 | 12 | impl EngineRegex { 13 | pub fn built_in_regexes() -> Vec { 14 | vec![ 15 | { 16 | let regex = 17 | Regex::new(r"(?i)(windows.+\sedge)/(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 18 | 19 | EngineRegex { 20 | regex, 21 | name_replacement: Some("EdgeHTML".to_string()), 22 | engine_v1_replacement: None, 23 | engine_v2_replacement: None, 24 | engine_v3_replacement: None, 25 | } 26 | }, 27 | { 28 | let regex = Regex::new(r"(?i)webkit/537\.36.+chrome/(?!27)").unwrap(); 29 | 30 | EngineRegex { 31 | regex, 32 | name_replacement: Some("Blink".to_string()), 33 | engine_v1_replacement: None, 34 | engine_v2_replacement: None, 35 | engine_v3_replacement: None, 36 | } 37 | }, 38 | { 39 | let regex = Regex::new(r"(?i)(presto)/(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 40 | 41 | EngineRegex { 42 | regex, 43 | name_replacement: Some("Presto".to_string()), 44 | engine_v1_replacement: None, 45 | engine_v2_replacement: None, 46 | engine_v3_replacement: None, 47 | } 48 | }, 49 | { 50 | let regex = Regex::new(r"(?i)(webkit)/(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 51 | 52 | EngineRegex { 53 | regex, 54 | name_replacement: Some("WebKit".to_string()), 55 | engine_v1_replacement: None, 56 | engine_v2_replacement: None, 57 | engine_v3_replacement: None, 58 | } 59 | }, 60 | { 61 | let regex = Regex::new(r"(?i)(trident)/(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 62 | 63 | EngineRegex { 64 | regex, 65 | name_replacement: Some("Trident".to_string()), 66 | engine_v1_replacement: None, 67 | engine_v2_replacement: None, 68 | engine_v3_replacement: None, 69 | } 70 | }, 71 | { 72 | let regex = Regex::new(r"(?i)(netfront)/(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 73 | 74 | EngineRegex { 75 | regex, 76 | name_replacement: Some("NetFront".to_string()), 77 | engine_v1_replacement: None, 78 | engine_v2_replacement: None, 79 | engine_v3_replacement: None, 80 | } 81 | }, 82 | { 83 | let regex = Regex::new(r"(?i)(netsurf)/(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 84 | 85 | EngineRegex { 86 | regex, 87 | name_replacement: Some("NetSurf".to_string()), 88 | engine_v1_replacement: None, 89 | engine_v2_replacement: None, 90 | engine_v3_replacement: None, 91 | } 92 | }, 93 | { 94 | let regex = Regex::new(r"(?i)(amaya)/(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 95 | 96 | EngineRegex { 97 | regex, 98 | name_replacement: Some("Amaya".to_string()), 99 | engine_v1_replacement: None, 100 | engine_v2_replacement: None, 101 | engine_v3_replacement: None, 102 | } 103 | }, 104 | { 105 | let regex = Regex::new(r"(?i)(lynx)/(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 106 | 107 | EngineRegex { 108 | regex, 109 | name_replacement: Some("Lynx".to_string()), 110 | engine_v1_replacement: None, 111 | engine_v2_replacement: None, 112 | engine_v3_replacement: None, 113 | } 114 | }, 115 | { 116 | let regex = Regex::new(r"(?i)(w3m)/(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 117 | 118 | EngineRegex { 119 | regex, 120 | name_replacement: Some("w3m".to_string()), 121 | engine_v1_replacement: None, 122 | engine_v2_replacement: None, 123 | engine_v3_replacement: None, 124 | } 125 | }, 126 | { 127 | let regex = Regex::new(r"(?i)(goanna)/(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 128 | 129 | EngineRegex { 130 | regex, 131 | name_replacement: Some("Goanna".to_string()), 132 | engine_v1_replacement: None, 133 | engine_v2_replacement: None, 134 | engine_v3_replacement: None, 135 | } 136 | }, 137 | { 138 | let regex = 139 | Regex::new(r"(?i)(khtml)[/\s]\(?(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 140 | 141 | EngineRegex { 142 | regex, 143 | name_replacement: Some("KHTML".to_string()), 144 | engine_v1_replacement: None, 145 | engine_v2_replacement: None, 146 | engine_v3_replacement: None, 147 | } 148 | }, 149 | { 150 | let regex = 151 | Regex::new(r"(?i)(tasman)[/\s]\(?(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 152 | 153 | EngineRegex { 154 | regex, 155 | name_replacement: Some("Tasman".to_string()), 156 | engine_v1_replacement: None, 157 | engine_v2_replacement: None, 158 | engine_v3_replacement: None, 159 | } 160 | }, 161 | { 162 | let regex = 163 | Regex::new(r"(?i)(links)[/\s]\(?(\w+)(?:\.(\w+))?(?:\.(\w+))?").unwrap(); 164 | 165 | EngineRegex { 166 | regex, 167 | name_replacement: Some("Links".to_string()), 168 | engine_v1_replacement: None, 169 | engine_v2_replacement: None, 170 | engine_v3_replacement: None, 171 | } 172 | }, 173 | { 174 | let regex = Regex::new(r"(?i)(icab)[/\s]([23])(?:\.(\d+))?(?:\.(\d+))?").unwrap(); 175 | 176 | EngineRegex { 177 | regex, 178 | name_replacement: Some("iCab".to_string()), 179 | engine_v1_replacement: None, 180 | engine_v2_replacement: None, 181 | engine_v3_replacement: None, 182 | } 183 | }, 184 | { 185 | let regex = 186 | Regex::new(r"(?i)(rv:)(\w+)(?:\.(\w+))?(?:\.(\w+))?(?:(?=\.)\w+)*.+gecko") 187 | .unwrap(); 188 | 189 | EngineRegex { 190 | regex, 191 | name_replacement: Some("Gecko".to_string()), 192 | engine_v1_replacement: None, 193 | engine_v2_replacement: None, 194 | engine_v3_replacement: None, 195 | } 196 | }, 197 | ] 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/regexes/mod.rs: -------------------------------------------------------------------------------- 1 | mod cpu_regex; 2 | mod device_regex; 3 | mod engine_regex; 4 | mod os_regex; 5 | mod product_regex; 6 | 7 | pub use cpu_regex::CPURegex; 8 | pub use device_regex::DeviceRegex; 9 | pub use engine_regex::EngineRegex; 10 | pub use os_regex::OSRegex; 11 | pub use product_regex::ProductRegex; 12 | -------------------------------------------------------------------------------- /src/regexes/os_regex.rs: -------------------------------------------------------------------------------- 1 | use onig::Regex; 2 | use yaml_rust::Yaml; 3 | 4 | use crate::UserAgentParserError; 5 | 6 | #[derive(Debug)] 7 | pub struct OSRegex { 8 | pub(crate) regex: Regex, 9 | pub(crate) os_replacement: Option, 10 | pub(crate) os_v1_replacement: Option, 11 | pub(crate) os_v2_replacement: Option, 12 | pub(crate) os_v3_replacement: Option, 13 | pub(crate) os_v4_replacement: Option, 14 | } 15 | 16 | impl OSRegex { 17 | pub fn from_yaml(yaml: &Yaml) -> Result, UserAgentParserError> { 18 | let yamls = yaml.as_vec().ok_or(UserAgentParserError::IncorrectSource)?; 19 | 20 | let yamls_len = yamls.len(); 21 | 22 | if yamls_len == 0 { 23 | Err(UserAgentParserError::IncorrectSource) 24 | } else { 25 | let mut os_regexes = Vec::with_capacity(yamls_len); 26 | 27 | let yaml_regex = Yaml::String("regex".to_string()); 28 | let yaml_os_replacement = Yaml::String("os_replacement".to_string()); 29 | let yaml_os_v1_replacement = Yaml::String("os_v1_replacement".to_string()); 30 | let yaml_os_v2_replacement = Yaml::String("os_v2_replacement".to_string()); 31 | let yaml_os_v3_replacement = Yaml::String("os_v3_replacement".to_string()); 32 | let yaml_os_v4_replacement = Yaml::String("os_v4_replacement".to_string()); 33 | 34 | for yaml in yamls { 35 | let yaml = yaml.as_hash().ok_or(UserAgentParserError::IncorrectSource)?; 36 | 37 | let regex = Regex::new( 38 | yaml.get(&yaml_regex) 39 | .ok_or(UserAgentParserError::IncorrectSource)? 40 | .as_str() 41 | .ok_or(UserAgentParserError::IncorrectSource)?, 42 | )?; 43 | 44 | let os_replacement = match yaml.get(&yaml_os_replacement) { 45 | Some(yaml) => yaml 46 | .as_str() 47 | .map(|s| Some(s.to_string())) 48 | .ok_or(UserAgentParserError::IncorrectSource)?, 49 | None => None, 50 | }; 51 | 52 | let os_v1_replacement = match yaml.get(&yaml_os_v1_replacement) { 53 | Some(yaml) => yaml 54 | .as_str() 55 | .map(|s| Some(s.to_string())) 56 | .ok_or(UserAgentParserError::IncorrectSource)?, 57 | None => None, 58 | }; 59 | 60 | let os_v2_replacement = match yaml.get(&yaml_os_v2_replacement) { 61 | Some(yaml) => yaml 62 | .as_str() 63 | .map(|s| Some(s.to_string())) 64 | .ok_or(UserAgentParserError::IncorrectSource)?, 65 | None => None, 66 | }; 67 | 68 | let os_v3_replacement = match yaml.get(&yaml_os_v3_replacement) { 69 | Some(yaml) => yaml 70 | .as_str() 71 | .map(|s| Some(s.to_string())) 72 | .ok_or(UserAgentParserError::IncorrectSource)?, 73 | None => None, 74 | }; 75 | 76 | let os_v4_replacement = match yaml.get(&yaml_os_v4_replacement) { 77 | Some(yaml) => yaml 78 | .as_str() 79 | .map(|s| Some(s.to_string())) 80 | .ok_or(UserAgentParserError::IncorrectSource)?, 81 | None => None, 82 | }; 83 | 84 | let os_regex = OSRegex { 85 | regex, 86 | os_replacement, 87 | os_v1_replacement, 88 | os_v2_replacement, 89 | os_v3_replacement, 90 | os_v4_replacement, 91 | }; 92 | 93 | os_regexes.push(os_regex); 94 | } 95 | 96 | Ok(os_regexes) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/regexes/product_regex.rs: -------------------------------------------------------------------------------- 1 | use onig::Regex; 2 | use yaml_rust::Yaml; 3 | 4 | use crate::UserAgentParserError; 5 | 6 | #[derive(Debug)] 7 | pub struct ProductRegex { 8 | pub(crate) regex: Regex, 9 | pub(crate) family_replacement: Option, 10 | pub(crate) v1_replacement: Option, 11 | pub(crate) v2_replacement: Option, 12 | pub(crate) v3_replacement: Option, 13 | } 14 | 15 | impl ProductRegex { 16 | pub fn from_yaml(yaml: &Yaml) -> Result, UserAgentParserError> { 17 | let yamls = yaml.as_vec().ok_or(UserAgentParserError::IncorrectSource)?; 18 | 19 | let yamls_len = yamls.len(); 20 | 21 | if yamls_len == 0 { 22 | Err(UserAgentParserError::IncorrectSource) 23 | } else { 24 | let mut user_agent_regexes = Vec::with_capacity(yamls_len); 25 | 26 | let yaml_regex = Yaml::String("regex".to_string()); 27 | let yaml_family_replacement = Yaml::String("family_replacement".to_string()); 28 | let yaml_v1_replacement = Yaml::String("v1_replacement".to_string()); 29 | let yaml_v2_replacement = Yaml::String("v2_replacement".to_string()); 30 | let yaml_v3_replacement = Yaml::String("v3_replacement".to_string()); 31 | 32 | for yaml in yamls { 33 | let yaml = yaml.as_hash().ok_or(UserAgentParserError::IncorrectSource)?; 34 | 35 | let regex = Regex::new( 36 | yaml.get(&yaml_regex) 37 | .ok_or(UserAgentParserError::IncorrectSource)? 38 | .as_str() 39 | .ok_or(UserAgentParserError::IncorrectSource)?, 40 | )?; 41 | 42 | let family_replacement = match yaml.get(&yaml_family_replacement) { 43 | Some(yaml) => yaml 44 | .as_str() 45 | .map(|s| Some(s.to_string())) 46 | .ok_or(UserAgentParserError::IncorrectSource)?, 47 | None => None, 48 | }; 49 | 50 | let v1_replacement = match yaml.get(&yaml_v1_replacement) { 51 | Some(yaml) => yaml 52 | .as_str() 53 | .map(|s| Some(s.to_string())) 54 | .ok_or(UserAgentParserError::IncorrectSource)?, 55 | None => None, 56 | }; 57 | 58 | let v2_replacement = match yaml.get(&yaml_v2_replacement) { 59 | Some(yaml) => yaml 60 | .as_str() 61 | .map(|s| Some(s.to_string())) 62 | .ok_or(UserAgentParserError::IncorrectSource)?, 63 | None => None, 64 | }; 65 | 66 | let v3_replacement = match yaml.get(&yaml_v3_replacement) { 67 | Some(yaml) => yaml 68 | .as_str() 69 | .map(|s| Some(s.to_string())) 70 | .ok_or(UserAgentParserError::IncorrectSource)?, 71 | None => None, 72 | }; 73 | 74 | let user_agent_regex = ProductRegex { 75 | regex, 76 | family_replacement, 77 | v1_replacement, 78 | v2_replacement, 79 | v3_replacement, 80 | }; 81 | 82 | user_agent_regexes.push(user_agent_regex); 83 | } 84 | 85 | Ok(user_agent_regexes) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/request_guards.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use rocket::{ 4 | outcome::Outcome, 5 | request::{FromRequest, Outcome as OutcomeResult, Request}, 6 | }; 7 | 8 | use crate::{models::*, UserAgentParser}; 9 | 10 | fn from_request_user_agent<'r>(request: &'r Request<'_>) -> UserAgent<'r> { 11 | let user_agent: Option> = 12 | request.headers().get("user-agent").next().map(Cow::from); 13 | 14 | UserAgent { 15 | user_agent, 16 | } 17 | } 18 | 19 | #[rocket::async_trait] 20 | impl<'r> FromRequest<'r> for UserAgent<'r> { 21 | type Error = (); 22 | 23 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 24 | Outcome::Success(from_request_user_agent(request)) 25 | } 26 | } 27 | 28 | #[rocket::async_trait] 29 | impl<'r> FromRequest<'r> for &UserAgent<'r> { 30 | type Error = (); 31 | 32 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 33 | let cache = request.local_cache(|| from_request_user_agent(request).into_owned()); 34 | 35 | Outcome::Success(cache) 36 | } 37 | } 38 | 39 | fn from_request_product<'r>(request: &'r Request<'_>) -> Product<'r> { 40 | let user_agent_parser = request.rocket().state::().unwrap(); 41 | 42 | let user_agent: Option<&str> = request.headers().get("user-agent").next(); 43 | 44 | match user_agent { 45 | Some(user_agent) => user_agent_parser.parse_product(user_agent), 46 | None => Product::default(), 47 | } 48 | } 49 | 50 | #[rocket::async_trait] 51 | impl<'r> FromRequest<'r> for Product<'r> { 52 | type Error = (); 53 | 54 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 55 | Outcome::Success(from_request_product(request)) 56 | } 57 | } 58 | 59 | #[rocket::async_trait] 60 | impl<'r> FromRequest<'r> for &Product<'r> { 61 | type Error = (); 62 | 63 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 64 | let cache = 65 | request.local_cache_async(async { from_request_product(request).into_owned() }).await; 66 | 67 | Outcome::Success(cache) 68 | } 69 | } 70 | 71 | fn from_request_os<'r>(request: &'r Request<'_>) -> OS<'r> { 72 | let user_agent_parser = request.rocket().state::().unwrap(); 73 | 74 | let user_agent: Option<&str> = request.headers().get("user-agent").next(); 75 | 76 | match user_agent { 77 | Some(user_agent) => user_agent_parser.parse_os(user_agent), 78 | None => OS::default(), 79 | } 80 | } 81 | 82 | #[rocket::async_trait] 83 | impl<'r> FromRequest<'r> for OS<'r> { 84 | type Error = (); 85 | 86 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 87 | Outcome::Success(from_request_os(request)) 88 | } 89 | } 90 | 91 | #[rocket::async_trait] 92 | impl<'r> FromRequest<'r> for &OS<'r> { 93 | type Error = (); 94 | 95 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 96 | let cache = 97 | request.local_cache_async(async { from_request_os(request).into_owned() }).await; 98 | 99 | Outcome::Success(cache) 100 | } 101 | } 102 | 103 | fn from_request_device<'r>(request: &'r Request<'_>) -> Device<'r> { 104 | let user_agent_parser = request.rocket().state::().unwrap(); 105 | 106 | let user_agent: Option<&str> = request.headers().get("user-agent").next(); 107 | 108 | match user_agent { 109 | Some(user_agent) => user_agent_parser.parse_device(user_agent), 110 | None => Device::default(), 111 | } 112 | } 113 | 114 | #[rocket::async_trait] 115 | impl<'r> FromRequest<'r> for Device<'r> { 116 | type Error = (); 117 | 118 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 119 | Outcome::Success(from_request_device(request)) 120 | } 121 | } 122 | 123 | #[rocket::async_trait] 124 | impl<'r> FromRequest<'r> for &Device<'r> { 125 | type Error = (); 126 | 127 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 128 | let cache = 129 | request.local_cache_async(async { from_request_device(request).into_owned() }).await; 130 | 131 | Outcome::Success(cache) 132 | } 133 | } 134 | 135 | fn from_request_cpu<'r>(request: &'r Request<'_>) -> CPU<'r> { 136 | let user_agent_parser = request.rocket().state::().unwrap(); 137 | 138 | let user_agent: Option<&str> = request.headers().get("user-agent").next(); 139 | 140 | match user_agent { 141 | Some(user_agent) => user_agent_parser.parse_cpu(user_agent), 142 | None => CPU::default(), 143 | } 144 | } 145 | 146 | #[rocket::async_trait] 147 | impl<'r> FromRequest<'r> for CPU<'r> { 148 | type Error = (); 149 | 150 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 151 | Outcome::Success(from_request_cpu(request)) 152 | } 153 | } 154 | 155 | #[rocket::async_trait] 156 | impl<'r> FromRequest<'r> for &CPU<'r> { 157 | type Error = (); 158 | 159 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 160 | let cache = 161 | request.local_cache_async(async { from_request_cpu(request).into_owned() }).await; 162 | 163 | Outcome::Success(cache) 164 | } 165 | } 166 | 167 | fn from_request_engine<'r>(request: &'r Request<'_>) -> Engine<'r> { 168 | let user_agent_parser = request.rocket().state::().unwrap(); 169 | 170 | let user_agent: Option<&str> = request.headers().get("user-agent").next(); 171 | 172 | match user_agent { 173 | Some(user_agent) => user_agent_parser.parse_engine(user_agent), 174 | None => Engine::default(), 175 | } 176 | } 177 | 178 | #[rocket::async_trait] 179 | impl<'r> FromRequest<'r> for Engine<'r> { 180 | type Error = (); 181 | 182 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 183 | Outcome::Success(from_request_engine(request)) 184 | } 185 | } 186 | 187 | #[rocket::async_trait] 188 | impl<'r> FromRequest<'r> for &Engine<'r> { 189 | type Error = (); 190 | 191 | async fn from_request(request: &'r Request<'_>) -> OutcomeResult { 192 | let cache = 193 | request.local_cache_async(async { from_request_engine(request).into_owned() }).await; 194 | 195 | Outcome::Success(cache) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /tests/uap_core.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, fs}; 2 | 3 | use user_agent_parser::UserAgentParser; 4 | use yaml_rust::{Yaml, YamlLoader}; 5 | 6 | #[test] 7 | fn test_product() { 8 | let ua_parser = UserAgentParser::from_path("uap-core/regexes.yaml").unwrap(); 9 | 10 | let yaml = fs::read_to_string("uap-core/tests/test_ua.yaml").unwrap(); 11 | 12 | let yamls = YamlLoader::load_from_str(&yaml).unwrap(); 13 | let yaml = &yamls[0]; 14 | 15 | let test_cases = yaml 16 | .as_hash() 17 | .unwrap() 18 | .get(&Yaml::String("test_cases".to_string())) 19 | .unwrap() 20 | .as_vec() 21 | .unwrap(); 22 | 23 | let yaml_user_agent_string = Yaml::String("user_agent_string".to_string()); 24 | let yaml_family = Yaml::String("family".to_string()); 25 | let yaml_major = Yaml::String("major".to_string()); 26 | let yaml_minor = Yaml::String("minor".to_string()); 27 | let yaml_patch = Yaml::String("patch".to_string()); 28 | 29 | for test_case in test_cases { 30 | let test_case = test_case.as_hash().unwrap(); 31 | 32 | let user_agent = test_case.get(&yaml_user_agent_string).unwrap().as_str().unwrap(); 33 | 34 | let name = test_case.get(&yaml_family).unwrap().as_str().map(Cow::from); 35 | let major = test_case.get(&yaml_major).unwrap().as_str().map(Cow::from); 36 | let minor = test_case.get(&yaml_minor).unwrap().as_str().map(Cow::from); 37 | let patch = test_case.get(&yaml_patch).unwrap().as_str().map(Cow::from); 38 | 39 | let product = ua_parser.parse_product(user_agent); 40 | 41 | assert_eq!(name, product.name); 42 | assert_eq!(major, product.major); 43 | assert_eq!(minor, product.minor); 44 | assert_eq!(patch, product.patch); 45 | } 46 | } 47 | 48 | #[test] 49 | fn test_os() { 50 | let ua_parser = UserAgentParser::from_path("uap-core/regexes.yaml").unwrap(); 51 | 52 | let yaml = fs::read_to_string("uap-core/tests/test_os.yaml").unwrap(); 53 | 54 | let yamls = YamlLoader::load_from_str(&yaml).unwrap(); 55 | let yaml = &yamls[0]; 56 | 57 | let test_cases = yaml 58 | .as_hash() 59 | .unwrap() 60 | .get(&Yaml::String("test_cases".to_string())) 61 | .unwrap() 62 | .as_vec() 63 | .unwrap(); 64 | 65 | let yaml_user_agent_string = Yaml::String("user_agent_string".to_string()); 66 | let yaml_family = Yaml::String("family".to_string()); 67 | let yaml_major = Yaml::String("major".to_string()); 68 | let yaml_minor = Yaml::String("minor".to_string()); 69 | let yaml_patch = Yaml::String("patch".to_string()); 70 | let yaml_patch_minor = Yaml::String("patch_minor".to_string()); 71 | 72 | for test_case in test_cases { 73 | let test_case = test_case.as_hash().unwrap(); 74 | 75 | let user_agent = test_case.get(&yaml_user_agent_string).unwrap().as_str().unwrap(); 76 | 77 | let name = test_case.get(&yaml_family).unwrap().as_str().map(Cow::from); 78 | let major = test_case.get(&yaml_major).unwrap().as_str().map(Cow::from); 79 | let minor = test_case.get(&yaml_minor).unwrap().as_str().map(Cow::from); 80 | let patch = test_case.get(&yaml_patch).unwrap().as_str().map(Cow::from); 81 | let patch_minor = test_case.get(&yaml_patch_minor).unwrap().as_str().map(Cow::from); 82 | 83 | let os = ua_parser.parse_os(user_agent); 84 | 85 | assert_eq!(name, os.name); 86 | assert_eq!(major, os.major); 87 | assert_eq!(minor, os.minor); 88 | assert_eq!(patch, os.patch); 89 | assert_eq!(patch_minor, os.patch_minor); 90 | } 91 | } 92 | 93 | #[test] 94 | fn test_device() { 95 | let ua_parser = UserAgentParser::from_path("uap-core/regexes.yaml").unwrap(); 96 | 97 | let yaml = fs::read_to_string("uap-core/tests/test_device.yaml").unwrap(); 98 | 99 | let yamls = YamlLoader::load_from_str(&yaml).unwrap(); 100 | let yaml = &yamls[0]; 101 | 102 | let test_cases = yaml 103 | .as_hash() 104 | .unwrap() 105 | .get(&Yaml::String("test_cases".to_string())) 106 | .unwrap() 107 | .as_vec() 108 | .unwrap(); 109 | 110 | let yaml_user_agent_string = Yaml::String("user_agent_string".to_string()); 111 | let yaml_family = Yaml::String("family".to_string()); 112 | let yaml_brand = Yaml::String("brand".to_string()); 113 | let yaml_model = Yaml::String("model".to_string()); 114 | 115 | for test_case in test_cases { 116 | let test_case = test_case.as_hash().unwrap(); 117 | 118 | let user_agent = test_case.get(&yaml_user_agent_string).unwrap().as_str().unwrap(); 119 | 120 | let name = test_case.get(&yaml_family).unwrap().as_str().map(Cow::from); 121 | let brand = test_case.get(&yaml_brand).unwrap().as_str().map(Cow::from); 122 | let model = test_case.get(&yaml_model).unwrap().as_str().map(Cow::from); 123 | 124 | let device = ua_parser.parse_device(user_agent); 125 | 126 | assert_eq!(name, device.name); 127 | assert_eq!(brand, device.brand); 128 | assert_eq!(model, device.model); 129 | } 130 | } 131 | 132 | #[test] 133 | fn test_cpu() { 134 | let test_cases = [ 135 | ("ia32", "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:19.0) Gecko/20100101 Firefox/19.0"), 136 | ("ia32", "Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.7) Gecko/20040628 Epiphany/1.2.6"), 137 | ("ia32", "QuickTime/7.5.6 (qtver=7.5.6;cpu=IA32;os=Mac 10.5.8)"), 138 | ("amd64", "Opera/9.80 (X11; Linux x86_64; U; Linux Mint; en) Presto/2.2.15 Version/10.10"), 139 | ( 140 | "amd64", 141 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; Win64; x64; Trident/6.0; \ 142 | .NET4.0E; .NET4.0C)", 143 | ), 144 | ("amd64", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)"), 145 | ( 146 | "amd64", 147 | "XBMC/12.0 Git:20130127-fb595f2 (Windows NT 6.1;WOW64;Win64;x64; http://www.xbmc.org)", 148 | ), 149 | ( 150 | "arm", 151 | "Mozilla/5.0 (X11; U; Linux armv61; en-US; rv:1.9.1b2pre) Gecko/20081015 Fennec/1.0a1", 152 | ), 153 | ( 154 | "arm", 155 | "Mozilla/5.0 (X11; CrOS armv7l 9765.85.0) AppleWebKit/537.36 (KHTML, like Gecko) \ 156 | Chrome/61.0.3163.123 Safari/537.36", 157 | ), 158 | ("arm", "Opera/9.7 (Windows Mobile; PPC; Opera Mobi/35166; U; en) Presto/2.2.1"), 159 | ("ppc", "Mozilla/4.0 (compatible; MSIE 4.5; Mac_PowerPC)"), 160 | ("ppc", "Mozilla/4.0 (compatible; MSIE 5.17; Mac_PowerPC Mac OS; en)"), 161 | ("ppc", "iCab/2.9.5 (Macintosh; U; PPC; Mac OS X)"), 162 | ( 163 | "sparc", 164 | "Mozilla/5.0 (X11; U; SunOS sun4u; en-US; rv:1.9b5) Gecko/2008032620 Firefox/3.0b5", 165 | ), 166 | ]; 167 | 168 | let ua_parser = UserAgentParser::from_path("uap-core/regexes.yaml").unwrap(); 169 | 170 | for (answer, user_agent) in test_cases.iter() { 171 | let cpu = ua_parser.parse_cpu(user_agent); 172 | 173 | assert_eq!(Some(Cow::from(*answer)), cpu.architecture); 174 | } 175 | } 176 | 177 | #[test] 178 | fn test_engine() { 179 | let test_cases = [ 180 | ( 181 | "Blink", 182 | None, 183 | None, 184 | None, 185 | "Mozilla/5.0 (Linux; Android 7.0; SM-G920I Build/NRD90M) AppleWebKit/537.36 (KHTML, \ 186 | like Gecko) OculusBrowser/3.4.9 SamsungBrowser/4.0 Chrome/57.0.2987.146 Mobile VR \ 187 | Safari/537.36", 188 | ), 189 | ( 190 | "EdgeHTML", 191 | Some("12"), 192 | Some("0"), 193 | None, 194 | "Mozilla/5.0 (Windows NT 6.4; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) \ 195 | Chrome/36.0.1985.143 Safari/537.36 Edge/12.0", 196 | ), 197 | ( 198 | "Gecko", 199 | Some("2"), 200 | Some("0b9pre"), 201 | None, 202 | "Mozilla/5.0 (X11; Linux x86_64; rv:2.0b9pre) Gecko/20110111 Firefox/4.0b9pre", 203 | ), 204 | ( 205 | "Goanna", 206 | Some("2"), 207 | Some("2"), 208 | None, 209 | "Mozilla/5.0 (Windows NT 5.1; rv:38.9) Gecko/20100101 Goanna/2.2 Firefox/38.9 \ 210 | PaleMoon/26.5.0", 211 | ), 212 | ( 213 | "KHTML", 214 | Some("4"), 215 | Some("5"), 216 | Some("4"), 217 | "Mozilla/5.0 (compatible; Konqueror/4.5; FreeBSD) KHTML/4.5.4 (like Gecko)", 218 | ), 219 | ( 220 | "NetFront", 221 | Some("3"), 222 | Some("0"), 223 | None, 224 | "Mozilla/4.0 (PDA; Windows CE/1.0.1) NetFront/3.0", 225 | ), 226 | ( 227 | "Presto", 228 | Some("2"), 229 | Some("8"), 230 | Some("149"), 231 | "Opera/9.80 (Windows NT 6.1; Opera Tablet/15165; U; en) Presto/2.8.149 Version/11.1", 232 | ), 233 | ( 234 | "Tasman", 235 | Some("1"), 236 | Some("0"), 237 | None, 238 | "Mozilla/4.0 (compatible; MSIE 6.0; PPC Mac OS X 10.4.7; Tasman 1.0)", 239 | ), 240 | ( 241 | "Trident", 242 | Some("6"), 243 | Some("0"), 244 | None, 245 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)", 246 | ), 247 | ( 248 | "WebKit", 249 | Some("533"), 250 | Some("19"), 251 | Some("4"), 252 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; sv-SE) AppleWebKit/533.19.4 (KHTML, like \ 253 | Gecko) Version/5.0.3 Safari/533.19.4", 254 | ), 255 | ( 256 | "WebKit", 257 | Some("537"), 258 | Some("36"), 259 | None, 260 | "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML like Gecko) \ 261 | Chrome/27.0.1453.110 Safari/537.36", 262 | ), 263 | ]; 264 | 265 | let ua_parser = UserAgentParser::from_path("uap-core/regexes.yaml").unwrap(); 266 | 267 | for (answer, major, minor, patch, user_agent) in test_cases.iter() { 268 | let engine = ua_parser.parse_engine(user_agent); 269 | 270 | assert_eq!(Some(Cow::from(*answer)), engine.name); 271 | assert_eq!(major.map(Cow::from), engine.major); 272 | assert_eq!(minor.map(Cow::from), engine.minor); 273 | assert_eq!(patch.map(Cow::from), engine.patch); 274 | } 275 | } 276 | --------------------------------------------------------------------------------