use crate::profile::{BrowserKind, FingerprintProfile};
#[must_use]
pub fn navigator_coherence_script(profile: &FingerprintProfile) -> String {
let cores = profile.hardware.cores;
let memory = profile.hardware.memory_gb;
let rtt = profile.network.rtt;
let downlink = profile.network.downlink;
let effective_type = &profile.network.effective_type;
let save_data = profile.network.save_data;
let max_touch = profile.platform.max_touch_points;
let pdf_viewer_enabled = matches!(
profile.browser.kind,
BrowserKind::Chrome | BrowserKind::Edge
);
let languages_js = build_languages_js(profile);
let plugins_js = build_plugins_js(profile);
let ua_data_js = build_ua_data_js(profile);
let user_agent = &profile.browser.user_agent;
let platform_string = &profile.platform.platform_string;
format!(
r"(function() {{
'use strict';
// ── toString spoof utility ───────────────────────────────────────────────
function _nts(name) {{ return function toString() {{ return 'function ' + name + '() {{ [native code] }}'; }}; }}
function _def(obj, prop, val) {{
Object.defineProperty(obj, prop, {{ value: val, writable: false, configurable: false, enumerable: false }});
}}
function _defGetter(obj, prop, getter) {{
getter.toString = _nts('get ' + prop);
Object.defineProperty(obj, prop, {{ get: getter, configurable: false, enumerable: true }});
}}
// ── 1. hardwareConcurrency ───────────────────────────────────────────────
_defGetter(Navigator.prototype, 'hardwareConcurrency', function() {{ return {cores}; }});
// ── 2. deviceMemory ─────────────────────────────────────────────────────
if ('deviceMemory' in navigator) {{
_defGetter(Navigator.prototype, 'deviceMemory', function() {{ return {memory}; }});
}}
// ── 3. connection (NetworkInformation) ──────────────────────────────────
const _conn = Object.create(EventTarget.prototype);
_def(_conn, 'rtt', {rtt});
_def(_conn, 'downlink', {downlink});
_def(_conn, 'effectiveType', '{effective_type}');
_def(_conn, 'saveData', {save_data_js});
_def(_conn, 'onchange', null);
_defGetter(Navigator.prototype, 'connection', function() {{ return _conn; }});
// ── 4. maxTouchPoints ───────────────────────────────────────────────────
_defGetter(Navigator.prototype, 'maxTouchPoints', function() {{ return {max_touch}; }});
// ── 5. languages ─────────────────────────────────────────────────────────
const _langs = {languages_js};
_defGetter(Navigator.prototype, 'languages', function() {{ return _langs; }});
_defGetter(Navigator.prototype, 'language', function() {{ return _langs[0] || 'en-US'; }});
// ── 6. pdfViewerEnabled ──────────────────────────────────────────────────
_defGetter(Navigator.prototype, 'pdfViewerEnabled', function() {{ return {pdf_viewer_enabled_js}; }});
// ── 7. plugins + mimeTypes ───────────────────────────────────────────────
{plugins_js}
// ── 8. userAgent + platform ─────────────────────────────────────────────
_defGetter(Navigator.prototype, 'userAgent', function() {{ return '{user_agent}'; }});
_defGetter(Navigator.prototype, 'platform', function() {{ return '{platform_string}'; }});
_defGetter(Navigator.prototype, 'appVersion', function() {{
return '{user_agent}'.replace('Mozilla/', '');
}});
// ── 9. userAgentData (Client Hints) ─────────────────────────────────────
{ua_data_js}
}})();
",
cores = cores,
memory = memory,
rtt = rtt,
downlink = downlink,
effective_type = effective_type,
save_data_js = if save_data { "true" } else { "false" },
max_touch = max_touch,
languages_js = languages_js,
pdf_viewer_enabled_js = if pdf_viewer_enabled { "true" } else { "false" },
plugins_js = plugins_js,
user_agent = user_agent.replace('\'', "\\'"),
platform_string = platform_string.replace('\'', "\\'"),
ua_data_js = ua_data_js,
)
}
fn build_languages_js(profile: &FingerprintProfile) -> String {
let _ = profile.browser.sec_ch_ua_mobile == "?1";
"['en-US', 'en']".into()
}
fn build_plugins_js(profile: &FingerprintProfile) -> String {
let is_chrome_like = matches!(
profile.browser.kind,
BrowserKind::Chrome | BrowserKind::Edge
);
if !is_chrome_like {
return String::new();
}
r"
(function() {
const _mimeTypes = [
{ type: 'application/pdf', suffixes: 'pdf', description: '' },
{ type: 'text/pdf', suffixes: 'pdf', description: '' },
];
const _pluginData = [
'PDF Viewer',
'Chrome PDF Viewer',
'Chromium PDF Viewer',
'Microsoft Edge PDF Viewer',
'WebKit built-in PDF',
];
function _makePlugin(name) {
const p = Object.create(Plugin.prototype);
Object.defineProperty(p, 'name', { value: name, enumerable: true });
Object.defineProperty(p, 'description', { value: '', enumerable: true });
Object.defineProperty(p, 'filename', { value: 'internal-pdf-viewer', enumerable: true });
Object.defineProperty(p, 'length', { value: _mimeTypes.length, enumerable: true });
_mimeTypes.forEach(function(mt, i) {
Object.defineProperty(p, i, { value: mt, enumerable: true });
});
p.item = function(i) { return _mimeTypes[i] || null; };
p.namedItem = function(n) { return _mimeTypes.find(function(m) { return m.type === n; }) || null; };
return p;
}
const _plugins = _pluginData.map(_makePlugin);
const _pluginArray = Object.create(PluginArray.prototype);
Object.defineProperty(_pluginArray, 'length', { value: _plugins.length, enumerable: true });
_plugins.forEach(function(p, i) { Object.defineProperty(_pluginArray, i, { value: p, enumerable: true }); });
_pluginArray.item = function(i) { return _plugins[i] || null; };
_pluginArray.namedItem = function(n) { return _plugins.find(function(p) { return p.name === n; }) || null; };
_pluginArray.refresh = function() {};
Object.defineProperty(Navigator.prototype, 'plugins', {
get: function() { return _pluginArray; }, configurable: false, enumerable: true
});
})();".into()
}
fn build_ua_data_js(profile: &FingerprintProfile) -> String {
let brands = parse_sec_ch_ua_brands(&profile.browser.sec_ch_ua);
let mobile = profile.browser.sec_ch_ua_mobile == "?1";
let platform = strip_quotes(&profile.browser.sec_ch_ua_platform);
let os_version = &profile.platform.os_version;
format!(
r"
if (typeof NavigatorUAData !== 'undefined' || 'userAgentData' in navigator) {{
const _brands = {brands};
const _uaData = {{
brands: _brands,
mobile: {mobile_js},
platform: '{platform}',
getHighEntropyValues: function(hints) {{
return Promise.resolve({{
architecture: 'x86',
model: '',
platform: '{platform}',
platformVersion: '{os_version}',
fullVersionList: _brands,
mobile: {mobile_js},
bitness: '64',
wow64: false,
}});
}},
toJSON: function() {{
return {{ brands: _brands, mobile: {mobile_js}, platform: '{platform}' }};
}},
}};
Object.defineProperty(Navigator.prototype, 'userAgentData', {{
get: function() {{ return _uaData; }}, configurable: false, enumerable: true
}});
}}",
brands = brands,
mobile_js = if mobile { "true" } else { "false" },
platform = platform,
os_version = os_version,
)
}
fn parse_sec_ch_ua_brands(sec_ch_ua: &str) -> String {
use std::fmt::Write;
let mut result = String::from('[');
for part in sec_ch_ua.split(',') {
let part = part.trim();
let mut iter = part.splitn(2, ";v=");
let brand_raw = iter.next().unwrap_or("").trim().trim_matches('"');
let version_raw = iter.next().unwrap_or("\"\"").trim().trim_matches('"');
if !brand_raw.is_empty() {
if result.len() > 1 {
result.push(',');
}
let _ = write!(
result,
"{{brand:\"{brand_raw}\",version:\"{version_raw}\"}}",
);
}
}
result.push(']');
result
}
fn strip_quotes(s: &str) -> String {
s.trim_matches('"').to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile::FingerprintProfile;
fn script_for(p: &FingerprintProfile) -> String {
navigator_coherence_script(p)
}
#[test]
fn script_overrides_all_nine_groups() {
let p = FingerprintProfile::windows_chrome_136_rtx3060();
let js = script_for(&p);
assert!(
js.contains("hardwareConcurrency"),
"missing hardwareConcurrency"
);
assert!(js.contains("deviceMemory"), "missing deviceMemory");
assert!(js.contains("connection"), "missing connection");
assert!(js.contains("maxTouchPoints"), "missing maxTouchPoints");
assert!(js.contains("languages"), "missing languages");
assert!(js.contains("pdfViewerEnabled"), "missing pdfViewerEnabled");
assert!(js.contains("plugins"), "missing plugins");
assert!(js.contains("userAgentData"), "missing userAgentData");
assert!(js.contains("userAgent"), "missing userAgent");
}
#[test]
fn hardware_concurrency_matches_profile() {
let p = FingerprintProfile::windows_chrome_136_rtx3060();
let js = script_for(&p);
assert!(
js.contains(&format!("return {};", p.hardware.cores)),
"hardwareConcurrency value not found"
);
}
#[test]
fn device_memory_matches_profile() {
let p = FingerprintProfile::windows_chrome_136_rtx3060();
let js = script_for(&p);
assert!(
js.contains(&format!("return {};", p.hardware.memory_gb)),
"deviceMemory value not found"
);
}
#[test]
fn connection_has_all_four_properties() {
let js = script_for(&FingerprintProfile::windows_chrome_136_rtx3060());
assert!(js.contains("rtt"), "missing rtt");
assert!(js.contains("downlink"), "missing downlink");
assert!(js.contains("effectiveType"), "missing effectiveType");
assert!(js.contains("saveData"), "missing saveData");
}
#[test]
fn plugins_count_five_for_chrome() {
let js = script_for(&FingerprintProfile::windows_chrome_136_rtx3060());
assert!(js.contains("PDF Viewer"), "missing PDF Viewer plugin");
assert!(
js.contains("Chrome PDF Viewer"),
"missing Chrome PDF Viewer"
);
assert!(
js.contains("Chromium PDF Viewer"),
"missing Chromium PDF Viewer"
);
assert!(
js.contains("Microsoft Edge PDF Viewer"),
"missing Edge plugin"
);
assert!(js.contains("WebKit built-in PDF"), "missing WebKit plugin");
}
#[test]
fn ua_data_brands_match_sec_ch_ua() {
let p = FingerprintProfile::windows_chrome_136_rtx3060();
let js = script_for(&p);
assert!(js.contains("Google Chrome"), "missing Google Chrome brand");
assert!(js.contains("Chromium"), "missing Chromium brand");
}
#[test]
fn max_touch_points_desktop_is_zero() {
let p = FingerprintProfile::windows_chrome_136_rtx3060();
assert_eq!(p.platform.max_touch_points, 0);
let js = script_for(&p);
assert!(js.contains("return 0;"), "maxTouchPoints not 0 on desktop");
}
#[test]
fn max_touch_points_mobile_is_nonzero() {
let p = FingerprintProfile::android_chrome_136_pixel();
assert!(p.platform.max_touch_points > 0);
let js = script_for(&p);
assert!(
js.contains(&format!("return {};", p.platform.max_touch_points)),
"maxTouchPoints not > 0 on mobile"
);
}
#[test]
fn parse_sec_ch_ua_brands_roundtrip() {
let input = r#""Chromium";v="136", "Google Chrome";v="136", "Not-A.Brand";v="99""#;
let out = parse_sec_ch_ua_brands(input);
assert!(out.contains("Chromium"), "Chromium not parsed");
assert!(out.contains("136"), "version 136 not parsed");
}
}