use serde::{Deserialize, Serialize};
use std::{
env, fs,
path::{Path, PathBuf},
};
use crate::error::PersonaConfigError;
use crate::fingerprint::*;
use crate::options::*;
use crate::seed::{BrowserPersonaPreset, PersonaSeed};
pub const DEFAULT_PERSONA_JSON: &str = "persona.json";
pub const PERSONA_JSON_ENV: &str = "BIMP_PERSONA_JSON";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PersonaConfig {
#[serde(default)]
pub preset: BrowserPersonaPreset,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub startup_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seed: Option<PersonaSeed>,
#[serde(default)]
pub viewport: Viewport,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub screen: Option<ScreenConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub window: Option<WindowConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub graphics: Option<GraphicsConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub navigator: Option<NavigatorConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub css: Option<CssConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media: Option<MediaConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fonts: Option<FontConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub canvas: Option<CanvasConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub domrect: Option<DomRectConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub engine: Option<EngineConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub languages: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accept_language: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timezone: Option<String>,
}
impl Default for PersonaConfig {
fn default() -> Self {
Self {
preset: BrowserPersonaPreset::ChromeStable,
startup_url: None,
seed: None,
viewport: Viewport::default(),
screen: None,
window: None,
graphics: None,
navigator: None,
css: None,
media: None,
fonts: None,
canvas: None,
domrect: None,
engine: None,
locale: None,
languages: None,
accept_language: None,
timezone: None,
}
}
}
impl PersonaConfig {
pub fn from_json_file(path: impl AsRef<Path>) -> Result<Self, PersonaConfigError> {
let contents = fs::read_to_string(path)?;
Ok(serde_json::from_str(&contents)?)
}
pub fn default_json_path() -> PathBuf {
env::var_os(PERSONA_JSON_ENV)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(DEFAULT_PERSONA_JSON))
}
pub fn load_default_json() -> Result<Option<Self>, PersonaConfigError> {
let path = Self::default_json_path();
if !path.exists() {
return Ok(None);
}
Self::from_json_file(path).map(Some)
}
pub fn resolve(&self) -> ResolvedPersona {
let seed = self.seed.clone().unwrap_or_else(|| {
PersonaSeed::from_stable_input(format!(
"preset:{:?}:{}x{}@{}",
self.preset,
self.viewport.width,
self.viewport.height,
self.viewport.device_scale_factor
))
});
let mut persona = ResolvedPersona::from_preset_and_seed(self.preset, seed);
persona.startup_url = self
.startup_url
.as_ref()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
persona.viewport = self.viewport.clone();
persona.screen = ScreenFingerprint::from_viewport(&persona.viewport);
if let Some(screen) = &self.screen {
screen.apply_to(&mut persona.screen);
}
persona.window = WindowFingerprint::from_viewport(&persona.viewport);
if let Some(window) = &self.window {
window.apply_to(&mut persona.window);
}
if let Some(graphics) = &self.graphics {
graphics.apply_to(&mut persona.graphics);
}
self.apply_locale_timezone(&mut persona);
if let Some(navigator) = &self.navigator {
navigator.apply_to(&mut persona.js);
if self.accept_language.is_none() && !persona.js.languages.is_empty() {
persona.network.accept_language = format_accept_language(&persona.js.languages);
}
}
if let Some(css) = &self.css {
css.apply_to(&mut persona.css);
}
if let Some(media) = &self.media {
media.apply_to(&mut persona.media);
}
if let Some(fonts) = &self.fonts {
fonts.apply_to(&mut persona.fonts);
}
if let Some(canvas) = &self.canvas {
canvas.apply_to(&mut persona.canvas);
}
if let Some(domrect) = &self.domrect {
domrect.apply_to(&mut persona.domrect);
}
if let Some(engine) = &self.engine {
engine.apply_to(&mut persona.engine);
}
persona
}
fn apply_locale_timezone(&self, persona: &mut ResolvedPersona) {
let mut languages = self
.languages
.clone()
.unwrap_or_else(|| parse_accept_language(self.accept_language.as_deref()));
if languages.is_empty()
&& let Some(locale) = self.locale.as_ref().filter(|value| !value.is_empty())
{
languages.push(locale.clone());
if let Some(primary) = primary_language_subtag(locale)
&& primary != *locale
{
languages.push(primary);
}
}
if let Some(locale) = self.locale.as_ref().filter(|value| !value.is_empty()) {
persona.js.language = locale.clone();
persona.geo.locale = locale.clone();
} else if let Some(locale) = languages.first() {
persona.js.language = locale.clone();
persona.geo.locale = locale.clone();
}
if !languages.is_empty() {
persona.js.languages = languages.clone();
}
if let Some(accept_language) = self
.accept_language
.as_ref()
.filter(|value| !value.is_empty())
{
persona.network.accept_language = accept_language.clone();
} else if !persona.js.languages.is_empty() {
persona.network.accept_language = format_accept_language(&persona.js.languages);
}
if let Some(timezone) = self.timezone.as_ref().filter(|value| !value.is_empty()) {
persona.js.timezone = timezone.clone();
persona.geo.timezone = timezone.clone();
}
}
}
fn primary_language_subtag(locale: &str) -> Option<String> {
locale
.split(['-', '_'])
.next()
.filter(|value| !value.is_empty())
.map(ToString::to_string)
}
fn parse_accept_language(header: Option<&str>) -> Vec<String> {
header
.unwrap_or_default()
.split(',')
.filter_map(|entry| {
entry
.split(';')
.next()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToString::to_string)
})
.collect()
}
fn format_accept_language(languages: &[String]) -> String {
languages
.iter()
.enumerate()
.map(|(index, language)| {
if index == 0 {
language.clone()
} else {
let quality = (10usize.saturating_sub(index)).max(1);
format!("{language};q=0.{quality}")
}
})
.collect::<Vec<_>>()
.join(",")
}