use crate::identity::{Browser, Identity, Os};
use crate::error::IdentityError;
fn full_version(parts: &[u16]) -> String {
parts.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(".")
}
fn reduced_version(parts: &[u16]) -> String {
format!("{}.0.0.0", parts.first().copied().unwrap_or(0))
}
pub fn parse_chrome_version(ua: &str) -> Option<Vec<u16>> {
parse_version_parts(ua.split("Chrome/").nth(1)?.split_whitespace().next()?)
}
pub fn parse_version_parts(version: &str) -> Option<Vec<u16>> {
let parts = version
.split('.')
.map(|p| p.parse::<u16>().ok())
.collect::<Option<Vec<_>>>()?;
if parts.is_empty() { None } else { Some(parts) }
}
#[cfg(test)]
mod tests {
use super::{parse_chrome_version, reduced_version};
#[test]
fn ua_version_is_reduced_to_the_major() {
assert_eq!(reduced_version(&[146, 0, 7680, 153]), "146.0.0.0");
}
#[test]
fn parses_a_desktop_chrome_ua() {
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/146.0.7444.60 Safari/537.36";
assert_eq!(parse_chrome_version(ua), Some(vec![146, 0, 7444, 60]));
}
#[test]
fn parses_an_edge_ua_using_the_chromium_part() {
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/146.0.7444.60 Safari/537.36 Edg/146.0.3535.20";
assert_eq!(parse_chrome_version(ua), Some(vec![146, 0, 7444, 60]));
}
#[test]
fn rejects_a_ua_without_a_chromium_token() {
let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 \
(KHTML, like Gecko) Version/17.0 Safari/605.1.15";
assert_eq!(parse_chrome_version(ua), None);
}
}
pub fn build_user_agent(identity: &Identity) -> Result<String, IdentityError> {
let chrome_ua = reduced_version(&identity.browser_version);
let chrome_full = full_version(&identity.browser_version);
match (&identity.os, &identity.browser) {
(Os::Windows, Browser::Chrome) => {
let nt_version = windows_nt_version(&identity.os_version);
Ok(format!(
"Mozilla/5.0 (Windows NT {}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
nt_version, chrome_ua
))
}
(Os::Macintosh, Browser::Chrome) => {
let osx_underscored = identity.os_version.replace('.', "_");
Ok(format!(
"Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
osx_underscored, chrome_ua
))
}
(Os::Android, Browser::Chrome) => {
let device = identity.device_model.as_deref().unwrap_or("Pixel 7");
Ok(format!(
"Mozilla/5.0 (Linux; Android {}; {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Mobile Safari/537.36",
identity.os_version, device, chrome_ua
))
}
(Os::Android, Browser::Edge) => {
let device = identity.device_model.as_deref().unwrap_or("Pixel 7");
Ok(format!(
"Mozilla/5.0 (Linux; Android {}; {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Mobile EdgA/{} Safari/537.36",
identity.os_version, device, chrome_ua, chrome_ua
))
}
(Os::Ios, Browser::Safari) => {
let os_underscored = identity.os_version.replace('.', "_");
let nav_platform = identity.platform.navigator_platform.as_str();
Ok(format!(
"Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} Mobile/15E148 Safari/604.1",
nav_platform, os_underscored, full_version(&identity.browser_version)
))
}
(Os::Ios, Browser::Chrome) => {
let os_underscored = identity.os_version.replace('.', "_");
let nav_platform = identity.platform.navigator_platform.as_str();
Ok(format!(
"Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/{} Mobile/15E148 Safari/604.1",
nav_platform, os_underscored, chrome_full
))
}
(Os::Ios, Browser::Edge) => {
let os_underscored = identity.os_version.replace('.', "_");
let nav_platform = identity.platform.navigator_platform.as_str();
Ok(format!(
"Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} EdgiOS/{} Mobile/15E148 Safari/604.1",
nav_platform, os_underscored, full_version(&identity.browser_version), chrome_full
))
}
(Os::Macintosh, Browser::Safari) => {
let osx_underscored = identity.os_version.replace('.', "_");
Ok(format!(
"Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} Safari/605.1.15",
osx_underscored, full_version(&identity.browser_version)
))
}
(Os::Linux, Browser::Chrome) => {
Ok(format!(
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
chrome_ua
))
}
(Os::Windows, Browser::Edge) => {
let nt_version = windows_nt_version(&identity.os_version);
Ok(format!(
"Mozilla/5.0 (Windows NT {}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36 Edg/{}",
nt_version, chrome_ua, chrome_ua
))
}
(Os::Macintosh, Browser::Edge) => {
let osx_underscored = identity.os_version.replace('.', "_");
Ok(format!(
"Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36 Edg/{}",
osx_underscored, chrome_ua, chrome_ua
))
}
(os, browser) => Err(IdentityError::UaError(format!(
"unsupported OS/browser combination: {:?}/{:?}",
os, browser
))),
}
}
fn windows_nt_version(os_version: &str) -> &str {
match os_version {
"11" => "10.0",
"10" => "10.0",
"8.1" => "6.3",
"8" => "6.2",
"7" => "6.1",
_ => "10.0",
}
}