use std::borrow::Cow;
use crate::{Browser, Error, Header, Platform, Result, language};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Profile {
browser: Browser,
version: u32,
platform: Platform,
pub(crate) language: Option<String>,
}
impl Profile {
pub fn new(browser: Browser, platform: Platform) -> Result<Self> {
if !browser.platforms().contains(&platform) {
return Err(Error::UnsupportedPlatform { browser, platform });
}
Ok(Self {
browser,
version: browser.latest(),
platform,
language: None,
})
}
pub fn with_version(mut self, version: u32) -> Result<Self> {
if version == 0 {
return Err(Error::InvalidVersion);
}
self.version = version;
Ok(self)
}
#[must_use]
pub const fn with_latest_version(mut self) -> Self {
self.version = self.browser.latest();
self
}
pub fn with_language(mut self, language: &str) -> Result<Self> {
language::validate(language)?;
self.language = Some(language.to_owned());
Ok(self)
}
#[must_use]
pub const fn browser(&self) -> Browser {
self.browser
}
#[must_use]
pub const fn version(&self) -> u32 {
self.version
}
#[must_use]
pub const fn platform(&self) -> Platform {
self.platform
}
#[must_use]
pub fn user_agent(&self) -> String {
let template = self
.browser
.headers()
.iter()
.find_map(|header| (header.name == "user-agent").then_some(header.value.as_ref()))
.unwrap_or_else(|| unreachable!("every browser capture has a user agent"));
let latest = self.browser.latest();
let version = self.version;
let mut agent = template.to_owned();
for product in ["Chrome/", "Edg/", "Firefox/", "Version/", "rv:"] {
agent = agent.replace(
&format!("{product}{latest}."),
&format!("{product}{version}."),
);
}
let platform = match (self.browser, self.platform) {
(Browser::Firefox, Platform::MacOs) => "Macintosh; Intel Mac OS X 10.15",
(Browser::Firefox, Platform::Android) => "Android 15; Mobile",
(_, Platform::Windows) => "Windows NT 10.0; Win64; x64",
(_, Platform::MacOs) => "Macintosh; Intel Mac OS X 10_15_7",
(_, Platform::Linux) => "X11; Linux x86_64",
(_, Platform::Android) => "Linux; Android 10; K",
(_, Platform::Ios) => "iPhone; CPU iPhone OS 18_6 like Mac OS X",
};
if let Some(start) = agent.find('(')
&& let Some(end) = agent[start..].find(')')
{
let platform = if self.browser == Browser::Firefox {
format!("{platform}; rv:{version}.0")
} else {
platform.to_owned()
};
agent.replace_range(start + 1..start + end, &platform);
}
if self.platform == Platform::Android {
if self.browser == Browser::Firefox {
agent = agent.replace("Gecko/20100101", &format!("Gecko/{version}.0"));
} else {
agent = agent.replace(" Safari/", " Mobile Safari/");
}
} else if self.platform == Platform::Ios {
agent = agent.replace(" Safari/605.1.15", " Mobile/15E148 Safari/604.1");
}
agent
}
fn brands(&self) -> String {
const GREASE_CHARS: [&str; 11] = [" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"];
const GREASE_VERSIONS: [&str; 3] = ["8", "99", "24"];
const ORDERS: [[usize; 3]; 6] = [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
];
let seed = usize::try_from(self.version).expect("u32 fits usize on supported targets");
let grease = format!(
"\"Not{}A{}Brand\";v=\"{}\"",
GREASE_CHARS[seed % GREASE_CHARS.len()],
GREASE_CHARS[(seed + 1) % GREASE_CHARS.len()],
GREASE_VERSIONS[seed % GREASE_VERSIONS.len()],
);
let brand = match self.browser {
Browser::Edge => "Microsoft Edge",
_ => "Google Chrome",
};
let order = ORDERS[seed % ORDERS.len()];
let mut brands = [String::new(), String::new(), String::new()];
brands[order[0]] = grease;
brands[order[1]] = format!("\"Chromium\";v=\"{}\"", self.version);
brands[order[2]] = format!("\"{brand}\";v=\"{}\"", self.version);
brands.join(", ")
}
#[must_use]
pub fn headers(&self) -> Vec<Header> {
self.browser
.headers()
.iter()
.map(|header| {
let value = match header.name {
"user-agent" => Cow::Owned(self.user_agent()),
"sec-ch-ua" => Cow::Owned(self.brands()),
"sec-ch-ua-mobile" => Cow::Borrowed(if self.platform.is_mobile() {
"?1"
} else {
"?0"
}),
"sec-ch-ua-platform" => Cow::Borrowed(self.platform.client_hint()),
"accept-language" => self.language.as_ref().map_or_else(
|| header.value.clone(),
|language| Cow::Owned(language.clone()),
),
_ => header.value.clone(),
};
Header {
name: header.name,
value,
}
})
.collect()
}
}