use std::fmt;
use std::ops::RangeInclusive;
use crate::{Browser, Platform, Profile, Result, language};
#[derive(Clone)]
pub struct UserAgents {
browsers: Vec<(Browser, u32)>,
platforms: Vec<Platform>,
versions: Vec<(Browser, RangeInclusive<u32>)>,
language: Option<String>,
}
impl Default for UserAgents {
fn default() -> Self {
Self {
browsers: [
Browser::Chrome,
Browser::Safari,
Browser::Edge,
Browser::Firefox,
]
.map(|browser| (browser, browser.default_share()))
.to_vec(),
platforms: Platform::DESKTOP.to_vec(),
versions: Vec::new(),
language: None,
}
}
}
impl UserAgents {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn browsers(mut self, browsers: impl IntoIterator<Item = Browser>) -> Self {
let mut unique = Vec::new();
for browser in browsers {
if !unique.contains(&browser) {
unique.push(browser);
}
}
self.browsers = unique
.into_iter()
.map(|browser| {
let share = self
.browsers
.iter()
.find(|(other, _)| *other == browser)
.map_or_else(|| browser.default_share(), |(_, share)| *share);
(browser, share)
})
.collect();
self.check();
self
}
#[must_use]
pub fn share(mut self, browser: Browser, share: u32) -> Self {
self.browsers.retain(|(other, _)| *other != browser);
if share > 0 {
self.browsers.push((browser, share));
}
self.check();
self
}
#[must_use]
pub fn platforms(mut self, platforms: impl IntoIterator<Item = Platform>) -> Self {
self.platforms = platforms.into_iter().collect();
self.check();
self
}
#[must_use]
pub fn versions(mut self, browser: Browser, versions: RangeInclusive<u32>) -> Self {
assert!(
!versions.is_empty() && *versions.start() > 0,
"`UserAgents::versions` needs versions above zero"
);
self.versions.retain(|(other, _)| *other != browser);
self.versions.push((browser, versions));
self
}
pub fn language(mut self, language: &str) -> Result<Self> {
language::validate(language)?;
self.language = Some(language.to_owned());
Ok(self)
}
fn check(&self) {
assert!(
self.browsers.iter().any(|(browser, _)| {
browser
.platforms()
.iter()
.any(|platform| self.platforms.contains(platform))
}),
"`UserAgents` has no browser that runs on its platforms"
);
}
#[must_use]
pub fn pick(&self, n: u64) -> Profile {
let mut draws = Draws(n);
let browsers: Vec<(Browser, u32)> = self
.browsers
.iter()
.copied()
.filter(|(browser, _)| {
browser
.platforms()
.iter()
.any(|platform| self.platforms.contains(platform))
})
.collect();
let browser = weighted(&browsers, draws.next());
let platforms: Vec<(Platform, u32)> = browser
.platforms()
.iter()
.copied()
.filter(|platform| self.platforms.contains(platform))
.map(|platform| (platform, browser.share(platform)))
.collect();
let platform = weighted(&platforms, draws.next());
let versions = self
.versions
.iter()
.find(|(other, _)| *other == browser)
.map_or_else(
|| browser.default_versions(),
|(_, versions)| versions.clone(),
);
let span = u64::from(versions.end() - versions.start()) + 1;
let offset = u32::try_from(draws.next() % span).unwrap_or_default();
let version = versions.start() + offset;
let mut profile = Profile::new(browser, platform)
.and_then(|profile| profile.with_version(version))
.unwrap_or_else(|_| unreachable!("browsers are only picked on their platforms"));
profile.language.clone_from(&self.language);
profile
}
}
impl fmt::Debug for UserAgents {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UserAgents")
.field("browsers", &self.browsers)
.field("platforms", &self.platforms)
.field("versions", &self.versions)
.finish_non_exhaustive()
}
}
fn weighted<T: Copy>(choices: &[(T, u32)], draw: u64) -> T {
let total: u64 = choices.iter().map(|(_, weight)| u64::from(*weight)).sum();
let mut point = draw % total.max(1);
for (choice, weight) in choices {
let weight = u64::from(*weight);
if point < weight {
return *choice;
}
point -= weight;
}
choices[0].0
}
struct Draws(u64);
impl Draws {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
}