#![cfg_attr(docsrs, feature(doc_cfg))]
mod errors;
mod models;
mod regexes;
#[cfg(feature = "rocket")]
mod request_guards;
#[cfg(feature = "axum")]
mod request_guards_axum;
use std::{borrow::Cow, fs, path::Path, str::FromStr};
pub use errors::UserAgentParserError;
pub use models::*;
use regexes::*;
use yaml_rust::{Yaml, YamlLoader};
#[derive(Debug)]
pub struct UserAgentParser {
product_regexes: Vec<ProductRegex>,
os_regexes: Vec<OSRegex>,
device_regexes: Vec<DeviceRegex>,
cpu_regexes: Cow<'static, [CPURegex]>,
engine_regexes: Cow<'static, [EngineRegex]>,
}
impl UserAgentParser {
#[inline]
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<UserAgentParser, UserAgentParserError> {
let yaml = fs::read_to_string(path)?;
Self::from_str(yaml)
}
#[allow(clippy::should_implement_trait)]
pub fn from_str<S: AsRef<str>>(yaml: S) -> Result<UserAgentParser, UserAgentParserError> {
let yamls = YamlLoader::load_from_str(yaml.as_ref())?;
let [yaml] = yamls.as_slice() else {
return Err(UserAgentParserError::IncorrectSource);
};
match yaml.as_hash() {
Some(yaml) => {
let user_agent_parsers = yaml.get(&Yaml::String("user_agent_parsers".to_string()));
let os_parsers = yaml.get(&Yaml::String("os_parsers".to_string()));
let device_parsers = yaml.get(&Yaml::String("device_parsers".to_string()));
let cpu_parsers = yaml.get(&Yaml::String("cpu_parsers".to_string()));
let engine_parsers = yaml.get(&Yaml::String("engine_parsers".to_string()));
let product_regexes = match user_agent_parsers {
Some(user_agent_parsers) => ProductRegex::from_yaml(user_agent_parsers)?,
None => Vec::new(),
};
let os_regexes = match os_parsers {
Some(os_parsers) => OSRegex::from_yaml(os_parsers)?,
None => Vec::new(),
};
let device_regexes = match device_parsers {
Some(device_parsers) => DeviceRegex::from_yaml(device_parsers)?,
None => Vec::new(),
};
let cpu_regexes = match cpu_parsers {
Some(cpu_parsers) => Cow::from(CPURegex::from_yaml(cpu_parsers)?),
None => Cow::from(CPURegex::built_in_regexes()),
};
let engine_regexes = match engine_parsers {
Some(engine_parsers) => Cow::from(EngineRegex::from_yaml(engine_parsers)?),
None => Cow::from(EngineRegex::built_in_regexes()),
};
Ok(UserAgentParser {
product_regexes,
os_regexes,
device_regexes,
cpu_regexes,
engine_regexes,
})
},
None => Err(UserAgentParserError::IncorrectSource),
}
}
}
impl UserAgentParser {
pub fn parse_product<'a, S: AsRef<str> + ?Sized>(&'a self, user_agent: &'a S) -> Product<'a> {
let user_agent = user_agent.as_ref();
let mut product = Product::default();
for product_regex in self.product_regexes.iter() {
if !product_regex.regex.is_match(user_agent) {
continue;
}
let captures = product_regex.regex.captures(user_agent).unwrap();
product.name = resolve(1, product_regex.family_replacement.as_ref(), &captures);
product.major = resolve(2, product_regex.v1_replacement.as_ref(), &captures);
product.minor = resolve(3, product_regex.v2_replacement.as_ref(), &captures);
product.patch = resolve(4, product_regex.v3_replacement.as_ref(), &captures);
product.patch_minor = capture_str(5, &captures).map(Cow::from);
break;
}
if product.name.is_none() {
product.name = Some(Cow::from("Other"));
}
product
}
pub fn parse_os<'a, S: AsRef<str> + ?Sized>(&'a self, user_agent: &'a S) -> OS<'a> {
let user_agent = user_agent.as_ref();
let mut os = OS::default();
for os_regex in self.os_regexes.iter() {
if !os_regex.regex.is_match(user_agent) {
continue;
}
let captures = os_regex.regex.captures(user_agent).unwrap();
os.name = resolve(1, os_regex.os_replacement.as_ref(), &captures);
os.major = resolve(2, os_regex.os_v1_replacement.as_ref(), &captures);
os.minor = resolve(3, os_regex.os_v2_replacement.as_ref(), &captures);
os.patch = resolve(4, os_regex.os_v3_replacement.as_ref(), &captures);
os.patch_minor = resolve(5, os_regex.os_v4_replacement.as_ref(), &captures);
break;
}
if os.name.is_none() {
os.name = Some(Cow::from("Other"));
}
os
}
pub fn parse_device<'a, S: AsRef<str> + ?Sized>(&'a self, user_agent: &'a S) -> Device<'a> {
let user_agent = user_agent.as_ref();
let mut device = Device::default();
for device_regex in self.device_regexes.iter() {
if !device_regex.regex.is_match(user_agent) {
continue;
}
let captures = device_regex.regex.captures(user_agent).unwrap();
device.name = resolve(1, device_regex.device_replacement.as_ref(), &captures);
device.brand =
device_regex.brand_replacement.as_ref().and_then(|r| r.resolve(&captures));
device.model = resolve(1, device_regex.model_replacement.as_ref(), &captures);
break;
}
if device.name.is_none() {
device.name = Some(Cow::from("Other"));
}
device
}
pub fn parse_cpu<'a, S: AsRef<str> + ?Sized>(&'a self, user_agent: &'a S) -> CPU<'a> {
let user_agent = user_agent.as_ref();
let mut cpu = CPU::default();
for cpu_regex in self.cpu_regexes.iter() {
if !cpu_regex.regex.is_match(user_agent) {
continue;
}
cpu.architecture = match &cpu_regex.architecture_replacement {
Some(Replacement::Literal(architecture)) if !architecture.is_empty() => {
Some(Cow::from(architecture.as_str()))
},
replacement => {
let captures = cpu_regex.regex.captures(user_agent).unwrap();
resolve(1, replacement.as_ref(), &captures)
},
};
break;
}
cpu
}
pub fn parse_engine<'a, S: AsRef<str> + ?Sized>(&'a self, user_agent: &'a S) -> Engine<'a> {
let user_agent = user_agent.as_ref();
let mut engine = Engine::default();
for engine_regex in self.engine_regexes.iter() {
if !engine_regex.regex.is_match(user_agent) {
continue;
}
let captures = engine_regex.regex.captures(user_agent).unwrap();
engine.name = resolve(1, engine_regex.engine_replacement.as_ref(), &captures);
engine.major = capture_str(2, &captures).map(Cow::from);
engine.minor = capture_str(3, &captures).map(Cow::from);
engine.patch = capture_str(4, &captures).map(Cow::from);
break;
}
engine
}
}
impl FromStr for UserAgentParser {
type Err = UserAgentParserError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
UserAgentParser::from_str(s)
}
}