use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::surface::Strategy;
use crate::fingerprint::{Brand, UserAgentMetadata};
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct SurfaceCfg {
pub strategy: Option<Strategy>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct UaSpec {
pub ua_string: Option<String>,
pub platform: Option<String>,
#[serde(rename = "userAgentMetadata")]
pub ua_metadata: Option<UaMetadata>,
}
impl UaSpec {
#[must_use]
pub fn overlay(self, over: UaSpec) -> UaSpec {
UaSpec {
ua_string: over.ua_string.or(self.ua_string),
platform: over.platform.or(self.platform),
ua_metadata: match (self.ua_metadata, over.ua_metadata) {
(Some(base), Some(add)) => Some(base.overlay(add)),
(base, add) => add.or(base),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct UaMetadata {
pub brands: Option<Vec<Brand>>,
#[serde(rename = "fullVersionList")]
pub full_version_list: Option<Vec<Brand>>,
pub platform: Option<String>,
#[serde(rename = "platformVersion")]
pub platform_version: Option<String>,
pub architecture: Option<String>,
pub bitness: Option<String>,
pub mobile: Option<bool>,
pub model: Option<String>,
}
impl UaMetadata {
#[must_use]
pub fn overlay(self, over: UaMetadata) -> UaMetadata {
UaMetadata {
brands: over.brands.or(self.brands),
full_version_list: over.full_version_list.or(self.full_version_list),
platform: over.platform.or(self.platform),
platform_version: over.platform_version.or(self.platform_version),
architecture: over.architecture.or(self.architecture),
bitness: over.bitness.or(self.bitness),
mobile: over.mobile.or(self.mobile),
model: over.model.or(self.model),
}
}
#[must_use]
pub fn resolve(&self, base: &UserAgentMetadata) -> UserAgentMetadata {
UserAgentMetadata {
brands: self.brands.clone().unwrap_or_else(|| base.brands.clone()),
full_version_list: self
.full_version_list
.clone()
.unwrap_or_else(|| base.full_version_list.clone()),
platform: self
.platform
.clone()
.unwrap_or_else(|| base.platform.clone()),
platform_version: self
.platform_version
.clone()
.unwrap_or_else(|| base.platform_version.clone()),
architecture: self
.architecture
.clone()
.unwrap_or_else(|| base.architecture.clone()),
bitness: self.bitness.clone().unwrap_or_else(|| base.bitness.clone()),
wow64: base.wow64,
mobile: self.mobile.unwrap_or(base.mobile),
model: self.model.clone().unwrap_or_else(|| base.model.clone()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ScreenSpec {
pub width: u32,
pub height: u32,
pub device_pixel_ratio: f64,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct WebglSpec {
pub strategy: Option<Strategy>,
pub unmasked_vendor: Option<String>,
pub unmasked_renderer: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct WebgpuSpec {
pub strategy: Option<Strategy>,
pub vendor: Option<String>,
pub architecture: Option<String>,
pub device: Option<String>,
pub description: Option<String>,
pub limits: Option<BTreeMap<String, u64>>,
pub features: Option<Vec<String>>,
pub fabricate_when_absent: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct FontSpec {
pub strategy: Option<Strategy>,
pub available: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct WebrtcSpec {
pub strategy: Option<Strategy>,
pub fake_ip: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct HardwareSpec {
pub strategy: Option<Strategy>,
pub battery_level: Option<f64>,
pub media_devices: Option<u32>,
pub speech_voices: Option<Vec<String>>,
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn specs_round_trip_json() {
let w = WebglSpec {
strategy: Some(Strategy::Value),
unmasked_vendor: Some("Google Inc. (NVIDIA)".into()),
unmasked_renderer: Some("ANGLE (NVIDIA, ...)".into()),
};
let s = serde_json::to_string(&w).unwrap();
let back: WebglSpec = serde_json::from_str(&s).unwrap();
assert_eq!(w, back);
}
#[test]
fn ua_metadata_round_trips_json_camelcase() {
let m = UaMetadata {
brands: Some(vec![Brand {
brand: "Not_A Brand".into(),
version: "8".into(),
}]),
full_version_list: Some(vec![Brand {
brand: "Chromium".into(),
version: "150.0.7500.0".into(),
}]),
platform: Some("Windows".into()),
platform_version: Some("15.0.0".into()),
architecture: Some("x86".into()),
bitness: Some("64".into()),
mobile: Some(false),
model: Some(String::new()),
};
let s = serde_json::to_string(&m).unwrap();
assert!(
s.contains("\"fullVersionList\""),
"expected camelCase key, got: {s}"
);
assert!(
s.contains("\"platformVersion\""),
"expected camelCase key, got: {s}"
);
let back: UaMetadata = serde_json::from_str(&s).unwrap();
assert_eq!(m, back);
}
#[test]
fn ua_spec_round_trips_ua_metadata_under_camelcase_key() {
let spec = UaSpec {
ua_string: Some("Mozilla/5.0 (Windows NT 10.0; Win64; x64)".into()),
platform: Some("Windows".into()),
ua_metadata: Some(UaMetadata {
platform_version: Some("15.0.0".into()),
architecture: Some("arm".into()),
..Default::default()
}),
};
let s = serde_json::to_string(&spec).unwrap();
assert!(
s.contains("\"userAgentMetadata\""),
"expected camelCase field key, got: {s}"
);
let back: UaSpec = serde_json::from_str(&s).unwrap();
assert_eq!(spec, back);
}
#[test]
fn ua_spec_overlay_composes_ua_metadata_field_wise() {
let base = UaSpec {
platform: Some("Windows".into()),
ua_metadata: Some(UaMetadata {
brands: Some(vec![Brand {
brand: "X".into(),
version: "1".into(),
}]),
..Default::default()
}),
..Default::default()
};
let over = UaSpec {
ua_metadata: Some(UaMetadata {
platform_version: Some("11.0.0".into()),
..Default::default()
}),
..Default::default()
};
let merged = base.overlay(over);
assert_eq!(merged.platform.as_deref(), Some("Windows"));
let uam = merged.ua_metadata.expect("ua_metadata merged");
assert!(uam.brands.is_some(), "brands inherited from base");
assert_eq!(uam.platform_version.as_deref(), Some("11.0.0")); }
#[test]
fn ua_spec_overlay_one_sided_is_a_no_op_merge() {
let base = UaSpec {
ua_string: Some("base-ua".into()),
..Default::default()
};
let merged = base.clone().overlay(UaSpec::default());
assert_eq!(merged, base);
}
#[test]
fn ua_metadata_resolve_fills_unset_fields_from_base() {
let base = UserAgentMetadata::realistic(crate::Platform::Win32, 150, "150.0.7500.0");
let custom = UaMetadata {
platform_version: Some("11.0.0".into()),
..Default::default()
};
let resolved = custom.resolve(&base);
assert_eq!(resolved.platform_version, "11.0.0"); assert_eq!(resolved.brands, base.brands); assert_eq!(resolved.wow64, base.wow64); }
#[test]
fn ua_metadata_resolve_empty_equals_base_exactly() {
let base = UserAgentMetadata::realistic(crate::Platform::MacIntel, 148, "148.0.7778.181");
let resolved = UaMetadata::default().resolve(&base);
assert_eq!(resolved, base);
}
#[test]
fn screen_spec_round_trips_json() {
let s = ScreenSpec {
width: 1536,
height: 864,
device_pixel_ratio: 1.25,
};
let json = serde_json::to_string(&s).unwrap();
let back: ScreenSpec = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn empty_spec_omits_nothing_required() {
let f = FontSpec::default();
assert!(f.available.is_none());
}
#[test]
fn webgpu_spec_default_is_all_none() {
let w = WebgpuSpec::default();
assert!(w.strategy.is_none());
assert!(w.vendor.is_none());
assert!(w.architecture.is_none());
assert!(w.device.is_none());
assert!(w.description.is_none());
assert!(w.limits.is_none());
assert!(w.features.is_none());
assert!(w.fabricate_when_absent.is_none());
}
#[test]
fn webgpu_spec_round_trips_json() {
let mut limits = std::collections::BTreeMap::new();
limits.insert("maxTextureDimension2D".to_string(), 16384u64);
let w = WebgpuSpec {
strategy: Some(Strategy::Value),
vendor: Some("apple".into()),
architecture: Some("metal-3".into()),
device: Some("Apple M4 Pro".into()),
description: Some("Metal 3".into()),
limits: Some(limits),
features: Some(vec!["texture-compression-bc".into()]),
fabricate_when_absent: Some(true),
};
let s = serde_json::to_string(&w).unwrap();
let back: WebgpuSpec = serde_json::from_str(&s).unwrap();
assert_eq!(w, back);
}
}