user-agent-parser 0.5.0

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
Documentation

User Agent Parser

CI

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

Usage

You can make a regexes.yaml file or copy one from https://github.com/ua-parser/uap-core

This is a simple example of regexes.yaml.

user_agent_parsers:
  - regex: '(ESPN)[%20| ]+Radio/(\d+)\.(\d+)\.(\d+) CFNetwork'
  - regex: '(Namoroka|Shiretoko|Minefield)/(\d+)\.(\d+)\.(\d+(?:pre|))'
    family_replacement: 'Firefox ($1)'
  - regex: '(Android) Eclair'
    v1_replacement: '2'
    v2_replacement: '1'

os_parsers:
  - regex: 'Win(?:dows)? ?(95|98|3.1|NT|ME|2000|XP|Vista|7|CE)'
    os_replacement: 'Windows'
    os_v1_replacement: '$1'

device_parsers:
  - regex: '\bSmartWatch *\( *([^;]+) *; *([^;]+) *;'
    device_replacement: '$1 $2'
    brand_replacement: '$1'
    model_replacement: '$2'

The CPU and the layout engine are not a part of the uap-core format, so this crate ships its own patterns for them. The optional cpu_parsers and engine_parsers sections replace those built-in patterns.

cpu_parsers:
  - regex: 'sun4\w[;)]'
    architecture_replacement: 'sparc'
  - regex: '\b(riscv\d*)\b'

engine_parsers:
  - regex: '(myengine)/(\w+)(?:\.(\w+))?(?:\.(\w+))?'
    engine_replacement: 'MyEngine'

In cpu_parsers, the first capture group is the architecture. In engine_parsers, the first one is the name, and the second, third and fourth ones are the major, minor and patch versions. A *_replacement overrides the group it stands for, just like in the sections above. Leaving a section out keeps every built-in pattern, and writing one down replaces all of them.

Then, use the from_path (or from_str if your YAML data is in-memory) associated function to create a UserAgentParser instance.

use user_agent_parser::UserAgentParser;

let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap();

Use the parse_* methods and input a user-agent string to get information.

use user_agent_parser::UserAgentParser;

let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap();

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]";

let product = ua_parser.parse_product(user_agent);

println!("{:#?}", product);

//    Product {
//        name: Some(
//            "Facebook",
//        ),
//        major: Some(
//            "8",
//        ),
//        minor: Some(
//            "0",
//        ),
//        patch: Some(
//            "0",
//        ),
//        patch_minor: None,
//    }

let os = ua_parser.parse_os(user_agent);

println!("{:#?}", os);

//    OS {
//        name: Some(
//            "iOS",
//        ),
//        major: None,
//        minor: None,
//        patch: None,
//        patch_minor: None,
//    }

let device = ua_parser.parse_device(user_agent);

println!("{:#?}", device);

//    Device {
//        name: Some(
//            "iPhone",
//        ),
//        brand: Some(
//            "Apple",
//        ),
//        model: Some(
//            "iPhone4,1",
//        ),
//    }

let cpu = ua_parser.parse_cpu(user_agent);

println!("{:#?}", cpu);

//    CPU {
//        architecture: Some(
//            "amd64",
//        ),
//    }

let engine = ua_parser.parse_engine(user_agent);

println!("{:#?}", engine);

//    Engine {
//        name: Some(
//            "Gecko",
//        ),
//        major: Some(
//            "10",
//        ),
//        minor: Some(
//            "0",
//        ),
//        patch: None,
//    }

The Product, OS and Engine models can also join their version parts into one string with the version method. It starts at the major version and stops at the first part which is missing.

use user_agent_parser::UserAgentParser;

let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap();

let product = ua_parser.parse_product("Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36");

assert_eq!(Some("79.0.3945.79"), product.version().as_deref());

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.

use user_agent_parser::UserAgentParser;

let ua_parser = UserAgentParser::from_path("/path/to/regexes.yaml").unwrap();

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();

Rocket Support

This crate supports the Rocket framework. All you have to do is enabling the rocket feature for this crate.

[dependencies.user-agent-parser]
version = "*"
features = ["rocket"]

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.

The Product, OS, Device, CPU and Engine guards panic if no UserAgentParser is managed, because that is a mistake in the setup rather than something a single request can recover from. The UserAgent guard needs no parser at all. A request without a User-Agent header is fine and yields the default model.

#[macro_use]
extern crate rocket;

use user_agent_parser::{UserAgentParser, UserAgent, Product, OS, Device, CPU, Engine};

#[get("/")]
fn index(user_agent: UserAgent, product: Product, os: OS, device: Device, cpu: CPU, engine: Engine) -> String {
    format!("{user_agent:#?}\n{product:#?}\n{os:#?}\n{device:#?}\n{cpu:#?}\n{engine:#?}",
            user_agent = user_agent,
            product = product,
            os = os,
            device = device,
            cpu = cpu,
            engine = engine,
    )
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(UserAgentParser::from_path("/path/to/regexes.yaml").unwrap())
        .mount("/", routes![index])
}

Axum Support

This crate also supports the Axum framework. All you have to do is enabling the axum feature for this crate.

[dependencies.user-agent-parser]
version = "*"
features = ["axum"]

Share 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.

The Product, OS, Device, CPU and Engine extractors panic if that layer is missing, because that is a mistake in the setup rather than something a single request can recover from. The UserAgent extractor needs no parser at all. A request without a User-Agent header is fine and yields the default model.

use std::sync::Arc;

use axum::{routing::get, Extension, Router};
use user_agent_parser::{UserAgentParser, UserAgent, Product, OS, Device, CPU, Engine};

async fn index(
    user_agent: UserAgent<'static>,
    product: Product<'static>,
    os: OS<'static>,
    device: Device<'static>,
    cpu: CPU<'static>,
    engine: Engine<'static>,
) -> String {
    format!("{user_agent:#?}\n{product:#?}\n{os:#?}\n{device:#?}\n{cpu:#?}\n{engine:#?}")
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(index))
        .layer(Extension(Arc::new(UserAgentParser::from_path("/path/to/regexes.yaml").unwrap())));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap();

    axum::serve(listener, app).await.unwrap();
}

Testing

# git clone --recurse-submodules https://github.com/magiclen/user-agent-parser.git

git clone https://github.com/magiclen/user-agent-parser.git

cd user-agent-parser

git submodule init
git submodule update --recursive

cargo test

Crates.io

https://crates.io/crates/user-agent-parser

Documentation

https://docs.rs/user-agent-parser

License

MIT