use serde::{Deserialize, Serialize};
use crate::{noise::NoiseSeed, webgl_noise::WebGlProfile};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Os {
Windows,
MacOs,
Linux,
Android,
Ios,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlatformProfile {
pub os: Os,
pub os_version: String,
pub platform_string: String,
pub max_touch_points: u8,
pub keyboard_layout: String,
}
impl PlatformProfile {
#[must_use]
pub fn windows() -> Self {
Self {
os: Os::Windows,
os_version: "10.0.0".into(),
platform_string: "Win32".into(),
max_touch_points: 0,
keyboard_layout: "en-US".into(),
}
}
#[must_use]
pub fn macos() -> Self {
Self {
os: Os::MacOs,
os_version: "14.0.0".into(),
platform_string: "MacIntel".into(),
max_touch_points: 0,
keyboard_layout: "en-US".into(),
}
}
#[must_use]
pub fn linux() -> Self {
Self {
os: Os::Linux,
os_version: "5.15.0".into(),
platform_string: "Linux x86_64".into(),
max_touch_points: 0,
keyboard_layout: "en-US".into(),
}
}
#[must_use]
pub fn android() -> Self {
Self {
os: Os::Android,
os_version: "13".into(),
platform_string: "Linux armv81".into(),
max_touch_points: 5,
keyboard_layout: "en-US".into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BrowserKind {
Chrome,
Edge,
Firefox,
Safari,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowserProfile {
pub kind: BrowserKind,
pub version: u32,
pub user_agent: String,
pub sec_ch_ua: String,
pub sec_ch_ua_platform: String,
pub sec_ch_ua_mobile: String,
}
impl BrowserProfile {
#[must_use]
pub fn chrome_136_windows() -> Self {
Self {
kind: BrowserKind::Chrome,
version: 136,
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36".into(),
sec_ch_ua: r#""Chromium";v="136", "Google Chrome";v="136", "Not-A.Brand";v="99""#.into(),
sec_ch_ua_platform: "\"Windows\"".into(),
sec_ch_ua_mobile: "?0".into(),
}
}
#[must_use]
pub fn chrome_136_macos() -> Self {
Self {
kind: BrowserKind::Chrome,
version: 136,
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36".into(),
sec_ch_ua: r#""Chromium";v="136", "Google Chrome";v="136", "Not-A.Brand";v="99""#.into(),
sec_ch_ua_platform: "\"macOS\"".into(),
sec_ch_ua_mobile: "?0".into(),
}
}
#[must_use]
pub fn chrome_136_linux() -> Self {
Self {
kind: BrowserKind::Chrome,
version: 136,
user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36".into(),
sec_ch_ua: r#""Chromium";v="136", "Google Chrome";v="136", "Not-A.Brand";v="99""#.into(),
sec_ch_ua_platform: "\"Linux\"".into(),
sec_ch_ua_mobile: "?0".into(),
}
}
#[must_use]
pub fn edge_136_windows() -> Self {
Self {
kind: BrowserKind::Edge,
version: 136,
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0".into(),
sec_ch_ua: r#""Chromium";v="136", "Microsoft Edge";v="136", "Not-A.Brand";v="99""#.into(),
sec_ch_ua_platform: "\"Windows\"".into(),
sec_ch_ua_mobile: "?0".into(),
}
}
#[must_use]
pub fn chrome_136_android() -> Self {
Self {
kind: BrowserKind::Chrome,
version: 136,
user_agent: "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Mobile Safari/537.36".into(),
sec_ch_ua: r#""Chromium";v="136", "Google Chrome";v="136", "Not-A.Brand";v="99""#.into(),
sec_ch_ua_platform: "\"Android\"".into(),
sec_ch_ua_mobile: "?1".into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScreenProfile {
pub width: u32,
pub height: u32,
pub avail_width: u32,
pub avail_height: u32,
pub dpr: f64,
pub color_depth: u8,
pub orientation: String,
}
impl ScreenProfile {
#[must_use]
pub fn fhd_desktop() -> Self {
Self {
width: 1920,
height: 1080,
avail_width: 1920,
avail_height: 1040,
dpr: 1.0,
color_depth: 24,
orientation: "landscape-primary".into(),
}
}
#[must_use]
pub fn qhd_desktop() -> Self {
Self {
width: 2560,
height: 1440,
avail_width: 2560,
avail_height: 1400,
dpr: 1.0,
color_depth: 24,
orientation: "landscape-primary".into(),
}
}
#[must_use]
pub fn macbook_pro_14() -> Self {
Self {
width: 1512,
height: 982,
avail_width: 1512,
avail_height: 957,
dpr: 2.0,
color_depth: 24,
orientation: "landscape-primary".into(),
}
}
#[must_use]
pub fn pixel_7() -> Self {
Self {
width: 393,
height: 851,
avail_width: 393,
avail_height: 851,
dpr: 2.75,
color_depth: 24,
orientation: "portrait-primary".into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HardwareProfile {
pub cores: u32,
pub memory_gb: u32,
pub gpu_vendor: String,
pub gpu_renderer: String,
}
impl HardwareProfile {
#[must_use]
pub fn desktop_gaming() -> Self {
Self {
cores: 8,
memory_gb: 8,
gpu_vendor: "NVIDIA".into(),
gpu_renderer: "NVIDIA GeForce RTX 3060".into(),
}
}
#[must_use]
pub fn desktop_gtx1660() -> Self {
Self {
cores: 8,
memory_gb: 8,
gpu_vendor: "NVIDIA".into(),
gpu_renderer: "NVIDIA GeForce GTX 1660 Ti".into(),
}
}
#[must_use]
pub fn apple_m1() -> Self {
Self {
cores: 8,
memory_gb: 8,
gpu_vendor: "Apple".into(),
gpu_renderer: "Apple M1".into(),
}
}
#[must_use]
pub fn intel_uhd_630() -> Self {
Self {
cores: 4,
memory_gb: 4,
gpu_vendor: "Intel".into(),
gpu_renderer: "Intel UHD Graphics 630".into(),
}
}
#[must_use]
pub fn mobile_snapdragon() -> Self {
Self {
cores: 8,
memory_gb: 4,
gpu_vendor: "Qualcomm".into(),
gpu_renderer: "Adreno (TM) 730".into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkProfile {
pub rtt: u32,
pub downlink: f64,
pub effective_type: String,
pub save_data: bool,
}
impl NetworkProfile {
#[must_use]
pub fn fast_wifi() -> Self {
Self {
rtt: 50,
downlink: 10.0,
effective_type: "4g".into(),
save_data: false,
}
}
#[must_use]
pub fn mobile_4g() -> Self {
Self {
rtt: 100,
downlink: 5.0,
effective_type: "4g".into(),
save_data: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FingerprintProfile {
pub name: String,
pub platform: PlatformProfile,
pub browser: BrowserProfile,
pub screen: ScreenProfile,
pub hardware: HardwareProfile,
pub webgl: WebGlProfile,
pub network: NetworkProfile,
pub noise_seed: NoiseSeed,
}
impl FingerprintProfile {
#[must_use]
pub fn windows_chrome_136_rtx3060() -> Self {
Self {
name: "windows-chrome-136-rtx3060".into(),
platform: PlatformProfile::windows(),
browser: BrowserProfile::chrome_136_windows(),
screen: ScreenProfile::fhd_desktop(),
hardware: HardwareProfile::desktop_gaming(),
webgl: WebGlProfile::nvidia_rtx_3060(),
network: NetworkProfile::fast_wifi(),
noise_seed: NoiseSeed::from(0x3c1b_6a2d_f5e0_9874_u64),
}
}
#[must_use]
pub fn windows_chrome_136_gtx1660() -> Self {
Self {
name: "windows-chrome-136-gtx1660".into(),
platform: PlatformProfile::windows(),
browser: BrowserProfile::chrome_136_windows(),
screen: ScreenProfile::qhd_desktop(),
hardware: HardwareProfile::desktop_gtx1660(),
webgl: WebGlProfile::nvidia_gtx_1660(),
network: NetworkProfile::fast_wifi(),
noise_seed: NoiseSeed::from(0x7a8e_2c3d_b4f1_5609_u64),
}
}
#[must_use]
pub fn macos_chrome_136_m1() -> Self {
Self {
name: "macos-chrome-136-m1".into(),
platform: PlatformProfile::macos(),
browser: BrowserProfile::chrome_136_macos(),
screen: ScreenProfile::macbook_pro_14(),
hardware: HardwareProfile::apple_m1(),
webgl: WebGlProfile::intel_uhd_630(), network: NetworkProfile::fast_wifi(),
noise_seed: NoiseSeed::from(0x1d2e_3f4a_5b6c_7d8e_u64),
}
}
#[must_use]
pub fn linux_chrome_136_intel() -> Self {
Self {
name: "linux-chrome-136-intel".into(),
platform: PlatformProfile::linux(),
browser: BrowserProfile::chrome_136_linux(),
screen: ScreenProfile::fhd_desktop(),
hardware: HardwareProfile::intel_uhd_630(),
webgl: WebGlProfile::intel_uhd_630(),
network: NetworkProfile::fast_wifi(),
noise_seed: NoiseSeed::from(0x9f8e_7d6c_5b4a_3021_u64),
}
}
#[must_use]
pub fn windows_edge_136_rtx3060() -> Self {
Self {
name: "windows-edge-136-rtx3060".into(),
platform: PlatformProfile::windows(),
browser: BrowserProfile::edge_136_windows(),
screen: ScreenProfile::fhd_desktop(),
hardware: HardwareProfile::desktop_gaming(),
webgl: WebGlProfile::nvidia_rtx_3060(),
network: NetworkProfile::fast_wifi(),
noise_seed: NoiseSeed::from(0x2b4d_6f80_a2c4_e6f8_u64),
}
}
#[must_use]
pub fn android_chrome_136_pixel() -> Self {
Self {
name: "android-chrome-136-pixel".into(),
platform: PlatformProfile::android(),
browser: BrowserProfile::chrome_136_android(),
screen: ScreenProfile::pixel_7(),
hardware: HardwareProfile::mobile_snapdragon(),
webgl: WebGlProfile::amd_rx_6700(), network: NetworkProfile::mobile_4g(),
noise_seed: NoiseSeed::from(0x4c8a_0f1e_2d3b_5a69_u64),
}
}
#[must_use]
pub fn random_weighted() -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0xdead_beef_1234_5678, |d| {
d.as_secs() ^ u64::from(d.subsec_nanos())
});
let v = seed
.wrapping_add(0x9e37_79b9_7f4a_7c15_u64)
.wrapping_mul(0xbf58_476d_1ce4_e5b9);
let pick = v % 100;
match pick {
0..=64 => Self::windows_chrome_136_rtx3060(), 65..=84 => Self::macos_chrome_136_m1(), 85..=89 => Self::linux_chrome_136_intel(), _ => Self::android_chrome_136_pixel(), }
}
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors: Vec<String> = Vec::new();
if matches!(self.platform.os, Os::Android | Os::Ios) && self.platform.max_touch_points == 0
{
errors.push(format!(
"Mobile OS {:?} must have max_touch_points > 0 (got 0)",
self.platform.os
));
}
if matches!(self.platform.os, Os::Windows | Os::MacOs | Os::Linux)
&& self.platform.max_touch_points > 0
&& self.platform.max_touch_points < 5
{
errors.push(format!(
"Desktop OS {:?} has suspicious max_touch_points {} (expected 0 or ≥5 for touch-screen)",
self.platform.os, self.platform.max_touch_points
));
}
if self.hardware.cores == 0 || self.hardware.cores > 128 {
errors.push(format!(
"hardwareConcurrency {} is out of range [1, 128]",
self.hardware.cores
));
}
let mem = self.hardware.memory_gb;
if mem == 0 || !mem.is_power_of_two() || mem > 32 {
errors.push(format!(
"deviceMemory {mem} GB is not a power-of-two in [1, 32]"
));
}
if self.screen.width == 0 || self.screen.height == 0 {
errors.push("screen width/height must be non-zero".into());
}
if self.screen.avail_width > self.screen.width
|| self.screen.avail_height > self.screen.height
{
errors.push(format!(
"avail_width/avail_height ({}/{}) must be ≤ screen size ({}/{})",
self.screen.avail_width,
self.screen.avail_height,
self.screen.width,
self.screen.height
));
}
if self.screen.dpr <= 0.0 {
errors.push(format!("dpr {} must be > 0", self.screen.dpr));
}
if self.platform.os == Os::Windows && self.platform.platform_string.contains("Mac") {
errors.push(format!(
"Windows OS has macOS-indicating platform_string '{}'",
self.platform.platform_string
));
}
if self.platform.os == Os::MacOs && self.platform.platform_string.contains("Win") {
errors.push(format!(
"macOS OS has Windows-indicating platform_string '{}'",
self.platform.platform_string
));
}
let is_mobile_ua = self.browser.sec_ch_ua_mobile == "?1";
let is_mobile_os = matches!(self.platform.os, Os::Android | Os::Ios);
if is_mobile_ua != is_mobile_os {
errors.push(format!(
"sec_ch_ua_mobile '{}' inconsistent with OS {:?}",
self.browser.sec_ch_ua_mobile, self.platform.os
));
}
let (vpw, vph) = self.webgl.max_viewport_dims;
if self.webgl.max_texture_size > vpw || self.webgl.max_texture_size > vph {
errors.push(format!(
"WebGL max_texture_size {} exceeds max_viewport_dims ({},{})",
self.webgl.max_texture_size, vpw, vph
));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_builtin_profiles_are_valid() {
let profiles = [
FingerprintProfile::windows_chrome_136_rtx3060(),
FingerprintProfile::windows_chrome_136_gtx1660(),
FingerprintProfile::macos_chrome_136_m1(),
FingerprintProfile::linux_chrome_136_intel(),
FingerprintProfile::windows_edge_136_rtx3060(),
FingerprintProfile::android_chrome_136_pixel(),
];
for p in &profiles {
let validation = p.validate();
assert!(
validation.is_ok(),
"profile '{}' failed validation: {validation:?}",
p.name
);
}
}
#[test]
fn inconsistent_profile_fails_validation() {
let mut p = FingerprintProfile::windows_chrome_136_rtx3060();
p.platform.platform_string = "MacIntel".into();
let result = p.validate();
assert!(result.is_err(), "expected validation failure");
let Err(errs) = result else {
return;
};
assert!(
errs.iter().any(|e| e.contains("macOS-indicating")),
"expected cross-OS platform_string error, got: {errs:?}"
);
}
#[test]
fn inconsistent_mobile_fails_validation() {
let mut p = FingerprintProfile::android_chrome_136_pixel();
p.platform.max_touch_points = 0;
assert!(
p.validate().is_err(),
"mobile with 0 touch points should fail"
);
}
#[test]
fn non_power_of_two_memory_fails() {
let mut p = FingerprintProfile::windows_chrome_136_rtx3060();
p.hardware.memory_gb = 6;
assert!(p.validate().is_err(), "6 GB is not a power of 2");
}
#[test]
fn random_weighted_is_valid() {
for _ in 0..20 {
let p = FingerprintProfile::random_weighted();
let validation = p.validate();
assert!(
validation.is_ok(),
"random_weighted() produced invalid profile '{}': {validation:?}",
p.name,
);
}
}
#[test]
fn random_weighted_windows_majority() {
let n = 1000_usize;
let windows_count = (0..n)
.filter(|_| FingerprintProfile::random_weighted().platform.os == Os::Windows)
.count();
assert!(
windows_count > 550,
"expected Windows > 55% in {n} samples, got {windows_count}"
);
}
#[test]
fn toml_round_trip() {
let p = FingerprintProfile::windows_chrome_136_rtx3060();
let toml_result = toml::to_string(&p);
assert!(
toml_result.is_ok(),
"serialize to TOML failed: {toml_result:?}"
);
let Ok(toml_str) = toml_result else {
return;
};
let profile_result: Result<FingerprintProfile, _> = toml::from_str(&toml_str);
assert!(
profile_result.is_ok(),
"deserialize from TOML failed: {profile_result:?}"
);
let Ok(p2) = profile_result else {
return;
};
assert_eq!(p.name, p2.name);
assert_eq!(p.hardware.cores, p2.hardware.cores);
assert_eq!(p.noise_seed.as_u64(), p2.noise_seed.as_u64());
}
}