commit 60e55491eaf2a875ca9fb7416281b0cdd6c0eaba Author: feder-cr <85809106+feder-cr@users.noreply.github.com> Date: Tue May 12 21:34:14 2026 -0700 feat: initial public release Stealthfox — a patched Firefox 150.0.1 for browser-fingerprint stealth, shipped as a Playwright-compatible Python wrapper. * Sync + async Stealthfox launcher (firefox_user_prefs, virtual desktop on Windows, SOCKS5 auth via patched nsProtocolProxyService) * fpforge: Bayesian fingerprint sampler over GPU / audio / fonts / screen / TCP options / ~400 other navigator fields * WebRTC stealth: srflx address swap, synthetic srflx fallback, private-LAN host candidates — no real public IP leak via STUN * GPU sandbox fix for FF150 alt-desktop regression * Bezier-curve mouse motion baked into Juggler Targets Windows x86_64 + Linux x86_64. Binary fetched on first run from GitHub Release "firefox-1". diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6136de --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.pytest_cache/ +.venv/ +firefox-source/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2133737 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 stealthfox contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf62e65 --- /dev/null +++ b/README.md @@ -0,0 +1,226 @@ +# stealthfox + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) +[![Firefox 150.0.1](https://img.shields.io/badge/firefox-150.0.1-orange.svg)](https://www.mozilla.org/firefox/) +[![GitHub release](https://img.shields.io/github/v/release/feder-cr/stealthfox.svg)](https://github.com/feder-cr/stealthfox/releases) +[![GitHub stars](https://img.shields.io/github/stars/feder-cr/stealthfox.svg?style=social)](https://github.com/feder-cr/stealthfox/stargazers) + +A patched Firefox **100% Playwright-compatible** that passes the hardest browser-fingerprint detectors in the wild. + + +## Results + +These are the "best" outcomes observed across independent runs on residential proxies. + +### Google reCAPTCHA v3 - **0.90 / 1.0** + +Top-tier score. Google classifies the session as "very likely a human". Most anti-detect stacks plateau around 0.3-0.7. + +![reCAPTCHA score 0.90](docs/screenshots/recaptcha_score.png) + +### Fingerprint Pro - **bot: not detected, VPN: false, tampering: false, dev tools: not detected** + +FingerprintJS Pro's full Smart Signals battery flips every flag to "Not detected". Browser correctly identified as Firefox 150 on Windows 10. Confidence score 0.9. + +![FingerprintPro not detected](docs/screenshots/fingerprintpro.png) + +### CreepJS - **0 lies**, fingerprint is internally coherent + +No contradictions between headless hints, spoofed values, and real rendering output. That "0 lies" is what kills most anti-detect browsers: one inconsistency (e.g. Chrome UA + Firefox WebGL) and the trust score collapses. + +![CreepJS 0 lies](docs/screenshots/creepjs.png) + +### BrowserLeaks WebRTC - **no public IP leak** + +WebRTC srflx address is the proxy egress IP; host candidates are private LAN. The real public IP never leaks via STUN, even on pages that configure their own ICE servers. Stock Firefox leaks the real local IP via WebRTC mDNS - stealthfox doesn't. + +![WebRTC no leaks](docs/screenshots/webrtc.png) + +### bot.sannysoft.com - **all checks pass** + +Every row green: WebDriver not present, Chrome-only properties absent, plugin/mime/languages arrays coherent, permissions API correct, iframe/source window checks pass. + +![Sannysoft all green](docs/screenshots/sannysoft.png) + +--- + +## Why it's powerful + +**Most anti-detect browsers patch Chromium at the JavaScript level** - they override `navigator`, `WebGLRenderingContext.getParameter`, canvas APIs, and so on via injected scripts. This has two fatal problems: + +1. **JS patches are detectable.** Anti-bots enumerate native function `.toString()`, check descriptor configurability, compare property enumeration order, watch for prototype mutations. Every patch leaves a fingerprint of its own. CreepJS has an entire battery of "lies detectors" built around this. +2. **Chromium itself is now suspect.** Residential-proxy bot traffic is overwhelmingly Chromium-based, so detectors weight anything Chromium-shaped as risky by default. And the parts that matter (TLS stack, renderer process) are not fully open-source in Chrome proper - forks either inherit all Chromium tells or drift in visible ways. + +**stealthfox patches Firefox at the C++ level.** The spoofed values come back out through the normal Gecko paths - there is no JS shim, no override, no `Object.defineProperty`. **From the page's point of view, the browser is just telling the truth.** Anti-bot lie-detectors have nothing to latch onto. + +stealthfox spoofs **all the layers that matter, together, coherently**: + +| Layer | What we do | Why it matters | +|-------|-----------|-----------------| +| Navigator / hardware | C++ overrides: UA, oscpu, languages, hardwareConcurrency, deviceMemory, storage quota | Self-description coherent across every API | +| Screen / window / pointer | C++ patch: screen WxH, outerSize bound, media-query device-size, pointer/hover/touch capabilities | `screen.*`, `window.outer*`, CSS `@media (pointer: fine)` all coherent | +| CSS system colors | 40 `ui.*` Win32 palette overrides | `getComputedStyle()` on system colors matches real Windows | +| GPU / WebGL | C++ patch: vendor, renderer, extensions whitelist, integer/float params, shader precisions, readPixels noise | Matches real Windows ANGLE down to enum values | +| Canvas 2D | C++ patch: per-pixel substitution + geometry skip-mask noise + TextMetrics variance | Defeats canvas hashing and text-metrics fingerprinting | +| Fonts / DirectWrite | C++ patch: family whitelist + fabricated authoritative list + per-family width scale + DWrite settings | Font enumeration matches real Win10; canvas text hash stable | +| Audio | C++ patch: sampleRate + output latency + max channels + AnalyserNode/DynamicsCompressor noise | AudioContext fingerprints bucket users very tightly | +| Speech synthesis | C++ patch: fabricated voices list | `navigator.speechSynthesis.getVoices()` matches the spoofed OS | +| WebRTC | C++ patch (nICEr): srflx address swap + synthetic srflx fallback + private-LAN host candidates | Real public IP never leaks via STUN | +| Timezone | C++ patch: per-Realm TZ via BrowsingContext (no IPC pref races) | `Date.getTimezoneOffset()`, `Intl.DateTimeFormat` match the spoofed location | +| DevTools detection | C++ patch: `Debugger.stealthMode` + Juggler `Runtime.js` + thread actor | FP Pro `developer_tools` = Not detected even with debugger attached | +| SOCKS5 auth | C++ patch | Stock Playwright+Firefox cannot negotiate it at all | +| DNS | Routed through SOCKS proxy by default | No DNS leak when using a residential gateway | +| Mouse motion | Bezier curves inside Juggler `PageHandler.js`, ~10 ms per waypoint | Even `page.click(selector)` moves like a human | +| GPU on virtual desktop | Pref-driven workaround for FF150 alt-desktop sandbox regression | WebGL renderer populated even in headless / multi-worker mass tests | +| Fission navigation | C++ patch: `nsDocShell` + `CanonicalBrowsingContext` Juggler navigation fix | `page.goto()` reliable on FF150 across proxy edge cases | +| about:newtab race | Async wrapper sleep around `new_page()` | No "Navigation interrupted by about:newtab" on FF150 | +| Proxy reliability | Juggler `PageHandler.equalsExceptRef` split try/catch | No spurious "Invalid url" with proxies like Evomi | + +Everything is driven by preferences - no hardcoded values in the binary. You change one pref, you change the spoofed value. + +--- + +## How it compares + +Commercial anti-detect browsers (Multilogin, GoLogin, AdsPower, Kameleo, Dolphin Anty, Browserbase) ship a patched Chromium and override fingerprints at the JavaScript layer. That's the ceiling - and it's a low one. + +| | stealthfox | Multilogin / GoLogin | AdsPower / Dolphin | Browserbase | +|---|---|---|---|---| +| Engine | Firefox (open source) | Chromium fork | Chromium fork | Chromium | +| Patch depth | C++ source | JS overrides | JS overrides | JS overrides | +| `.toString()` clean | ✅ Native Gecko path | ❌ Detectable shims | ❌ Detectable shims | ❌ Detectable shims | +| Canvas / WebGL | ✅ C++ level | ⚠️ JS override | ⚠️ JS override | ⚠️ JS override | +| SOCKS5 auth | ✅ Patched | ⚠️ Varies | ⚠️ Varies | ❌ | +| Self-hosted | ✅ | ❌ SaaS | ❌ SaaS | ❌ Cloud | +| reCAPTCHA v3 score | **0.90** | ~0.3-0.6 | ~0.3-0.5 | ~0.3-0.5 | +| FP Pro - bot detected | ✅ Not detected | ❌ Detected | ❌ Detected | ❌ Detected | +| FP Pro - tampering | ✅ Not detected | ❌ Detected | ❌ Detected | ❌ Detected | +| FP Pro - VPN flag | ✅ false | ❌ true | ❌ true | ❌ true | +| CreepJS lies | ✅ 0 | ❌ multiple | ❌ multiple | ❌ multiple | + +--- + +## Install + +```bash +pip install stealthfox +python -m stealthfox fetch # one-time ~100 MB download, SHA256-verified +``` + +Supported platforms: **Windows x86_64**, **Linux x86_64**. + +--- + +## Usage +### Random fingerprint per session +**100% Playwright-compatible** - sync and async, all methods, zero API changes. If you already use Playwright, switching is two lines: + +```diff +- from playwright.sync_api import sync_playwright +- with sync_playwright() as p: +- browser = p.firefox.launch() ++ from stealthfox import Stealthfox ++ with Stealthfox() as browser: +``` + +Every session gets a unique, coherent fingerprint drawn from real-world Firefox telemetry (GPU / audio / fonts / ~400 other fields) and Bezier-curve mouse motion baked into the browser itself. + +**Sync** +```python +from stealthfox import Stealthfox + +with Stealthfox(proxy={"server": "socks5://...", "username": "u", "password": "p"}) as browser: + page = browser.new_page() + page.goto("https://example.com") + page.click("#submit") # mouse arcs to the button on a Bezier curve +``` + +**Async** +```python +from stealthfox.async_api import Stealthfox + +async with Stealthfox(proxy={"server": "socks5://...", "username": "u", "password": "p"}) as browser: + page = await browser.new_page() + await page.goto("https://example.com") + await page.click("#submit") +``` + +The `browser` object is a `playwright.sync_api.Browser` / `playwright.async_api.Browser` - every Playwright method works as-is. + +--- + +### Random fingerprint per session + +```python +from stealthfox import Stealthfox + +with Stealthfox() as browser: + page = browser.new_page() + page.goto("https://creepjs-api.web.app") +``` + +Every call samples a new coherent profile. Log the seed to reproduce interesting runs: + +```python +sf = Stealthfox() +with sf as browser: + print("seed =", sf.seed) + # ... +``` + +### Reproducible fingerprint + +```python +with Stealthfox(seed=42) as browser: + ... # same GPU, same canvas hash, same audio context, every run +``` + +### Proxies + +```python +proxy = { + "server": "socks5://gate.example.com:1080", + "username": "user", + "password": "pass", +} +with Stealthfox(proxy=proxy) as browser: + ... +``` + +Schemes supported: `socks5`, `socks4`, `http`, `https`. Auth works on all of them (SOCKS5 via patched `nsProtocolProxyService.cpp`, HTTP/HTTPS via Playwright). DNS is routed through the proxy by default, no local leak. + +### Pinning specific fingerprint fields + +By default everything comes from `seed`. To force specific values while the rest stays seed-derived: + +```python +with Stealthfox( + seed=42, + pin={ + "gpu.renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 Direct3D11)", + "gpu.vendor": "Google Inc. (NVIDIA)", + "screen.width": 2560, + "screen.height": 1440, + "hardware.concurrency": 16, + }, +) as browser: + ... +``` + +Full list of pinnable keys, how pinning interacts with the Bayesian sampler, and common patterns are in **[docs/pinning.md](docs/pinning.md)**. + +--- + +## CLI + +```bash +stealthfox fetch # download the binary if missing +stealthfox path # print the absolute path to the cached binary +stealthfox version # wrapper and binary versions +stealthfox clear-cache # remove all cached binaries +``` + +## License + +MIT - see [LICENSE](LICENSE). The patched Firefox binary is distributed under the MPL-2.0 (Firefox upstream license). The C++ patches against mozilla-central that produce that binary are at [feder-cr/firefox-stealth](https://github.com/feder-cr/firefox-stealth). diff --git a/docs/pinning.md b/docs/pinning.md new file mode 100644 index 0000000..6d71727 --- /dev/null +++ b/docs/pinning.md @@ -0,0 +1,143 @@ +# Pinning fingerprint fields + +By default, every field of the fingerprint is sampled from a Bayesian network of real-world Firefox telemetry, seeded by an integer. Pass the same `seed` and you get the same fingerprint; omit it and each session is fresh. + +`pin` lets you **force specific fields** while letting the rest stay seed-derived. Useful when you need to replicate a known device (e.g. an NVIDIA 1080p laptop), test a specific GPU/screen combo, or pin just one noisy signal that a target site weighs heavily. + +```python +from stealthfox import Stealthfox + +with Stealthfox( + seed=42, + pin={ + "gpu.renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 Direct3D11)", + "gpu.vendor": "Google Inc. (NVIDIA)", + "screen.width": 2560, + "screen.height": 1440, + "hardware.concurrency": 16, + }, +) as browser: + ... +``` + +## How sampling + pinning interact + +The generator is a Bayesian network: every field has a probability distribution **conditioned on its parents**. For example `gpu_class_tier` conditions `screen.tier`, `hardware.concurrency` and `webgl.msaa_samples`. A high-end GPU will tend to pair with a 2560x1440+ screen and 16+ cores. + +When you pin a field: + +1. The pinned value is written directly, bypassing the sampler. +2. **Unpinned children are still sampled from their conditionals** — using the parent's original posterior, not the pinned value. + +That last point is the subtle one: pinning breaks the conditional chain. If you pin `gpu.renderer` to an RTX 4090 string but leave `screen` unpinned, the sampler will pick `screen` from the seed-derived tier (which might be `low_end`), producing a physically implausible "RTX 4090 + 1366x768" pairing. + +**Rule of thumb:** pin correlated fields together, or just trust the sampler. + +## Full list of pinnable keys + +Keys are dotted paths. All values are optional — omitted keys fall back to the sampler. + +### `gpu.*` + +| Key | Type | Example | Notes | +|-----|------|---------|-------| +| `gpu.class_tier` | str | `"high_end"` | The **root** of the Bayesian network. One of `"low_end"`, `"mid_range"`, `"high_end"`, `"integrated_old"`, `"integrated_modern"`. Pin this alone to steer the whole profile (screen, concurrency, MSAA, …) toward a coherent tier without having to name each sub-field. | +| `gpu.vendor` | str | `"Google Inc. (NVIDIA)"` | Must exactly match the renderer vendor prefix, otherwise detectors catch the mismatch. | +| `gpu.renderer` | str | `"ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 Direct3D11)"` | Windows ANGLE string. Used by WebGL `UNMASKED_RENDERER_WEBGL`. | + +**Why `class_tier` is pinnable separately from `renderer`.** They live at different levels of abstraction: + +- `class_tier` is a **coarse handle** over the whole Bayesian graph. It gates the distribution of `screen`, `hardware.concurrency`, `webgl.msaa_samples`, and storage quota. Pin `{"gpu.class_tier": "low_end"}` and the sampler returns a *coherent* low-end machine — small screen, 2-4 cores, 4x MSAA — without you having to specify each field. +- `renderer` is an **exact string** that lands verbatim in WebGL's `UNMASKED_RENDERER_WEBGL`. Useful when you want to imitate a specific GPU the target site has seen before. Does **not** condition other fields — if you pin `renderer` to an RTX 4090 but leave `class_tier` unpinned, `class_tier` is re-sampled from scratch and might disagree with the renderer string (see [How sampling + pinning interact](#how-sampling--pinning-interact)). + +In practice most users should pin `class_tier` alone, or pin `renderer`+`vendor`+`class_tier` together if they want full control. + +### `screen.*` + +| Key | Type | Example | +|-----|------|---------| +| `screen.width` | int | `2560` | +| `screen.height` | int | `1440` | +| `screen.avail_width` | int | `2560` | +| `screen.avail_height` | int | `1400` | +| `screen.dpr` | float | `1.0`, `1.25`, `1.5`, `2.0` | +| `screen.tier` | str | `"1080p"`, `"1440p"`, `"4k"`, ... | + +### `hardware.*` + +| Key | Type | Example | Notes | +|-----|------|---------|-------| +| `hardware.concurrency` | int | `16` | `navigator.hardwareConcurrency`. | +| `hardware.storage_quota_mb` | int | `10_000` | `navigator.storage.estimate().quota / 1024²`. | + +### `audio.*` + +| Key | Type | Example | Notes | +|-----|------|---------|-------| +| `audio.sample_rate` | int | `48000`, `44100` | `AudioContext.sampleRate`. | +| `audio.output_latency_ms` | float | `20.0` | `AudioContext.outputLatency * 1000`. | +| `audio.max_channel_count` | int | `2`, `6`, `8` | `AudioDestinationNode.maxChannelCount`. | + +### `codec.*` (booleans) + +| Key | Effect | +|-----|--------| +| `codec.av1_enabled` | `true` → `canPlayType('video/av01')` returns `"probably"`. | +| `codec.webm_encoder_enabled` | `MediaRecorder` advertises WebM support. | +| `codec.mediasource_webm` | `MediaSource.isTypeSupported('video/webm')`. | +| `codec.mediasource_mp4` | `MediaSource.isTypeSupported('video/mp4')`. | +| `codec.webspeech_synth` | `speechSynthesis.getVoices()` returns a fabricated voice list. | + +### `webgl.*` + +| Key | Type | Example | Notes | +|-----|------|---------|-------| +| `webgl.msaa_samples` | int | `4`, `8`, `16` | `MAX_SAMPLES` WebGL parameter. Conditioned on `gpu.class_tier` when sampled. | + +### Top-level + +| Key | Type | Example | Notes | +|-----|------|---------|-------| +| `fonts` | list[str] | `["Arial", "Segoe UI", ...]` | Complete font allowlist. **Every other font is hidden**. The sampler usually picks 14–24 system fonts. | +| `dark_theme` | bool | `False` | `prefers-color-scheme: dark`. Real traffic is ~85% light, 15% dark. | + +## Reading the chosen values back + +Every sampled (or pinned) value lands in a `zoom.stealth.*` pref inside the browser. Open `about:config` in a launched stealthfox session and filter for `zoom.stealth` to see the exact values in effect. + +Alternatively, inspect the instance before the `with` block exits: + +```python +sf = Stealthfox(seed=42) +with sf as browser: + # sf.seed is set; the full profile is in browser's prefs + ... +``` + +## Common patterns + +### Mimic a specific real device + +Pin the whole visible tuple — GPU, screen, concurrency, fonts, audio: + +```python +pin = { + "gpu.vendor": "Google Inc. (Intel)", + "gpu.renderer": "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11)", + "gpu.class_tier": "mid_range", + "screen.width": 1920, + "screen.height": 1080, + "screen.dpr": 1.0, + "hardware.concurrency": 8, + "audio.sample_rate": 48000, +} +``` + +### Test the low-end GPU path only + +```python +pin = {"gpu.class_tier": "low_end"} +# screen, msaa, concurrency re-sample from the seed but conditioned +# correctly on the low-end tier. +``` + diff --git a/docs/screenshots/creepjs.png b/docs/screenshots/creepjs.png new file mode 100644 index 0000000..ca1bbcb Binary files /dev/null and b/docs/screenshots/creepjs.png differ diff --git a/docs/screenshots/fingerprintpro.png b/docs/screenshots/fingerprintpro.png new file mode 100644 index 0000000..2d2ad79 Binary files /dev/null and b/docs/screenshots/fingerprintpro.png differ diff --git a/docs/screenshots/peet_ws.png b/docs/screenshots/peet_ws.png new file mode 100644 index 0000000..ef79267 Binary files /dev/null and b/docs/screenshots/peet_ws.png differ diff --git a/docs/screenshots/recaptcha_score.png b/docs/screenshots/recaptcha_score.png new file mode 100644 index 0000000..789e134 Binary files /dev/null and b/docs/screenshots/recaptcha_score.png differ diff --git a/docs/screenshots/sannysoft.png b/docs/screenshots/sannysoft.png new file mode 100644 index 0000000..682bcc8 Binary files /dev/null and b/docs/screenshots/sannysoft.png differ diff --git a/docs/screenshots/webrtc.png b/docs/screenshots/webrtc.png new file mode 100644 index 0000000..9fafa36 Binary files /dev/null and b/docs/screenshots/webrtc.png differ diff --git a/examples/basic.py b/examples/basic.py new file mode 100644 index 0000000..3af5c25 --- /dev/null +++ b/examples/basic.py @@ -0,0 +1,13 @@ +"""Launch a patched Firefox with a random stealth profile and load example.com.""" +from stealthfox import Stealthfox + + +def main() -> None: + with Stealthfox() as browser: + page = browser.new_page() + page.goto("https://example.com") + print("title:", page.title()) + + +if __name__ == "__main__": + main() diff --git a/examples/with_proxy.py b/examples/with_proxy.py new file mode 100644 index 0000000..9344c29 --- /dev/null +++ b/examples/with_proxy.py @@ -0,0 +1,24 @@ +"""Same as basic.py but route through a SOCKS5 proxy.""" +import os + +from stealthfox import Stealthfox + + +def main() -> None: + proxy = { + "server": os.environ.get("STEALTHFOX_PROXY_SERVER", "socks5://127.0.0.1:1080"), + } + user = os.environ.get("STEALTHFOX_PROXY_USER") + password = os.environ.get("STEALTHFOX_PROXY_PASS") + if user and password: + proxy["username"] = user + proxy["password"] = password + + with Stealthfox(proxy=proxy) as browser: + page = browser.new_page() + page.goto("https://httpbin.org/ip") + print(page.content()[:500]) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..89b70e1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,45 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "stealthfox" +version = "0.1.0" +description = "Playwright wrapper for a patched Firefox with deterministic stealth profile." +readme = "README.md" +requires-python = ">=3.11" +license = "MIT" +authors = [{ name = "feder-cr", email = "85809106+feder-cr@users.noreply.github.com" }] +keywords = ["playwright", "firefox", "stealth", "anti-detect", "automation"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "playwright>=1.40", + "platformdirs>=4", + "requests>=2.31", + "tqdm>=4.66", + "pywin32>=306; sys_platform == 'win32'", +] + +[project.optional-dependencies] +dev = ["pytest>=7", "pytest-mock>=3", "responses>=0.24"] + +[project.scripts] +stealthfox = "stealthfox.cli:main" + +[project.urls] +Homepage = "https://github.com/feder-cr/stealthfox" +Issues = "https://github.com/feder-cr/stealthfox/issues" + +[tool.hatch.build.targets.wheel] +packages = ["src/stealthfox"] + +[tool.hatch.build.targets.wheel.force-include] +"src/stealthfox/data" = "stealthfox/data" +"src/stealthfox/_fpforge/data" = "stealthfox/_fpforge/data" diff --git a/scripts/audit_cpt_realism.py b/scripts/audit_cpt_realism.py new file mode 100644 index 0000000..346ccc6 --- /dev/null +++ b/scripts/audit_cpt_realism.py @@ -0,0 +1,271 @@ +"""Bayesian network realism audit (one-shot regenerator). + +Each (gpu_class, intra_tier) cell maps to a real-world persona. +Distributions derived from: +- Steam HW Survey March 2026 Windows + cores: 2c=3%, 4c=13%, 6c=29%, 8c=27%, 10c=7%, 12c=5%, 14c=5%, 16c=6%, 24c=3%, 32c=0.01% +- Intel ARK + AMD spec sheets (CPU release era -> cores) +- Tom's Hardware: most users 100-250GB free (= 512GB-1TB drives baseline) +""" +import json +import os +import sys + +OUT = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "src", "stealthfox", "_fpforge", "data", +) + + +def _norm(d): + """Normalize prob dict / list of (val, prob) pairs to sum 1.0.""" + if isinstance(d, dict): + total = sum(d.values()) + return [{"value": v, "prob": round(p / total, 4)} for v, p in d.items()] + total = sum(p for _, p in d) + return [{"value": v, "prob": round(p / total, 4)} for v, p in d] + + +def _scr(w, h, dpr=1.0): + return {"w": w, "h": h, "aw": w, "ah": h - 40, "dpr": dpr} + + +# ============================================================ +# HW concurrency per (gpu_class, intra_tier) +# ============================================================ +HWC = { + ("integrated_old", "budget"): {2: 0.65, 4: 0.30, 8: 0.05}, + ("integrated_old", "standard"): {2: 0.30, 4: 0.55, 8: 0.15}, + ("integrated_old", "premium"): {4: 0.65, 8: 0.30, 12: 0.05}, + + ("integrated_modern", "budget"): {4: 0.55, 6: 0.20, 8: 0.20, 12: 0.05}, + ("integrated_modern", "standard"): {6: 0.20, 8: 0.30, 10: 0.20, 12: 0.20, 16: 0.10}, + ("integrated_modern", "premium"): {8: 0.20, 10: 0.20, 12: 0.30, 14: 0.15, 16: 0.15}, + + ("low_end", "budget"): {4: 0.50, 6: 0.25, 8: 0.20, 12: 0.05}, + ("low_end", "standard"): {4: 0.10, 6: 0.35, 8: 0.30, 12: 0.18, 16: 0.07}, + ("low_end", "premium"): {6: 0.10, 8: 0.30, 12: 0.30, 16: 0.22, 24: 0.08}, + + ("mid_range", "budget"): {6: 0.55, 8: 0.30, 12: 0.10, 16: 0.05}, + ("mid_range", "standard"): {6: 0.40, 8: 0.30, 12: 0.18, 16: 0.10, 24: 0.02}, + ("mid_range", "premium"): {6: 0.15, 8: 0.45, 12: 0.20, 16: 0.15, 24: 0.05}, + + ("high_end", "budget"): {6: 0.10, 8: 0.55, 12: 0.20, 16: 0.15}, + ("high_end", "standard"): {8: 0.30, 12: 0.18, 14: 0.15, 16: 0.27, 24: 0.10}, + ("high_end", "premium"): {12: 0.10, 14: 0.15, 16: 0.30, 24: 0.35, 32: 0.10}, + + ("workstation", "budget"): {8: 0.40, 12: 0.30, 16: 0.20, 24: 0.10}, + ("workstation", "standard"): {12: 0.10, 16: 0.40, 24: 0.30, 32: 0.20}, + ("workstation", "premium"): {24: 0.30, 32: 0.45, 48: 0.15, 64: 0.10}, +} + + +# ============================================================ +# Screen resolution per (gpu_class, intra_tier) +# Each value list is [(screen_obj, prob), ...]. +# ============================================================ +SCREEN = { + ("integrated_old", "budget"): [(_scr(1366, 768), 0.78), (_scr(1280, 800), 0.15), (_scr(1024, 768), 0.07)], + ("integrated_old", "standard"): [(_scr(1366, 768), 0.55), (_scr(1600, 900), 0.25), (_scr(1280, 800), 0.10), (_scr(1920, 1080), 0.10)], + ("integrated_old", "premium"): [(_scr(1600, 900), 0.45), (_scr(1920, 1080), 0.40), (_scr(1366, 768), 0.15)], + + ("integrated_modern", "budget"): [(_scr(1366, 768), 0.30), (_scr(1920, 1080), 0.65), (_scr(1600, 900), 0.05)], + ("integrated_modern", "standard"):[(_scr(1920, 1080), 0.70), (_scr(1920, 1200), 0.10), (_scr(2560, 1440), 0.12), (_scr(2560, 1600), 0.08)], + ("integrated_modern", "premium"): [(_scr(1920, 1080), 0.30), (_scr(1920, 1200), 0.10), (_scr(2560, 1440), 0.25), (_scr(2560, 1600), 0.15), (_scr(3840, 2160), 0.20)], + + ("low_end", "budget"): [(_scr(1920, 1080), 0.85), (_scr(1920, 1200), 0.10), (_scr(2560, 1440), 0.05)], + ("low_end", "standard"): [(_scr(1920, 1080), 0.60), (_scr(1920, 1200), 0.10), (_scr(2560, 1440), 0.25), (_scr(3840, 2160), 0.05)], + ("low_end", "premium"): [(_scr(1920, 1080), 0.25), (_scr(2560, 1440), 0.45), (_scr(3840, 2160), 0.20), (_scr(3440, 1440), 0.05), (_scr(1920, 1200), 0.05)], + + ("mid_range", "budget"): [(_scr(1920, 1080), 0.75), (_scr(2560, 1440), 0.20), (_scr(1920, 1200), 0.05)], + ("mid_range", "standard"): [(_scr(1920, 1080), 0.45), (_scr(2560, 1440), 0.40), (_scr(3840, 2160), 0.10), (_scr(3440, 1440), 0.05)], + ("mid_range", "premium"): [(_scr(1920, 1080), 0.15), (_scr(2560, 1440), 0.50), (_scr(3840, 2160), 0.20), (_scr(3440, 1440), 0.10), (_scr(2560, 1080), 0.05)], + + ("high_end", "budget"): [(_scr(1920, 1080), 0.20), (_scr(2560, 1440), 0.55), (_scr(3840, 2160), 0.20), (_scr(3440, 1440), 0.05)], + ("high_end", "standard"): [(_scr(2560, 1440), 0.40), (_scr(3840, 2160), 0.40), (_scr(3440, 1440), 0.15), (_scr(5120, 1440), 0.05)], + ("high_end", "premium"): [(_scr(3840, 2160), 0.55), (_scr(2560, 1440), 0.15), (_scr(3440, 1440), 0.10), (_scr(5120, 1440), 0.15), (_scr(7680, 2160), 0.05)], + + ("workstation", "budget"): [(_scr(2560, 1440), 0.55), (_scr(3840, 2160), 0.30), (_scr(1920, 1200), 0.10), (_scr(2560, 1600), 0.05)], + ("workstation", "standard"): [(_scr(2560, 1440), 0.30), (_scr(3840, 2160), 0.45), (_scr(2560, 1600), 0.15), (_scr(3440, 1440), 0.10)], + ("workstation", "premium"): [(_scr(3840, 2160), 0.55), (_scr(2560, 1600), 0.15), (_scr(5120, 2160), 0.15), (_scr(3840, 2400), 0.10), (_scr(7680, 2160), 0.05)], +} + + +# ============================================================ +# Storage quota MB per (gpu_class, intra_tier) +# ============================================================ +STORAGE = { + ("integrated_old", "budget"): {32_000: 0.30, 64_000: 0.40, 128_000: 0.25, 256_000: 0.05}, + ("integrated_old", "standard"): {64_000: 0.20, 128_000: 0.45, 256_000: 0.25, 500_000: 0.10}, + ("integrated_old", "premium"): {128_000: 0.20, 256_000: 0.45, 500_000: 0.30, 1_000_000: 0.05}, + + ("integrated_modern", "budget"): {64_000: 0.20, 128_000: 0.30, 256_000: 0.30, 500_000: 0.20}, + ("integrated_modern", "standard"):{256_000: 0.25, 500_000: 0.45, 1_000_000: 0.25, 2_000_000: 0.05}, + ("integrated_modern", "premium"): {500_000: 0.25, 1_000_000: 0.50, 2_000_000: 0.20, 4_000_000: 0.05}, + + ("low_end", "budget"): {128_000: 0.20, 256_000: 0.50, 500_000: 0.25, 1_000_000: 0.05}, + ("low_end", "standard"): {256_000: 0.20, 500_000: 0.50, 1_000_000: 0.25, 2_000_000: 0.05}, + ("low_end", "premium"): {500_000: 0.20, 1_000_000: 0.50, 2_000_000: 0.25, 4_000_000: 0.05}, + + ("mid_range", "budget"): {256_000: 0.15, 500_000: 0.50, 1_000_000: 0.30, 2_000_000: 0.05}, + ("mid_range", "standard"):{500_000: 0.20, 1_000_000: 0.55, 2_000_000: 0.20, 4_000_000: 0.05}, + ("mid_range", "premium"): {1_000_000: 0.40, 2_000_000: 0.45, 4_000_000: 0.15}, + + ("high_end", "budget"): {500_000: 0.15, 1_000_000: 0.50, 2_000_000: 0.30, 4_000_000: 0.05}, + ("high_end", "standard"):{1_000_000: 0.30, 2_000_000: 0.50, 4_000_000: 0.20}, + ("high_end", "premium"): {2_000_000: 0.40, 4_000_000: 0.45, 8_000_000: 0.15}, + + ("workstation", "budget"): {1_000_000: 0.30, 2_000_000: 0.50, 4_000_000: 0.20}, + ("workstation", "standard"):{2_000_000: 0.30, 4_000_000: 0.50, 8_000_000: 0.20}, + ("workstation", "premium"): {4_000_000: 0.35, 8_000_000: 0.45, 16_000_000: 0.20}, +} + + +# ============================================================ +# Audio profile per gpu_class +# ============================================================ +AUDIO = { + "integrated_old": [ + ({"rate": 44100, "latency": 50, "channels": 2}, 0.70), + ({"rate": 48000, "latency": 50, "channels": 2}, 0.30), + ], + "integrated_modern": [ + ({"rate": 48000, "latency": 30, "channels": 2}, 0.60), + ({"rate": 44100, "latency": 40, "channels": 2}, 0.25), + ({"rate": 48000, "latency": 25, "channels": 6}, 0.15), + ], + "low_end": [ + ({"rate": 48000, "latency": 40, "channels": 2}, 0.55), + ({"rate": 44100, "latency": 50, "channels": 2}, 0.30), + ({"rate": 48000, "latency": 30, "channels": 6}, 0.15), + ], + "mid_range": [ + ({"rate": 48000, "latency": 25, "channels": 2}, 0.45), + ({"rate": 48000, "latency": 20, "channels": 6}, 0.30), + ({"rate": 48000, "latency": 20, "channels": 8}, 0.15), + ({"rate": 44100, "latency": 30, "channels": 2}, 0.10), + ], + "high_end": [ + ({"rate": 48000, "latency": 15, "channels": 6}, 0.30), + ({"rate": 48000, "latency": 15, "channels": 8}, 0.30), + ({"rate": 48000, "latency": 15, "channels": 2}, 0.20), + ({"rate": 96000, "latency": 15, "channels": 6}, 0.10), + ({"rate": 96000, "latency": 15, "channels": 8}, 0.10), + ], + "workstation": [ + ({"rate": 48000, "latency": 10, "channels": 8}, 0.25), + ({"rate": 96000, "latency": 10, "channels": 8}, 0.30), + ({"rate": 96000, "latency": 10, "channels": 6}, 0.20), + ({"rate": 192000, "latency": 10, "channels": 8}, 0.15), + ({"rate": 48000, "latency": 15, "channels": 6}, 0.10), + ], +} + + +# ============================================================ +# Codec per gpu_class +# ============================================================ +CODEC = { + "integrated_old": [ + ({"av1_enabled": False, "webm_encoder_enabled": False, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": True}, 1.0), + ], + "integrated_modern": [ + ({"av1_enabled": True, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": True}, 0.55), + ({"av1_enabled": False, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": True}, 0.35), + ({"av1_enabled": True, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": False}, 0.10), + ], + "low_end": [ + ({"av1_enabled": False, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": True}, 0.85), + ({"av1_enabled": False, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": False}, 0.15), + ], + "mid_range": [ + ({"av1_enabled": True, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": True}, 0.55), + ({"av1_enabled": False, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": True}, 0.35), + ({"av1_enabled": True, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": False}, 0.10), + ], + "high_end": [ + ({"av1_enabled": True, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": True}, 0.85), + ({"av1_enabled": True, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": False}, 0.15), + ], + "workstation": [ + ({"av1_enabled": True, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": True}, 0.70), + ({"av1_enabled": True, "webm_encoder_enabled": True, "mediasource_webm": True, "mediasource_mp4": True, "webspeech_synth": False}, 0.30), + ], +} + + +# ============================================================ +# MSAA per (gpu_class, screen_tier) +# screen_tier classifier in _sampler.py: "1080p", "1440p", "2160p", "ultrawide" +# Note: prefs.py clamps msaa to >=2 (antialias=True always), so msaa=0 is no longer +# emitted at the browser layer. CPT here keeps the 2/4/8 distribution matching what +# the prefs layer actually exposes. +# ============================================================ +MSAA = { + ("integrated_old", "1080p"): [(2, 0.75), (4, 0.20), (8, 0.05)], + ("integrated_old", "1440p"): [(2, 0.85), (4, 0.15)], + ("integrated_old", "2160p"): [(2, 1.0)], + ("integrated_old", "ultrawide"): [(2, 1.0)], + + ("integrated_modern", "1080p"): [(2, 0.50), (4, 0.40), (8, 0.10)], + ("integrated_modern", "1440p"): [(2, 0.60), (4, 0.35), (8, 0.05)], + ("integrated_modern", "2160p"): [(2, 0.85), (4, 0.15)], + ("integrated_modern", "ultrawide"):[(2, 0.80), (4, 0.20)], + + ("low_end", "1080p"): [(2, 0.40), (4, 0.45), (8, 0.15)], + ("low_end", "1440p"): [(2, 0.55), (4, 0.40), (8, 0.05)], + ("low_end", "2160p"): [(2, 0.85), (4, 0.15)], + ("low_end", "ultrawide"): [(2, 0.70), (4, 0.30)], + + ("mid_range", "1080p"): [(2, 0.30), (4, 0.50), (8, 0.20)], + ("mid_range", "1440p"): [(2, 0.40), (4, 0.45), (8, 0.15)], + ("mid_range", "2160p"): [(2, 0.65), (4, 0.30), (8, 0.05)], + ("mid_range", "ultrawide"): [(2, 0.55), (4, 0.40), (8, 0.05)], + + ("high_end", "1080p"): [(2, 0.20), (4, 0.45), (8, 0.35)], + ("high_end", "1440p"): [(2, 0.25), (4, 0.50), (8, 0.25)], + ("high_end", "2160p"): [(2, 0.40), (4, 0.45), (8, 0.15)], + ("high_end", "ultrawide"): [(2, 0.30), (4, 0.50), (8, 0.20)], + + ("workstation", "1080p"): [(2, 0.15), (4, 0.50), (8, 0.35)], + ("workstation", "1440p"): [(2, 0.15), (4, 0.55), (8, 0.30)], + ("workstation", "2160p"): [(2, 0.25), (4, 0.55), (8, 0.20)], + ("workstation", "ultrawide"): [(2, 0.20), (4, 0.55), (8, 0.25)], +} + + +def write_pair_table(table, fname, meta): + out = {"_meta": meta, "table": {}} + for key_pair, dist in table.items(): + key = json.dumps(list(key_pair)) + out["table"][key] = _norm(dist) + with open(os.path.join(OUT, fname), "w", encoding="utf-8") as f: + json.dump(out, f, indent=2, ensure_ascii=False) + + +def write_class_table(table, fname, meta): + out = {"_meta": meta, "table": {}} + for cls, dist in table.items(): + out["table"][cls] = _norm(dist) + with open(os.path.join(OUT, fname), "w", encoding="utf-8") as f: + json.dump(out, f, indent=2, ensure_ascii=False) + + +def main(): + write_pair_table(HWC, "cpt_hwc_given_class_tier.json", + "hardware_concurrency given (gpu_class, intra_tier)") + write_pair_table(SCREEN, "cpt_screen_given_class_tier.json", + "screen given (gpu_class, intra_tier)") + write_pair_table(STORAGE, "cpt_storage_given_class_tier.json", + "storage_quota_mb given (gpu_class, intra_tier)") + write_pair_table(MSAA, "cpt_msaa_given_class_screen.json", + "msaa_samples given (gpu_class, screen_tier)") + write_class_table(AUDIO, "cpt_audio_given_class.json", + "audio (rate/latency/channels) given gpu_class") + write_class_table(CODEC, "cpt_codec_given_class.json", + "codec given gpu_class") + print("All 6 CPT files updated.") + + +if __name__ == "__main__": + main() diff --git a/src/stealthfox/__init__.py b/src/stealthfox/__init__.py new file mode 100644 index 0000000..f665239 --- /dev/null +++ b/src/stealthfox/__init__.py @@ -0,0 +1,22 @@ +"""stealthfox — Playwright wrapper for a patched Firefox with stealth profile. + +Quickstart: + + from stealthfox import Stealthfox + + with Stealthfox() as browser: # random seed + page = browser.new_page() + page.goto("https://example.com") + + with Stealthfox(seed=42) as browser: # deterministic + ... + + with Stealthfox(humanize=True) as browser: # human-like cursor motion + page = browser.new_page() + page.click("#submit") # expanded into a Bezier trajectory +""" +from .launcher import Stealthfox +from .constants import BINARY_VERSION, FIREFOX_UPSTREAM_VERSION + +__version__ = "0.1.0" +__all__ = ["Stealthfox", "BINARY_VERSION", "FIREFOX_UPSTREAM_VERSION", "__version__"] diff --git a/src/stealthfox/__main__.py b/src/stealthfox/__main__.py new file mode 100644 index 0000000..0149c03 --- /dev/null +++ b/src/stealthfox/__main__.py @@ -0,0 +1,4 @@ +from .cli import main +import sys + +sys.exit(main()) diff --git a/src/stealthfox/_fpforge/__init__.py b/src/stealthfox/_fpforge/__init__.py new file mode 100644 index 0000000..134a2be --- /dev/null +++ b/src/stealthfox/_fpforge/__init__.py @@ -0,0 +1,26 @@ +"""Internal Bayesian fingerprint generator used by stealthfox. + +Private module — do not import from user code. Use +stealthfox.Stealthfox(seed=..., pin=...) instead. +""" +from .profile import ( + AudioProfile, + CodecProfile, + GPUProfile, + HardwareProfile, + Profile, + ScreenProfile, + WebGLProfile, + generate_profile, +) + +__all__ = [ + "generate_profile", + "Profile", + "GPUProfile", + "ScreenProfile", + "HardwareProfile", + "AudioProfile", + "CodecProfile", + "WebGLProfile", +] diff --git a/src/stealthfox/_fpforge/_network.py b/src/stealthfox/_fpforge/_network.py new file mode 100644 index 0000000..78e503d --- /dev/null +++ b/src/stealthfox/_fpforge/_network.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +"""Generic Bayesian network for fingerprint sampling. + +A Node has: + - name + - parents (list of parent node names) + - CPT: either + * marginal (no parents): flat [{value, prob}, ...] + * conditional: {parent_tuple: [{value, prob}, ...]} + - OR deterministic: a classifier function `(context) -> value` (no CPT) + +Sampling: + - Nodes are topologically sorted + - For each node, look up the conditional distribution given parent values + already sampled in `context`, then weighted-pick + - Deterministic nodes apply their classifier directly + +Values can be ANY JSON-serializable type (int, str, dict, list, bool). +Complex values (e.g. screen joint {w, h, dpr}) are stored as dicts in the +CPT and returned as-is in the context. +""" +import json +import random +from typing import Any, Callable, Dict, List, Optional, Tuple + + +class Node: + """Single Bayesian node.""" + + __slots__ = ("name", "parents", "cpt", "classifier", "_marginal") + + def __init__( + self, + name: str, + parents: Optional[List[str]] = None, + cpt: Optional[Any] = None, + classifier: Optional[Callable[[Dict[str, Any]], Any]] = None, + ): + self.name = name + self.parents = list(parents or []) + self.cpt = cpt + self.classifier = classifier + # Precompute: for no-parent nodes, cpt is the marginal list + self._marginal = cpt if not self.parents and classifier is None else None + + def sample(self, context: Dict[str, Any], rng: random.Random) -> Any: + # Deterministic nodes don't sample + if self.classifier is not None: + return self.classifier(context) + + if not self.parents: + # Marginal root + return _weighted_pick(self._marginal, rng) + + # Conditional node: build the key from parent values + key = _parent_key(self.parents, context) + if key not in self.cpt: + # Fallback: concatenate all parents' tables (uniform over union) + # Keeps sampler from crashing if data doesn't cover some combo. + pool = [] + for v in self.cpt.values(): + pool.extend(v) + if not pool: + raise ValueError( + f"Node {self.name!r}: no CPT entries for {self.parents}={key}" + ) + return _weighted_pick(pool, rng) + return _weighted_pick(self.cpt[key], rng) + + +class Network: + """Collection of nodes with topological sampling.""" + + def __init__(self, nodes: List[Node]): + self.nodes = _topsort(nodes) + self.by_name = {n.name: n for n in self.nodes} + + def sample(self, rng: random.Random) -> Dict[str, Any]: + context: Dict[str, Any] = {} + for node in self.nodes: + context[node.name] = node.sample(context, rng) + return context + + +# ── Helpers ───────────────────────────────────────────────────────────── + +def _weighted_pick(table: List[Dict[str, Any]], rng: random.Random) -> Any: + """`table` is a list of {value, prob} dicts. Returns one value.""" + values = [e["value"] for e in table] + probs = [float(e["prob"]) for e in table] + if not values: + raise ValueError("Empty CPT entry") + total = sum(probs) + if total <= 0: + return rng.choice(values) + # Normalize to be safe (CPTs can be unnormalized) + probs = [p / total for p in probs] + return rng.choices(values, weights=probs, k=1)[0] + + +def _parent_key(parents: List[str], context: Dict[str, Any]) -> str: + """Build a JSON-stable key from parent values in declared order.""" + if len(parents) == 1: + v = context[parents[0]] + return v if isinstance(v, str) else json.dumps(v, sort_keys=True) + return json.dumps([context[p] for p in parents], sort_keys=True) + + +def _topsort(nodes: List[Node]) -> List[Node]: + """Topological sort by parent-before-child.""" + by_name = {n.name: n for n in nodes} + visited: set = set() + order: List[Node] = [] + + def visit(n: Node, path: set): + if n.name in visited: + return + if n.name in path: + raise ValueError(f"Cycle at {n.name}") + path.add(n.name) + for p in n.parents: + if p not in by_name: + raise ValueError(f"Node {n.name} has unknown parent {p}") + visit(by_name[p], path) + path.discard(n.name) + visited.add(n.name) + order.append(n) + + for n in nodes: + visit(n, set()) + return order diff --git a/src/stealthfox/_fpforge/_sampler.py b/src/stealthfox/_fpforge/_sampler.py new file mode 100644 index 0000000..5653db8 --- /dev/null +++ b/src/stealthfox/_fpforge/_sampler.py @@ -0,0 +1,358 @@ +# -*- coding: utf-8 -*- +"""stealth_forge — Bayesian fingerprint generator for Firefox 150 Windows. + +Everything the Firefox build exposes to JS (screen, hardwareConcurrency, +WebGL, audio, MSAA, theme, media codecs) is sampled from a Bayesian network +with coherent cross-field dependencies. Identity (userAgent, platform, +oscpu, webdriver=false, maxTouchPoints=0) is locked by the compiled build. + +Graph: + + gpu (root, 444 real Windows ANGLE renderers) + │ + └─> gpu_class (deterministic classifier, 6 classes) + ├─> hw_concurrency (CPT per class) + ├─> screen (w/h/dpr/av) (CPT per class) + └─> msaa_samples (CPT per class) + + audio (root, joint rate+latency+channels — marginal) + dark_theme (marginal) + av1_enabled (marginal) + webm_encoder_enabled (marginal) + + font_exclude ← deterministic hash of stealth_seed (seed-derived) + +CPTs live in `data/*.json` (easy to tune without code changes). +Sampling is deterministic per stealth_seed via a private random.Random. +""" +import json +import os +import re +from typing import Any, Dict + +from ._network import Network, Node + +_HERE = os.path.dirname(os.path.abspath(__file__)) + + +def _load(filename: str) -> Any: + with open(os.path.join(_HERE, "data", filename), "r", encoding="utf-8") as f: + return json.load(f) + + +# ═══════════════════════════════════════════════════════════════════════ +# LOCKED IDENTITY (compiled into our Firefox 150 build — never varies) +# ═══════════════════════════════════════════════════════════════════════ +_LOCKED: Dict[str, Any] = { + "user_agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) " + "Gecko/20100101 Firefox/150.0.1" + ), + "platform": "Win32", + "oscpu": "Windows NT 10.0; Win64; x64", + "app_code_name": "Mozilla", + "app_version": "5.0 (Windows)", + "product_sub": "20100101", + "webdriver": False, + "max_touch_points": 0, +} + + +# ═══════════════════════════════════════════════════════════════════════ +# DATA +# ═══════════════════════════════════════════════════════════════════════ +_GPU_POOL = _load("webgl_renderer_pool.json")["entries"] +# hwc/screen/storage now keyed on (gpu_class, intra_tier) for triangulation +_CPT_HWC = _load("cpt_hwc_given_class_tier.json")["table"] +_CPT_SCREEN = _load("cpt_screen_given_class_tier.json")["table"] +_CPT_STORAGE = _load("cpt_storage_given_class_tier.json")["table"] +# Hidden tier variable that makes hwc/screen/storage jointly coherent +_CPT_INTRA_TIER = _load("cpt_intra_tier_given_class.json")["table"] +# MSAA depends on (gpu_class, screen_tier) — 4K gaming → MSAA=0, 1080p+GPU → MSAA=4 +_CPT_MSAA = _load("cpt_msaa_given_class_screen.json")["table"] +# Codec unchanged +_CPT_CODEC = _load("cpt_codec_given_class.json")["table"] +# Audio now conditional on gpu_class (workstation → pro audio, old → 44.1kHz onboard) +_CPT_AUDIO = _load("cpt_audio_given_class.json")["table"] +_INDEP = _load("priors_independent.json") +_FONT_POOL = _load("font_pool.json") +# Each entry is a dict {"name": "", "factor": float}. +# - name: the font family advertised to the page. +# - factor: per-family width scale used by the consumer to make the family +# detectable by width-diff probes. +# Core = always-included; Optional = sampled with P(font | gpu_class). +_FONT_CORE: list = _FONT_POOL["core"] +_FONT_OPTIONAL: list = _FONT_POOL["optional"] +_CPT_FONTS_OPT = _load("cpt_fonts_optional_given_class.json")["table"] + + +# ═══════════════════════════════════════════════════════════════════════ +# GPU CLASSIFIER (deterministic function of gpu → gpu_class) +# ═══════════════════════════════════════════════════════════════════════ +_GPU_CLASSES = ( + "integrated_old", "integrated_modern", "low_end", + "mid_range", "high_end", "workstation", +) + + +def classify_gpu(gpu_value: Dict[str, str]) -> str: + """Deterministic: maps (renderer, vendor) dict to one of 6 classes. + + See data/cpt_*.json — each CPT table has an entry for every class. + """ + r = gpu_value.get("renderer", "") + + if re.search(r"Intel.*HD Graphics (3000|4000|2500)", r): + return "integrated_old" + if re.search( + r"Intel.*(HD Graphics (4[56]|5\d\d|6\d\d)|UHD Graphics|Graphics Family|Iris|Arc)", + r, + ): + return "integrated_modern" + if re.search( + r"AMD.*(Radeon(\(TM\))? (Graphics|6\d\dM|7\d\dM|8\d\dM)|Vega [0-9]|" + r"Renoir|Rembrandt|TM Graphics)", + r, re.IGNORECASE, + ): + return "integrated_modern" + + # NVIDIA: Firefox SanitizeRenderer.cpp collapses every GeForce into one of + # 3 vintage buckets (8800 GTX / GTX 480 / GTX 980). The renderer string + # exposed to JS is therefore vintage; pairing it with modern cores/screen + # creates an internal mismatch that FP Pro's tampering_ml flags. We pick + # `low_end` for all 3 buckets so cores stay 4-12 and screen 1080-1440p, + # consistent with what a real user with each of those (vintage) cards + # would have. Workstation overrides keep their high-tier classification. + if re.search( + r"(GeForce (8\d\d\d?|9\d\d\d?|GTX 980|GTX 480|GT 1030|GT 710|GT 730|" + r"GT 220|GT 240|210|310)|Quadro K\d|Radeon HD [1234]\d\d\d)", r, + ): + return "low_end" + + # NVIDIA discrete (any other GeForce — should be rare after the pool was + # collapsed to the 3 sanitize buckets, but kept as a safety net). + m = re.search(r"GeForce\s+(?:GTX\s+|RTX\s+)?(\d{3,4})", r) + if m: + if "Quadro" in r or "Workstation" in r: + return "workstation" + # Anything that survives the sanitize collapse stays low_end to avoid + # the modern-cores/vintage-renderer pairing. + return "low_end" + + # AMD discrete + m = re.search(r"Radeon[^0-9]*(\d{3,4})", r) + if m: + n = int(m.group(1)) + if "FirePro" in r or "Radeon Pro" in r: + return "workstation" + if n >= 5700: + return "high_end" + if 5500 <= n <= 5600 or 580 <= n <= 590: + return "mid_range" + return "low_end" + + # Fallback + return "mid_range" + + +# ═══════════════════════════════════════════════════════════════════════ +# NETWORK CONSTRUCTION +# ═══════════════════════════════════════════════════════════════════════ +# Build once at import — the network is stateless, only the RNG varies. + +def _gpu_marginal(): + """Build marginal distribution over GPU pool (uniform for now).""" + n = len(_GPU_POOL) + p = 1.0 / n + return [{"value": g, "prob": p} for g in _GPU_POOL] + + +def _cpt_from_table(table: Dict[str, Any]) -> Dict[str, list]: + """CPT for conditional nodes: `{class_name: [{value, prob}, ...]}`.""" + return dict(table) + + +def _screen_tier(ctx): + """Classify screen width into tier for (gpu_class, screen_tier) CPTs.""" + s = ctx.get("screen", {}) or {} + w = int(s.get("w", 1920)) + h = int(s.get("h", 1080)) + # Ultrawide: aspect ratio > 2.1 (e.g. 3440x1440, 5120x1440) + if h > 0 and (w / h) > 2.1: + return "ultrawide" + if w <= 1920: + return "1080p" + if w <= 2560: + return "1440p" + if w <= 3840: + return "2160p" + return "ultrawide" + + +_NETWORK = Network([ + Node("gpu", parents=[], cpt=_gpu_marginal()), + Node("gpu_class", parents=["gpu"], classifier=lambda ctx: classify_gpu(ctx["gpu"])), + # Hidden variable: within a gpu_class, user's OTHER components (RAM, SSD, + # cores, screen) correlate — a 'premium' mid_range user has more cores, + # larger SSD, higher-res screen than a 'budget' mid_range user. Without + # this, hwc/screen/storage would be independent given gpu_class (noisy). + Node("intra_tier", parents=["gpu_class"], cpt=_cpt_from_table(_CPT_INTRA_TIER)), + # hwc/screen/storage now jointly coherent via (gpu_class, intra_tier). + Node("hw_concurrency", parents=["gpu_class", "intra_tier"], + cpt=_cpt_from_table(_CPT_HWC)), + Node("screen", parents=["gpu_class", "intra_tier"], + cpt=_cpt_from_table(_CPT_SCREEN)), + # Derive screen_tier from screen for msaa parent lookup. + Node("screen_tier", parents=["screen"], classifier=_screen_tier), + # MSAA: realistic combo (4K + high_end GPU → MSAA=0 due to perf cost; + # 1080p + high_end → MSAA=4 common; 1080p + integrated → MSAA=0). + Node("msaa_samples", parents=["gpu_class", "screen_tier"], + cpt=_cpt_from_table(_CPT_MSAA)), + # Joint codec distribution (gpu_class only). + Node("codec", parents=["gpu_class"], cpt=_cpt_from_table(_CPT_CODEC)), + # Storage quota: coherent within gpu_class × intra_tier (premium workstation + # user → 2-3TB SSD; budget workstation user → 512GB; budget integrated_old + # → 128GB). + Node("storage_quota_mb", parents=["gpu_class", "intra_tier"], + cpt=_cpt_from_table(_CPT_STORAGE)), + # Audio: pro users (workstation) → 48/96kHz 6-8ch; old onboard → 44.1kHz + # 2ch high latency. Workstation GPU + 44.1kHz mono was previously + # implausible; now blocked by the CPT. + Node("audio", parents=["gpu_class"], cpt=_cpt_from_table(_CPT_AUDIO)), + Node("dark_theme", parents=[], cpt=_INDEP["dark_theme"]["table"]), +]) + + +# ═══════════════════════════════════════════════════════════════════════ +# FONT WHITELIST (Bayesian: core ∪ sampled_optional | gpu_class) +# ═══════════════════════════════════════════════════════════════════════ +# Semantic flip: previously exclude-list (block N probed fonts per seed). +# Now whitelist (browser sees ONLY these fonts, everything else hidden). +# Core (~112): always included — fresh Win11 + Office 2021 English. +# Optional (~40): sampled per-session with P(present | gpu_class). Gives +# small realistic variance (~3-8 optional fonts differ per session) while +# keeping the profile strongly centered on 'typical Windows user'. + + +def derive_font_prefs(gpu_class: str, rng) -> Dict[str, str]: + """Build COHERENT whitelist + metrics strings for the session. + + Sampling: + - Core fonts always included. + - Optional fonts sampled with P(font | gpu_class) from the CPT table. + + Returns: + { + "whitelist": "arial,calibri,marlett,...", + "metrics": "arial|0.978,calibri|0.934,marlett|0.855,..." + } + + The whitelist is the list of font families to advertise. The metrics + string encodes per-family width scale factors that the consumer can + use to make each family detectable by width-diff font probes. + + Each entry in font_pool.json carries its own {name, factor} pair so the + two pref strings are GUARANTEED coherent — no chance of a fabricated + font with factor 1.0 (undetectable) or a metrics entry for a font not + in the whitelist (useless). + + Markers & add-new-font: simply add an entry to font_pool.json:core (with + a factor at least 4% away from 1.0) — no special-case code needed. + """ + cpt = _CPT_FONTS_OPT.get(gpu_class) + if cpt is None: + cpt = _CPT_FONTS_OPT["integrated_modern"] + included: list = list(_FONT_CORE) # always present + for entry in _FONT_OPTIONAL: + name = entry["name"] + p = cpt.get(name, 0.7) # default 0.7 if CPT has no row for this font + if rng.random() < p: + included.append(entry) + # Deterministic ordering: sort by name + included.sort(key=lambda e: e["name"]) + whitelist = ",".join(e["name"] for e in included) + metrics = ",".join( + f'{e["name"]}|{e["factor"]:.3f}' for e in included + ) + return {"whitelist": whitelist, "metrics": metrics} + + +# Back-compat shim: legacy callers still import derive_font_whitelist. +def derive_font_whitelist(gpu_class: str, rng) -> str: + return derive_font_prefs(gpu_class, rng)["whitelist"] + + +# ═══════════════════════════════════════════════════════════════════════ +# PUBLIC API: Forge +# ═══════════════════════════════════════════════════════════════════════ +import random + + +class Forge: + """Fingerprint forge — single seed → coherent bundle.""" + + def __init__(self, seed: int): + self.seed = int(seed) + self._rng = random.Random(self.seed) + + def sample(self) -> Dict[str, Any]: + bundle = _NETWORK.sample(self._rng) + gpu = bundle["gpu"] + screen = bundle["screen"] + audio = bundle["audio"] + codec = bundle["codec"] + return { + # Seed tracking + "stealth_seed": self.seed, + # Locked identity + **_LOCKED, + # GPU (coherent pair from 444 pool) + "webgl_renderer": gpu["renderer"], + "webgl_vendor": gpu["vendor"], + "gpu_class": bundle["gpu_class"], + # Hidden-variable debug metadata (not a Firefox pref, just for + # analysis / test result correlation tracking) + "intra_tier": bundle["intra_tier"], + "screen_tier": bundle["screen_tier"], + # Screen (coherent with GPU class) + "screen_w": int(screen["w"]), + "screen_h": int(screen["h"]), + "screen_avail_w": int(screen.get("aw", screen["w"])), + "screen_avail_h": int(screen.get("ah", screen["h"] - 40)), + "dpr": float(screen["dpr"]), + # Hardware (coherent with GPU class) + "hw_concurrency": int(bundle["hw_concurrency"]), + # WebGL MSAA (coherent with GPU class) + "msaa_samples": int(bundle["msaa_samples"]), + # Audio (independent joint) + "audio_sample_rate": int(audio["rate"]), + "audio_output_latency_ms": int(audio["latency"]), + "audio_max_channel_count": int(audio["channels"]), + # Codec prefs (joint, coherent with GPU class). All 5 are + # JS-visible: av1/webm_encoder via canPlayType/MediaRecorder, + # mediasource_* via MediaSource.isTypeSupported, webspeech_synth + # via 'speechSynthesis' in window (CreepJS voices probe). + "av1_enabled": bool(codec["av1_enabled"]), + "webm_encoder_enabled": bool(codec["webm_encoder_enabled"]), + "mediasource_webm": bool(codec["mediasource_webm"]), + "mediasource_mp4": bool(codec["mediasource_mp4"]), + "webspeech_synth": bool(codec["webspeech_synth"]), + # Storage quota MB (coherent with GPU class — workstation larger SSDs). + "storage_quota_mb": int(bundle["storage_quota_mb"]), + # Independent marginals + "dark_theme": int(bundle["dark_theme"]), + # Bayesian font prefs (coherent pair: whitelist + per-family + # width scale metrics, both sampled from the same font_pool.json + # and conditioned on gpu_class). + **{ + f"font_{k}": v + for k, v in derive_font_prefs( + bundle["gpu_class"], self._rng + ).items() + }, + } + + +def sample(seed: int) -> Dict[str, Any]: + """Convenience: `Forge(seed).sample()`.""" + return Forge(seed).sample() diff --git a/src/stealthfox/_fpforge/data/cpt_audio_given_class.json b/src/stealthfox/_fpforge/data/cpt_audio_given_class.json new file mode 100644 index 0000000..cd91672 --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_audio_given_class.json @@ -0,0 +1,193 @@ +{ + "_meta": "audio (rate/latency/channels) given gpu_class", + "table": { + "integrated_old": [ + { + "value": { + "rate": 44100, + "latency": 50, + "channels": 2 + }, + "prob": 0.7 + }, + { + "value": { + "rate": 48000, + "latency": 50, + "channels": 2 + }, + "prob": 0.3 + } + ], + "integrated_modern": [ + { + "value": { + "rate": 48000, + "latency": 30, + "channels": 2 + }, + "prob": 0.6 + }, + { + "value": { + "rate": 44100, + "latency": 40, + "channels": 2 + }, + "prob": 0.25 + }, + { + "value": { + "rate": 48000, + "latency": 25, + "channels": 6 + }, + "prob": 0.15 + } + ], + "low_end": [ + { + "value": { + "rate": 48000, + "latency": 40, + "channels": 2 + }, + "prob": 0.55 + }, + { + "value": { + "rate": 44100, + "latency": 50, + "channels": 2 + }, + "prob": 0.3 + }, + { + "value": { + "rate": 48000, + "latency": 30, + "channels": 6 + }, + "prob": 0.15 + } + ], + "mid_range": [ + { + "value": { + "rate": 48000, + "latency": 25, + "channels": 2 + }, + "prob": 0.45 + }, + { + "value": { + "rate": 48000, + "latency": 20, + "channels": 6 + }, + "prob": 0.3 + }, + { + "value": { + "rate": 48000, + "latency": 20, + "channels": 8 + }, + "prob": 0.15 + }, + { + "value": { + "rate": 44100, + "latency": 30, + "channels": 2 + }, + "prob": 0.1 + } + ], + "high_end": [ + { + "value": { + "rate": 48000, + "latency": 15, + "channels": 6 + }, + "prob": 0.3 + }, + { + "value": { + "rate": 48000, + "latency": 15, + "channels": 8 + }, + "prob": 0.3 + }, + { + "value": { + "rate": 48000, + "latency": 15, + "channels": 2 + }, + "prob": 0.2 + }, + { + "value": { + "rate": 96000, + "latency": 15, + "channels": 6 + }, + "prob": 0.1 + }, + { + "value": { + "rate": 96000, + "latency": 15, + "channels": 8 + }, + "prob": 0.1 + } + ], + "workstation": [ + { + "value": { + "rate": 48000, + "latency": 10, + "channels": 8 + }, + "prob": 0.25 + }, + { + "value": { + "rate": 96000, + "latency": 10, + "channels": 8 + }, + "prob": 0.3 + }, + { + "value": { + "rate": 96000, + "latency": 10, + "channels": 6 + }, + "prob": 0.2 + }, + { + "value": { + "rate": 192000, + "latency": 10, + "channels": 8 + }, + "prob": 0.15 + }, + { + "value": { + "rate": 48000, + "latency": 15, + "channels": 6 + }, + "prob": 0.1 + } + ] + } +} \ No newline at end of file diff --git a/src/stealthfox/_fpforge/data/cpt_codec_given_class.json b/src/stealthfox/_fpforge/data/cpt_codec_given_class.json new file mode 100644 index 0000000..e5a3d05 --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_codec_given_class.json @@ -0,0 +1,147 @@ +{ + "_meta": "codec given gpu_class", + "table": { + "integrated_old": [ + { + "value": { + "av1_enabled": false, + "webm_encoder_enabled": false, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": true + }, + "prob": 1.0 + } + ], + "integrated_modern": [ + { + "value": { + "av1_enabled": true, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": true + }, + "prob": 0.55 + }, + { + "value": { + "av1_enabled": false, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": true + }, + "prob": 0.35 + }, + { + "value": { + "av1_enabled": true, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": false + }, + "prob": 0.1 + } + ], + "low_end": [ + { + "value": { + "av1_enabled": false, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": true + }, + "prob": 0.85 + }, + { + "value": { + "av1_enabled": false, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": false + }, + "prob": 0.15 + } + ], + "mid_range": [ + { + "value": { + "av1_enabled": true, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": true + }, + "prob": 0.55 + }, + { + "value": { + "av1_enabled": false, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": true + }, + "prob": 0.35 + }, + { + "value": { + "av1_enabled": true, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": false + }, + "prob": 0.1 + } + ], + "high_end": [ + { + "value": { + "av1_enabled": true, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": true + }, + "prob": 0.85 + }, + { + "value": { + "av1_enabled": true, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": false + }, + "prob": 0.15 + } + ], + "workstation": [ + { + "value": { + "av1_enabled": true, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": true + }, + "prob": 0.7 + }, + { + "value": { + "av1_enabled": true, + "webm_encoder_enabled": true, + "mediasource_webm": true, + "mediasource_mp4": true, + "webspeech_synth": false + }, + "prob": 0.3 + } + ] + } +} \ No newline at end of file diff --git a/src/stealthfox/_fpforge/data/cpt_fonts_optional_given_class.json b/src/stealthfox/_fpforge/data/cpt_fonts_optional_given_class.json new file mode 100644 index 0000000..12c098d --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_fonts_optional_given_class.json @@ -0,0 +1,295 @@ +{ + "_meta": { + "name": "optional_font presence | gpu_class", + "parents": [ + "gpu_class" + ], + "child": "optional_fonts_subset", + "description": "Per-optional-font presence probabilities given gpu_class. Each optional font in font_pool.json sampled INDEPENDENTLY with P(present | gpu_class) given here. Integrated_old: fewer language packs (older/cheaper machines). Workstation: more regional/language packs (international users, enterprise deployments).", + "rationale": "Near-invariant by design: most optional fonts have baseline P ~ 0.60-0.85 across classes. Per-session variance is small (~3-6 fonts toggling on/off out of 40 optional). Result: fingerprint always looks like 'typical Windows 11 + Office', with small realistic per-user variance in regional support." + }, + "table": { + "integrated_old": { + "aparajita": 0.45, + "calibri": 0.9, + "dengxian": 0.3, + "dfkai-sb": 0.25, + "dokchampa": 0.2, + "estrangelo edessa": 0.55, + "euphemia": 0.6, + "fangsong": 0.35, + "gadugi": 0.88, + "gautami": 0.55, + "helv": 0.7, + "iskoola pota": 0.5, + "javanese text": 0.85, + "kaiti": 0.3, + "kalinga": 0.5, + "kartika": 0.5, + "khmer ui": 0.35, + "kokila": 0.55, + "lao ui": 0.35, + "latha": 0.65, + "leelawadee ui": 0.87, + "mangal": 0.65, + "meiryo": 0.6, + "microsoft uighur": 0.4, + "ms pmincho": 0.65, + "ms reference sans serif": 0.55, + "ms reference specialty": 0.5, + "ms ui gothic": 0.82, + "myanmar text": 0.84, + "nyala": 0.55, + "plantagenet cherokee": 0.6, + "raavi": 0.55, + "segoe fluent icons": 0.5, + "segoe ui light": 0.75, + "shonar bangla": 0.55, + "shruti": 0.55, + "simkai": 0.25, + "small fonts": 0.75, + "traditional arabic": 0.55, + "tunga": 0.55, + "urdu typesetting": 0.45, + "utsaah": 0.55, + "vani": 0.55, + "vijaya": 0.55, + "yu mincho": 0.55 + }, + "integrated_modern": { + "aparajita": 0.65, + "calibri": 0.78, + "dengxian": 0.5, + "dfkai-sb": 0.4, + "dokchampa": 0.4, + "estrangelo edessa": 0.7, + "euphemia": 0.78, + "fangsong": 0.55, + "gadugi": 0.96, + "gautami": 0.75, + "helv": 0.6, + "iskoola pota": 0.7, + "javanese text": 0.94, + "kaiti": 0.45, + "kalinga": 0.7, + "kartika": 0.7, + "khmer ui": 0.55, + "kokila": 0.75, + "lao ui": 0.55, + "latha": 0.82, + "leelawadee ui": 0.95, + "mangal": 0.82, + "meiryo": 0.8, + "microsoft uighur": 0.55, + "ms pmincho": 0.85, + "ms reference sans serif": 0.72, + "ms reference specialty": 0.68, + "ms ui gothic": 0.75, + "myanmar text": 0.94, + "nyala": 0.72, + "plantagenet cherokee": 0.78, + "raavi": 0.72, + "segoe fluent icons": 0.92, + "segoe ui light": 0.72, + "shonar bangla": 0.72, + "shruti": 0.72, + "simkai": 0.4, + "small fonts": 0.65, + "traditional arabic": 0.72, + "tunga": 0.72, + "urdu typesetting": 0.65, + "utsaah": 0.72, + "vani": 0.72, + "vijaya": 0.72, + "yu mincho": 0.75 + }, + "low_end": { + "aparajita": 0.55, + "calibri": 0.94, + "dengxian": 0.4, + "dfkai-sb": 0.3, + "dokchampa": 0.3, + "estrangelo edessa": 0.62, + "euphemia": 0.68, + "fangsong": 0.45, + "gadugi": 0.93, + "gautami": 0.65, + "helv": 0.78, + "iskoola pota": 0.6, + "javanese text": 0.91, + "kaiti": 0.35, + "kalinga": 0.6, + "kartika": 0.6, + "khmer ui": 0.45, + "kokila": 0.65, + "lao ui": 0.45, + "latha": 0.72, + "leelawadee ui": 0.92, + "mangal": 0.72, + "meiryo": 0.7, + "microsoft uighur": 0.48, + "ms pmincho": 0.75, + "ms reference sans serif": 0.62, + "ms reference specialty": 0.58, + "ms ui gothic": 0.9, + "myanmar text": 0.9, + "nyala": 0.62, + "plantagenet cherokee": 0.68, + "raavi": 0.62, + "segoe fluent icons": 0.8, + "segoe ui light": 0.88, + "shonar bangla": 0.62, + "shruti": 0.62, + "simkai": 0.3, + "small fonts": 0.83, + "traditional arabic": 0.62, + "tunga": 0.62, + "urdu typesetting": 0.55, + "utsaah": 0.62, + "vani": 0.62, + "vijaya": 0.62, + "yu mincho": 0.65 + }, + "mid_range": { + "aparajita": 0.72, + "calibri": 0.98, + "dengxian": 0.6, + "dfkai-sb": 0.5, + "dokchampa": 0.5, + "estrangelo edessa": 0.78, + "euphemia": 0.82, + "fangsong": 0.65, + "gadugi": 0.97, + "gautami": 0.8, + "helv": 0.85, + "iskoola pota": 0.78, + "javanese text": 0.96, + "kaiti": 0.55, + "kalinga": 0.78, + "kartika": 0.78, + "khmer ui": 0.65, + "kokila": 0.8, + "lao ui": 0.65, + "latha": 0.85, + "leelawadee ui": 0.97, + "mangal": 0.85, + "meiryo": 0.85, + "microsoft uighur": 0.65, + "ms pmincho": 0.88, + "ms reference sans serif": 0.78, + "ms reference specialty": 0.75, + "ms ui gothic": 0.96, + "myanmar text": 0.96, + "nyala": 0.78, + "plantagenet cherokee": 0.82, + "raavi": 0.78, + "segoe fluent icons": 0.94, + "segoe ui light": 0.94, + "shonar bangla": 0.78, + "shruti": 0.78, + "simkai": 0.5, + "small fonts": 0.9, + "traditional arabic": 0.78, + "tunga": 0.78, + "urdu typesetting": 0.72, + "utsaah": 0.78, + "vani": 0.78, + "vijaya": 0.78, + "yu mincho": 0.8 + }, + "high_end": { + "aparajita": 0.8, + "calibri": 0.99, + "dengxian": 0.7, + "dfkai-sb": 0.6, + "dokchampa": 0.6, + "estrangelo edessa": 0.85, + "euphemia": 0.88, + "fangsong": 0.72, + "gadugi": 0.98, + "gautami": 0.85, + "helv": 0.88, + "iskoola pota": 0.82, + "javanese text": 0.97, + "kaiti": 0.65, + "kalinga": 0.82, + "kartika": 0.82, + "khmer ui": 0.72, + "kokila": 0.85, + "lao ui": 0.72, + "latha": 0.9, + "leelawadee ui": 0.98, + "mangal": 0.9, + "meiryo": 0.9, + "microsoft uighur": 0.72, + "ms pmincho": 0.92, + "ms reference sans serif": 0.85, + "ms reference specialty": 0.82, + "ms ui gothic": 0.98, + "myanmar text": 0.97, + "nyala": 0.85, + "plantagenet cherokee": 0.88, + "raavi": 0.85, + "segoe fluent icons": 0.97, + "segoe ui light": 0.96, + "shonar bangla": 0.85, + "shruti": 0.85, + "simkai": 0.6, + "small fonts": 0.92, + "traditional arabic": 0.85, + "tunga": 0.85, + "urdu typesetting": 0.8, + "utsaah": 0.85, + "vani": 0.85, + "vijaya": 0.85, + "yu mincho": 0.88 + }, + "workstation": { + "aparajita": 0.88, + "calibri": 0.99, + "dengxian": 0.8, + "dfkai-sb": 0.75, + "dokchampa": 0.72, + "estrangelo edessa": 0.9, + "euphemia": 0.92, + "fangsong": 0.82, + "gadugi": 0.99, + "gautami": 0.92, + "helv": 0.9, + "iskoola pota": 0.9, + "javanese text": 0.98, + "kaiti": 0.78, + "kalinga": 0.9, + "kartika": 0.9, + "khmer ui": 0.8, + "kokila": 0.92, + "lao ui": 0.8, + "latha": 0.95, + "leelawadee ui": 0.99, + "mangal": 0.95, + "meiryo": 0.95, + "microsoft uighur": 0.82, + "ms pmincho": 0.95, + "ms reference sans serif": 0.92, + "ms reference specialty": 0.9, + "ms ui gothic": 0.98, + "myanmar text": 0.98, + "nyala": 0.92, + "plantagenet cherokee": 0.92, + "raavi": 0.92, + "segoe fluent icons": 0.98, + "segoe ui light": 0.97, + "shonar bangla": 0.92, + "shruti": 0.92, + "simkai": 0.72, + "small fonts": 0.94, + "traditional arabic": 0.92, + "tunga": 0.92, + "urdu typesetting": 0.88, + "utsaah": 0.92, + "vani": 0.92, + "vijaya": 0.92, + "yu mincho": 0.92 + } + } +} \ No newline at end of file diff --git a/src/stealthfox/_fpforge/data/cpt_hwc_given_class.json b/src/stealthfox/_fpforge/data/cpt_hwc_given_class.json new file mode 100644 index 0000000..68c6ce9 --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_hwc_given_class.json @@ -0,0 +1,50 @@ +{ + "_meta": { + "name": "hw_concurrency | gpu_class", + "parents": ["gpu_class"], + "child": "hw_concurrency", + "source": "Curated from realistic CPU/GPU pairings (Steam HW Survey + DIY build norms + laptop SKU patterns)" + }, + "table": { + "integrated_old": [ + {"value": 2, "prob": 0.20}, + {"value": 4, "prob": 0.60}, + {"value": 8, "prob": 0.20} + ], + "integrated_modern": [ + {"value": 4, "prob": 0.20}, + {"value": 6, "prob": 0.15}, + {"value": 8, "prob": 0.35}, + {"value": 12, "prob": 0.20}, + {"value": 16, "prob": 0.10} + ], + "low_end": [ + {"value": 4, "prob": 0.25}, + {"value": 6, "prob": 0.25}, + {"value": 8, "prob": 0.35}, + {"value": 12, "prob": 0.15} + ], + "mid_range": [ + {"value": 6, "prob": 0.15}, + {"value": 8, "prob": 0.30}, + {"value": 12, "prob": 0.30}, + {"value": 16, "prob": 0.20}, + {"value": 24, "prob": 0.05} + ], + "high_end": [ + {"value": 8, "prob": 0.10}, + {"value": 12, "prob": 0.25}, + {"value": 16, "prob": 0.35}, + {"value": 20, "prob": 0.05}, + {"value": 24, "prob": 0.20}, + {"value": 32, "prob": 0.05} + ], + "workstation": [ + {"value": 8, "prob": 0.15}, + {"value": 12, "prob": 0.20}, + {"value": 16, "prob": 0.30}, + {"value": 24, "prob": 0.20}, + {"value": 32, "prob": 0.15} + ] + } +} diff --git a/src/stealthfox/_fpforge/data/cpt_hwc_given_class_tier.json b/src/stealthfox/_fpforge/data/cpt_hwc_given_class_tier.json new file mode 100644 index 0000000..2418cdb --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_hwc_given_class_tier.json @@ -0,0 +1,349 @@ +{ + "_meta": "hardware_concurrency given (gpu_class, intra_tier)", + "table": { + "[\"integrated_old\", \"budget\"]": [ + { + "value": 2, + "prob": 0.65 + }, + { + "value": 4, + "prob": 0.3 + }, + { + "value": 8, + "prob": 0.05 + } + ], + "[\"integrated_old\", \"standard\"]": [ + { + "value": 2, + "prob": 0.3 + }, + { + "value": 4, + "prob": 0.55 + }, + { + "value": 8, + "prob": 0.15 + } + ], + "[\"integrated_old\", \"premium\"]": [ + { + "value": 4, + "prob": 0.65 + }, + { + "value": 8, + "prob": 0.3 + }, + { + "value": 12, + "prob": 0.05 + } + ], + "[\"integrated_modern\", \"budget\"]": [ + { + "value": 4, + "prob": 0.55 + }, + { + "value": 6, + "prob": 0.2 + }, + { + "value": 8, + "prob": 0.2 + }, + { + "value": 12, + "prob": 0.05 + } + ], + "[\"integrated_modern\", \"standard\"]": [ + { + "value": 6, + "prob": 0.2 + }, + { + "value": 8, + "prob": 0.3 + }, + { + "value": 10, + "prob": 0.2 + }, + { + "value": 12, + "prob": 0.2 + }, + { + "value": 16, + "prob": 0.1 + } + ], + "[\"integrated_modern\", \"premium\"]": [ + { + "value": 8, + "prob": 0.2 + }, + { + "value": 10, + "prob": 0.2 + }, + { + "value": 12, + "prob": 0.3 + }, + { + "value": 14, + "prob": 0.15 + }, + { + "value": 16, + "prob": 0.15 + } + ], + "[\"low_end\", \"budget\"]": [ + { + "value": 4, + "prob": 0.5 + }, + { + "value": 6, + "prob": 0.25 + }, + { + "value": 8, + "prob": 0.2 + }, + { + "value": 12, + "prob": 0.05 + } + ], + "[\"low_end\", \"standard\"]": [ + { + "value": 4, + "prob": 0.1 + }, + { + "value": 6, + "prob": 0.35 + }, + { + "value": 8, + "prob": 0.3 + }, + { + "value": 12, + "prob": 0.18 + }, + { + "value": 16, + "prob": 0.07 + } + ], + "[\"low_end\", \"premium\"]": [ + { + "value": 6, + "prob": 0.1 + }, + { + "value": 8, + "prob": 0.3 + }, + { + "value": 12, + "prob": 0.3 + }, + { + "value": 16, + "prob": 0.22 + }, + { + "value": 24, + "prob": 0.08 + } + ], + "[\"mid_range\", \"budget\"]": [ + { + "value": 6, + "prob": 0.55 + }, + { + "value": 8, + "prob": 0.3 + }, + { + "value": 12, + "prob": 0.1 + }, + { + "value": 16, + "prob": 0.05 + } + ], + "[\"mid_range\", \"standard\"]": [ + { + "value": 6, + "prob": 0.4 + }, + { + "value": 8, + "prob": 0.3 + }, + { + "value": 12, + "prob": 0.18 + }, + { + "value": 16, + "prob": 0.1 + }, + { + "value": 24, + "prob": 0.02 + } + ], + "[\"mid_range\", \"premium\"]": [ + { + "value": 6, + "prob": 0.15 + }, + { + "value": 8, + "prob": 0.45 + }, + { + "value": 12, + "prob": 0.2 + }, + { + "value": 16, + "prob": 0.15 + }, + { + "value": 24, + "prob": 0.05 + } + ], + "[\"high_end\", \"budget\"]": [ + { + "value": 6, + "prob": 0.1 + }, + { + "value": 8, + "prob": 0.55 + }, + { + "value": 12, + "prob": 0.2 + }, + { + "value": 16, + "prob": 0.15 + } + ], + "[\"high_end\", \"standard\"]": [ + { + "value": 8, + "prob": 0.3 + }, + { + "value": 12, + "prob": 0.18 + }, + { + "value": 14, + "prob": 0.15 + }, + { + "value": 16, + "prob": 0.27 + }, + { + "value": 24, + "prob": 0.1 + } + ], + "[\"high_end\", \"premium\"]": [ + { + "value": 12, + "prob": 0.1 + }, + { + "value": 14, + "prob": 0.15 + }, + { + "value": 16, + "prob": 0.3 + }, + { + "value": 24, + "prob": 0.35 + }, + { + "value": 32, + "prob": 0.1 + } + ], + "[\"workstation\", \"budget\"]": [ + { + "value": 8, + "prob": 0.4 + }, + { + "value": 12, + "prob": 0.3 + }, + { + "value": 16, + "prob": 0.2 + }, + { + "value": 24, + "prob": 0.1 + } + ], + "[\"workstation\", \"standard\"]": [ + { + "value": 12, + "prob": 0.1 + }, + { + "value": 16, + "prob": 0.4 + }, + { + "value": 24, + "prob": 0.3 + }, + { + "value": 32, + "prob": 0.2 + } + ], + "[\"workstation\", \"premium\"]": [ + { + "value": 24, + "prob": 0.3 + }, + { + "value": 32, + "prob": 0.45 + }, + { + "value": 48, + "prob": 0.15 + }, + { + "value": 64, + "prob": 0.1 + } + ] + } +} \ No newline at end of file diff --git a/src/stealthfox/_fpforge/data/cpt_intra_tier_given_class.json b/src/stealthfox/_fpforge/data/cpt_intra_tier_given_class.json new file mode 100644 index 0000000..fcd8f31 --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_intra_tier_given_class.json @@ -0,0 +1,17 @@ +{ + "_meta": { + "name": "intra_tier | gpu_class", + "parents": ["gpu_class"], + "child": "intra_tier", + "value_type": "str (budget|standard|premium)", + "rationale": "Hidden variable catching 'intra-class premium-ness'. Within a gpu_class, users vary in how premium their OTHER components are (RAM, SSD, cores). A mid_range GPU paired with 32 cores + 2TB SSD is a 'premium' mid_range user. Without this var, hwc/screen/storage would be statistically independent given gpu_class — unrealistic (real users pick coherent bundles). Tier distribution biased: integrated_old mostly budget/standard, workstation mostly premium." + }, + "table": { + "integrated_old": [{"value": "budget", "prob": 0.65}, {"value": "standard", "prob": 0.30}, {"value": "premium", "prob": 0.05}], + "integrated_modern": [{"value": "budget", "prob": 0.30}, {"value": "standard", "prob": 0.55}, {"value": "premium", "prob": 0.15}], + "low_end": [{"value": "budget", "prob": 0.50}, {"value": "standard", "prob": 0.40}, {"value": "premium", "prob": 0.10}], + "mid_range": [{"value": "budget", "prob": 0.25}, {"value": "standard", "prob": 0.50}, {"value": "premium", "prob": 0.25}], + "high_end": [{"value": "budget", "prob": 0.15}, {"value": "standard", "prob": 0.50}, {"value": "premium", "prob": 0.35}], + "workstation": [{"value": "budget", "prob": 0.10}, {"value": "standard", "prob": 0.40}, {"value": "premium", "prob": 0.50}] + } +} diff --git a/src/stealthfox/_fpforge/data/cpt_msaa_given_class.json b/src/stealthfox/_fpforge/data/cpt_msaa_given_class.json new file mode 100644 index 0000000..e5f22d8 --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_msaa_given_class.json @@ -0,0 +1,16 @@ +{ + "_meta": { + "name": "webgl.msaa-samples | gpu_class", + "parents": ["gpu_class"], + "child": "msaa_samples", + "source": "High-end GPUs more likely to have MSAA enabled in games; integrated usually off" + }, + "table": { + "integrated_old": [{"value": 0, "prob": 0.85}, {"value": 2, "prob": 0.15}], + "integrated_modern": [{"value": 0, "prob": 0.70}, {"value": 2, "prob": 0.25}, {"value": 4, "prob": 0.05}], + "low_end": [{"value": 0, "prob": 0.55}, {"value": 2, "prob": 0.35}, {"value": 4, "prob": 0.10}], + "mid_range": [{"value": 0, "prob": 0.35}, {"value": 2, "prob": 0.35}, {"value": 4, "prob": 0.30}], + "high_end": [{"value": 0, "prob": 0.20}, {"value": 2, "prob": 0.25}, {"value": 4, "prob": 0.55}], + "workstation": [{"value": 0, "prob": 0.50}, {"value": 2, "prob": 0.25}, {"value": 4, "prob": 0.25}] + } +} diff --git a/src/stealthfox/_fpforge/data/cpt_msaa_given_class_screen.json b/src/stealthfox/_fpforge/data/cpt_msaa_given_class_screen.json new file mode 100644 index 0000000..adf5c9e --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_msaa_given_class_screen.json @@ -0,0 +1,305 @@ +{ + "_meta": "msaa_samples given (gpu_class, screen_tier)", + "table": { + "[\"integrated_old\", \"1080p\"]": [ + { + "value": 2, + "prob": 0.75 + }, + { + "value": 4, + "prob": 0.2 + }, + { + "value": 8, + "prob": 0.05 + } + ], + "[\"integrated_old\", \"1440p\"]": [ + { + "value": 2, + "prob": 0.85 + }, + { + "value": 4, + "prob": 0.15 + } + ], + "[\"integrated_old\", \"2160p\"]": [ + { + "value": 2, + "prob": 1.0 + } + ], + "[\"integrated_old\", \"ultrawide\"]": [ + { + "value": 2, + "prob": 1.0 + } + ], + "[\"integrated_modern\", \"1080p\"]": [ + { + "value": 2, + "prob": 0.5 + }, + { + "value": 4, + "prob": 0.4 + }, + { + "value": 8, + "prob": 0.1 + } + ], + "[\"integrated_modern\", \"1440p\"]": [ + { + "value": 2, + "prob": 0.6 + }, + { + "value": 4, + "prob": 0.35 + }, + { + "value": 8, + "prob": 0.05 + } + ], + "[\"integrated_modern\", \"2160p\"]": [ + { + "value": 2, + "prob": 0.85 + }, + { + "value": 4, + "prob": 0.15 + } + ], + "[\"integrated_modern\", \"ultrawide\"]": [ + { + "value": 2, + "prob": 0.8 + }, + { + "value": 4, + "prob": 0.2 + } + ], + "[\"low_end\", \"1080p\"]": [ + { + "value": 2, + "prob": 0.4 + }, + { + "value": 4, + "prob": 0.45 + }, + { + "value": 8, + "prob": 0.15 + } + ], + "[\"low_end\", \"1440p\"]": [ + { + "value": 2, + "prob": 0.55 + }, + { + "value": 4, + "prob": 0.4 + }, + { + "value": 8, + "prob": 0.05 + } + ], + "[\"low_end\", \"2160p\"]": [ + { + "value": 2, + "prob": 0.85 + }, + { + "value": 4, + "prob": 0.15 + } + ], + "[\"low_end\", \"ultrawide\"]": [ + { + "value": 2, + "prob": 0.7 + }, + { + "value": 4, + "prob": 0.3 + } + ], + "[\"mid_range\", \"1080p\"]": [ + { + "value": 2, + "prob": 0.3 + }, + { + "value": 4, + "prob": 0.5 + }, + { + "value": 8, + "prob": 0.2 + } + ], + "[\"mid_range\", \"1440p\"]": [ + { + "value": 2, + "prob": 0.4 + }, + { + "value": 4, + "prob": 0.45 + }, + { + "value": 8, + "prob": 0.15 + } + ], + "[\"mid_range\", \"2160p\"]": [ + { + "value": 2, + "prob": 0.65 + }, + { + "value": 4, + "prob": 0.3 + }, + { + "value": 8, + "prob": 0.05 + } + ], + "[\"mid_range\", \"ultrawide\"]": [ + { + "value": 2, + "prob": 0.55 + }, + { + "value": 4, + "prob": 0.4 + }, + { + "value": 8, + "prob": 0.05 + } + ], + "[\"high_end\", \"1080p\"]": [ + { + "value": 2, + "prob": 0.2 + }, + { + "value": 4, + "prob": 0.45 + }, + { + "value": 8, + "prob": 0.35 + } + ], + "[\"high_end\", \"1440p\"]": [ + { + "value": 2, + "prob": 0.25 + }, + { + "value": 4, + "prob": 0.5 + }, + { + "value": 8, + "prob": 0.25 + } + ], + "[\"high_end\", \"2160p\"]": [ + { + "value": 2, + "prob": 0.4 + }, + { + "value": 4, + "prob": 0.45 + }, + { + "value": 8, + "prob": 0.15 + } + ], + "[\"high_end\", \"ultrawide\"]": [ + { + "value": 2, + "prob": 0.3 + }, + { + "value": 4, + "prob": 0.5 + }, + { + "value": 8, + "prob": 0.2 + } + ], + "[\"workstation\", \"1080p\"]": [ + { + "value": 2, + "prob": 0.15 + }, + { + "value": 4, + "prob": 0.5 + }, + { + "value": 8, + "prob": 0.35 + } + ], + "[\"workstation\", \"1440p\"]": [ + { + "value": 2, + "prob": 0.15 + }, + { + "value": 4, + "prob": 0.55 + }, + { + "value": 8, + "prob": 0.3 + } + ], + "[\"workstation\", \"2160p\"]": [ + { + "value": 2, + "prob": 0.25 + }, + { + "value": 4, + "prob": 0.55 + }, + { + "value": 8, + "prob": 0.2 + } + ], + "[\"workstation\", \"ultrawide\"]": [ + { + "value": 2, + "prob": 0.2 + }, + { + "value": 4, + "prob": 0.55 + }, + { + "value": 8, + "prob": 0.25 + } + ] + } +} \ No newline at end of file diff --git a/src/stealthfox/_fpforge/data/cpt_screen_given_class.json b/src/stealthfox/_fpforge/data/cpt_screen_given_class.json new file mode 100644 index 0000000..f1f3c02 --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_screen_given_class.json @@ -0,0 +1,54 @@ +{ + "_meta": { + "name": "screen | gpu_class", + "parents": ["gpu_class"], + "child": "screen", + "value_shape": {"w": "int css px", "h": "int css px", "dpr": "float", "aw": "availWidth", "ah": "availHeight"}, + "source": "Curated Windows desktop/laptop resolutions. Restricted to width >= 1920 to avoid FP Pro's server-side screen_resolution normalization (FP Pro sorts non-standard landscape resolutions like 1536x864 into [smaller, larger] order in raw_device_attributes.screen_resolution, appearing flipped vs our spoof). All retained resolutions are common and NOT normalized by FP Pro." + }, + "table": { + "integrated_old": [ + {"value": {"w": 1920, "h": 1080, "dpr": 1.0, "aw": 1920, "ah": 1040}, "prob": 0.90}, + {"value": {"w": 1920, "h": 1200, "dpr": 1.0, "aw": 1920, "ah": 1160}, "prob": 0.10} + ], + "integrated_modern": [ + {"value": {"w": 1920, "h": 1080, "dpr": 1.0, "aw": 1920, "ah": 1040}, "prob": 0.60}, + {"value": {"w": 1920, "h": 1080, "dpr": 1.25, "aw": 1920, "ah": 1040}, "prob": 0.18}, + {"value": {"w": 2560, "h": 1440, "dpr": 1.0, "aw": 2560, "ah": 1400}, "prob": 0.10}, + {"value": {"w": 1920, "h": 1200, "dpr": 1.0, "aw": 1920, "ah": 1160}, "prob": 0.08}, + {"value": {"w": 2560, "h": 1600, "dpr": 1.25, "aw": 2560, "ah": 1560}, "prob": 0.04} + ], + "low_end": [ + {"value": {"w": 1920, "h": 1080, "dpr": 1.0, "aw": 1920, "ah": 1040}, "prob": 0.70}, + {"value": {"w": 2560, "h": 1440, "dpr": 1.0, "aw": 2560, "ah": 1400}, "prob": 0.20}, + {"value": {"w": 1920, "h": 1200, "dpr": 1.0, "aw": 1920, "ah": 1160}, "prob": 0.10} + ], + "mid_range": [ + {"value": {"w": 1920, "h": 1080, "dpr": 1.0, "aw": 1920, "ah": 1040}, "prob": 0.50}, + {"value": {"w": 2560, "h": 1440, "dpr": 1.0, "aw": 2560, "ah": 1400}, "prob": 0.25}, + {"value": {"w": 1920, "h": 1080, "dpr": 1.25, "aw": 1920, "ah": 1040}, "prob": 0.08}, + {"value": {"w": 2560, "h": 1440, "dpr": 1.25, "aw": 2560, "ah": 1400}, "prob": 0.05}, + {"value": {"w": 3840, "h": 2160, "dpr": 1.0, "aw": 3840, "ah": 2120}, "prob": 0.04}, + {"value": {"w": 3840, "h": 2160, "dpr": 1.5, "aw": 3840, "ah": 2120}, "prob": 0.04}, + {"value": {"w": 2560, "h": 1080, "dpr": 1.0, "aw": 2560, "ah": 1040}, "prob": 0.04} + ], + "high_end": [ + {"value": {"w": 1920, "h": 1080, "dpr": 1.0, "aw": 1920, "ah": 1040}, "prob": 0.25}, + {"value": {"w": 2560, "h": 1440, "dpr": 1.0, "aw": 2560, "ah": 1400}, "prob": 0.30}, + {"value": {"w": 3840, "h": 2160, "dpr": 1.0, "aw": 3840, "ah": 2120}, "prob": 0.15}, + {"value": {"w": 3840, "h": 2160, "dpr": 1.5, "aw": 3840, "ah": 2120}, "prob": 0.10}, + {"value": {"w": 3440, "h": 1440, "dpr": 1.0, "aw": 3440, "ah": 1400}, "prob": 0.08}, + {"value": {"w": 2560, "h": 1440, "dpr": 1.25, "aw": 2560, "ah": 1400}, "prob": 0.05}, + {"value": {"w": 5120, "h": 1440, "dpr": 1.0, "aw": 5120, "ah": 1400}, "prob": 0.04}, + {"value": {"w": 3840, "h": 2160, "dpr": 2.0, "aw": 3840, "ah": 2120}, "prob": 0.03} + ], + "workstation": [ + {"value": {"w": 1920, "h": 1080, "dpr": 1.0, "aw": 1920, "ah": 1040}, "prob": 0.25}, + {"value": {"w": 2560, "h": 1440, "dpr": 1.0, "aw": 2560, "ah": 1400}, "prob": 0.25}, + {"value": {"w": 3840, "h": 2160, "dpr": 1.0, "aw": 3840, "ah": 2120}, "prob": 0.25}, + {"value": {"w": 3840, "h": 2160, "dpr": 1.5, "aw": 3840, "ah": 2120}, "prob": 0.15}, + {"value": {"w": 5120, "h": 2880, "dpr": 1.0, "aw": 5120, "ah": 2840}, "prob": 0.05}, + {"value": {"w": 3840, "h": 2400, "dpr": 1.5, "aw": 3840, "ah": 2360}, "prob": 0.05} + ] + } +} diff --git a/src/stealthfox/_fpforge/data/cpt_screen_given_class_tier.json b/src/stealthfox/_fpforge/data/cpt_screen_given_class_tier.json new file mode 100644 index 0000000..04771f7 --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_screen_given_class_tier.json @@ -0,0 +1,761 @@ +{ + "_meta": "screen given (gpu_class, intra_tier)", + "table": { + "[\"integrated_old\", \"budget\"]": [ + { + "value": { + "w": 1366, + "h": 768, + "aw": 1366, + "ah": 728, + "dpr": 1.0 + }, + "prob": 0.78 + }, + { + "value": { + "w": 1280, + "h": 800, + "aw": 1280, + "ah": 760, + "dpr": 1.0 + }, + "prob": 0.15 + }, + { + "value": { + "w": 1024, + "h": 768, + "aw": 1024, + "ah": 728, + "dpr": 1.0 + }, + "prob": 0.07 + } + ], + "[\"integrated_old\", \"standard\"]": [ + { + "value": { + "w": 1366, + "h": 768, + "aw": 1366, + "ah": 728, + "dpr": 1.0 + }, + "prob": 0.55 + }, + { + "value": { + "w": 1600, + "h": 900, + "aw": 1600, + "ah": 860, + "dpr": 1.0 + }, + "prob": 0.25 + }, + { + "value": { + "w": 1280, + "h": 800, + "aw": 1280, + "ah": 760, + "dpr": 1.0 + }, + "prob": 0.1 + }, + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.1 + } + ], + "[\"integrated_old\", \"premium\"]": [ + { + "value": { + "w": 1600, + "h": 900, + "aw": 1600, + "ah": 860, + "dpr": 1.0 + }, + "prob": 0.45 + }, + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.4 + }, + { + "value": { + "w": 1366, + "h": 768, + "aw": 1366, + "ah": 728, + "dpr": 1.0 + }, + "prob": 0.15 + } + ], + "[\"integrated_modern\", \"budget\"]": [ + { + "value": { + "w": 1366, + "h": 768, + "aw": 1366, + "ah": 728, + "dpr": 1.0 + }, + "prob": 0.3 + }, + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.65 + }, + { + "value": { + "w": 1600, + "h": 900, + "aw": 1600, + "ah": 860, + "dpr": 1.0 + }, + "prob": 0.05 + } + ], + "[\"integrated_modern\", \"standard\"]": [ + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.7 + }, + { + "value": { + "w": 1920, + "h": 1200, + "aw": 1920, + "ah": 1160, + "dpr": 1.0 + }, + "prob": 0.1 + }, + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.12 + }, + { + "value": { + "w": 2560, + "h": 1600, + "aw": 2560, + "ah": 1560, + "dpr": 1.0 + }, + "prob": 0.08 + } + ], + "[\"integrated_modern\", \"premium\"]": [ + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.3 + }, + { + "value": { + "w": 1920, + "h": 1200, + "aw": 1920, + "ah": 1160, + "dpr": 1.0 + }, + "prob": 0.1 + }, + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.25 + }, + { + "value": { + "w": 2560, + "h": 1600, + "aw": 2560, + "ah": 1560, + "dpr": 1.0 + }, + "prob": 0.15 + }, + { + "value": { + "w": 3840, + "h": 2160, + "aw": 3840, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.2 + } + ], + "[\"low_end\", \"budget\"]": [ + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.85 + }, + { + "value": { + "w": 1920, + "h": 1200, + "aw": 1920, + "ah": 1160, + "dpr": 1.0 + }, + "prob": 0.1 + }, + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.05 + } + ], + "[\"low_end\", \"standard\"]": [ + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.6 + }, + { + "value": { + "w": 1920, + "h": 1200, + "aw": 1920, + "ah": 1160, + "dpr": 1.0 + }, + "prob": 0.1 + }, + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.25 + }, + { + "value": { + "w": 3840, + "h": 2160, + "aw": 3840, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.05 + } + ], + "[\"low_end\", \"premium\"]": [ + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.25 + }, + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.45 + }, + { + "value": { + "w": 3840, + "h": 2160, + "aw": 3840, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.2 + }, + { + "value": { + "w": 3440, + "h": 1440, + "aw": 3440, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.05 + }, + { + "value": { + "w": 1920, + "h": 1200, + "aw": 1920, + "ah": 1160, + "dpr": 1.0 + }, + "prob": 0.05 + } + ], + "[\"mid_range\", \"budget\"]": [ + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.75 + }, + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.2 + }, + { + "value": { + "w": 1920, + "h": 1200, + "aw": 1920, + "ah": 1160, + "dpr": 1.0 + }, + "prob": 0.05 + } + ], + "[\"mid_range\", \"standard\"]": [ + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.45 + }, + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.4 + }, + { + "value": { + "w": 3840, + "h": 2160, + "aw": 3840, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.1 + }, + { + "value": { + "w": 3440, + "h": 1440, + "aw": 3440, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.05 + } + ], + "[\"mid_range\", \"premium\"]": [ + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.15 + }, + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.5 + }, + { + "value": { + "w": 3840, + "h": 2160, + "aw": 3840, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.2 + }, + { + "value": { + "w": 3440, + "h": 1440, + "aw": 3440, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.1 + }, + { + "value": { + "w": 2560, + "h": 1080, + "aw": 2560, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.05 + } + ], + "[\"high_end\", \"budget\"]": [ + { + "value": { + "w": 1920, + "h": 1080, + "aw": 1920, + "ah": 1040, + "dpr": 1.0 + }, + "prob": 0.2 + }, + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.55 + }, + { + "value": { + "w": 3840, + "h": 2160, + "aw": 3840, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.2 + }, + { + "value": { + "w": 3440, + "h": 1440, + "aw": 3440, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.05 + } + ], + "[\"high_end\", \"standard\"]": [ + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.4 + }, + { + "value": { + "w": 3840, + "h": 2160, + "aw": 3840, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.4 + }, + { + "value": { + "w": 3440, + "h": 1440, + "aw": 3440, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.15 + }, + { + "value": { + "w": 5120, + "h": 1440, + "aw": 5120, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.05 + } + ], + "[\"high_end\", \"premium\"]": [ + { + "value": { + "w": 3840, + "h": 2160, + "aw": 3840, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.55 + }, + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.15 + }, + { + "value": { + "w": 3440, + "h": 1440, + "aw": 3440, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.1 + }, + { + "value": { + "w": 5120, + "h": 1440, + "aw": 5120, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.15 + }, + { + "value": { + "w": 7680, + "h": 2160, + "aw": 7680, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.05 + } + ], + "[\"workstation\", \"budget\"]": [ + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.55 + }, + { + "value": { + "w": 3840, + "h": 2160, + "aw": 3840, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.3 + }, + { + "value": { + "w": 1920, + "h": 1200, + "aw": 1920, + "ah": 1160, + "dpr": 1.0 + }, + "prob": 0.1 + }, + { + "value": { + "w": 2560, + "h": 1600, + "aw": 2560, + "ah": 1560, + "dpr": 1.0 + }, + "prob": 0.05 + } + ], + "[\"workstation\", \"standard\"]": [ + { + "value": { + "w": 2560, + "h": 1440, + "aw": 2560, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.3 + }, + { + "value": { + "w": 3840, + "h": 2160, + "aw": 3840, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.45 + }, + { + "value": { + "w": 2560, + "h": 1600, + "aw": 2560, + "ah": 1560, + "dpr": 1.0 + }, + "prob": 0.15 + }, + { + "value": { + "w": 3440, + "h": 1440, + "aw": 3440, + "ah": 1400, + "dpr": 1.0 + }, + "prob": 0.1 + } + ], + "[\"workstation\", \"premium\"]": [ + { + "value": { + "w": 3840, + "h": 2160, + "aw": 3840, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.55 + }, + { + "value": { + "w": 2560, + "h": 1600, + "aw": 2560, + "ah": 1560, + "dpr": 1.0 + }, + "prob": 0.15 + }, + { + "value": { + "w": 5120, + "h": 2160, + "aw": 5120, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.15 + }, + { + "value": { + "w": 3840, + "h": 2400, + "aw": 3840, + "ah": 2360, + "dpr": 1.0 + }, + "prob": 0.1 + }, + { + "value": { + "w": 7680, + "h": 2160, + "aw": 7680, + "ah": 2120, + "dpr": 1.0 + }, + "prob": 0.05 + } + ] + } +} \ No newline at end of file diff --git a/src/stealthfox/_fpforge/data/cpt_storage_given_class.json b/src/stealthfox/_fpforge/data/cpt_storage_given_class.json new file mode 100644 index 0000000..c8647c9 --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_storage_given_class.json @@ -0,0 +1,60 @@ +{ + "_meta": { + "name": "storage_quota_mb | gpu_class", + "parents": ["gpu_class"], + "child": "storage_quota_mb", + "value_shape": "int megabytes (reported as navigator.storage.estimate().quota / (1024*1024))", + "description": "Storage quota override returned by navigator.storage.estimate(). Real Firefox computes quota from disk space, which is a fingerprinting signal (stable per host). We replace it with a realistic per-session value conditioned on gpu_class — modern/high-end users tend to have larger SSDs, integrated_old older/smaller disks.", + "source": "Typical Windows 11 disk sizes 2024-2026 per segment: budget laptops 256-512GB SSD, mainstream 512GB-1TB SSD, gaming/workstation 1-4TB SSD. Firefox default storage quota is ~50% of available disk (capped). We simulate that computation per-tier.", + "rationale": "Values are pre-quota-limit MB as Firefox would return to navigator.storage.estimate().quota. Probabilities cover realistic ranges centered on the segment median. A single session returns one value; fingerprinters that hash the exact quota get per-session variance, matching cross-user diversity." + }, + "table": { + "integrated_old": [ + {"value": 61440, "prob": 0.30}, + {"value": 102400, "prob": 0.25}, + {"value": 122880, "prob": 0.20}, + {"value": 204800, "prob": 0.15}, + {"value": 40960, "prob": 0.10} + ], + "integrated_modern": [ + {"value": 204800, "prob": 0.30}, + {"value": 307200, "prob": 0.25}, + {"value": 122880, "prob": 0.15}, + {"value": 409600, "prob": 0.15}, + {"value": 102400, "prob": 0.10}, + {"value": 512000, "prob": 0.05} + ], + "low_end": [ + {"value": 122880, "prob": 0.30}, + {"value": 204800, "prob": 0.25}, + {"value": 102400, "prob": 0.20}, + {"value": 61440, "prob": 0.15}, + {"value": 307200, "prob": 0.10} + ], + "mid_range": [ + {"value": 307200, "prob": 0.30}, + {"value": 409600, "prob": 0.25}, + {"value": 204800, "prob": 0.20}, + {"value": 512000, "prob": 0.15}, + {"value": 716800, "prob": 0.05}, + {"value": 122880, "prob": 0.05} + ], + "high_end": [ + {"value": 512000, "prob": 0.25}, + {"value": 716800, "prob": 0.25}, + {"value": 409600, "prob": 0.20}, + {"value": 1024000, "prob": 0.15}, + {"value": 307200, "prob": 0.10}, + {"value": 1536000, "prob": 0.05} + ], + "workstation": [ + {"value": 1024000, "prob": 0.25}, + {"value": 1536000, "prob": 0.20}, + {"value": 716800, "prob": 0.20}, + {"value": 2048000, "prob": 0.15}, + {"value": 512000, "prob": 0.10}, + {"value": 3072000, "prob": 0.05}, + {"value": 409600, "prob": 0.05} + ] + } +} diff --git a/src/stealthfox/_fpforge/data/cpt_storage_given_class_tier.json b/src/stealthfox/_fpforge/data/cpt_storage_given_class_tier.json new file mode 100644 index 0000000..acf834e --- /dev/null +++ b/src/stealthfox/_fpforge/data/cpt_storage_given_class_tier.json @@ -0,0 +1,305 @@ +{ + "_meta": "storage_quota_mb given (gpu_class, intra_tier)", + "table": { + "[\"integrated_old\", \"budget\"]": [ + { + "value": 32000, + "prob": 0.3 + }, + { + "value": 64000, + "prob": 0.4 + }, + { + "value": 128000, + "prob": 0.25 + }, + { + "value": 256000, + "prob": 0.05 + } + ], + "[\"integrated_old\", \"standard\"]": [ + { + "value": 64000, + "prob": 0.2 + }, + { + "value": 128000, + "prob": 0.45 + }, + { + "value": 256000, + "prob": 0.25 + }, + { + "value": 500000, + "prob": 0.1 + } + ], + "[\"integrated_old\", \"premium\"]": [ + { + "value": 128000, + "prob": 0.2 + }, + { + "value": 256000, + "prob": 0.45 + }, + { + "value": 500000, + "prob": 0.3 + }, + { + "value": 1000000, + "prob": 0.05 + } + ], + "[\"integrated_modern\", \"budget\"]": [ + { + "value": 64000, + "prob": 0.2 + }, + { + "value": 128000, + "prob": 0.3 + }, + { + "value": 256000, + "prob": 0.3 + }, + { + "value": 500000, + "prob": 0.2 + } + ], + "[\"integrated_modern\", \"standard\"]": [ + { + "value": 256000, + "prob": 0.25 + }, + { + "value": 500000, + "prob": 0.45 + }, + { + "value": 1000000, + "prob": 0.25 + }, + { + "value": 2000000, + "prob": 0.05 + } + ], + "[\"integrated_modern\", \"premium\"]": [ + { + "value": 500000, + "prob": 0.25 + }, + { + "value": 1000000, + "prob": 0.5 + }, + { + "value": 2000000, + "prob": 0.2 + }, + { + "value": 4000000, + "prob": 0.05 + } + ], + "[\"low_end\", \"budget\"]": [ + { + "value": 128000, + "prob": 0.2 + }, + { + "value": 256000, + "prob": 0.5 + }, + { + "value": 500000, + "prob": 0.25 + }, + { + "value": 1000000, + "prob": 0.05 + } + ], + "[\"low_end\", \"standard\"]": [ + { + "value": 256000, + "prob": 0.2 + }, + { + "value": 500000, + "prob": 0.5 + }, + { + "value": 1000000, + "prob": 0.25 + }, + { + "value": 2000000, + "prob": 0.05 + } + ], + "[\"low_end\", \"premium\"]": [ + { + "value": 500000, + "prob": 0.2 + }, + { + "value": 1000000, + "prob": 0.5 + }, + { + "value": 2000000, + "prob": 0.25 + }, + { + "value": 4000000, + "prob": 0.05 + } + ], + "[\"mid_range\", \"budget\"]": [ + { + "value": 256000, + "prob": 0.15 + }, + { + "value": 500000, + "prob": 0.5 + }, + { + "value": 1000000, + "prob": 0.3 + }, + { + "value": 2000000, + "prob": 0.05 + } + ], + "[\"mid_range\", \"standard\"]": [ + { + "value": 500000, + "prob": 0.2 + }, + { + "value": 1000000, + "prob": 0.55 + }, + { + "value": 2000000, + "prob": 0.2 + }, + { + "value": 4000000, + "prob": 0.05 + } + ], + "[\"mid_range\", \"premium\"]": [ + { + "value": 1000000, + "prob": 0.4 + }, + { + "value": 2000000, + "prob": 0.45 + }, + { + "value": 4000000, + "prob": 0.15 + } + ], + "[\"high_end\", \"budget\"]": [ + { + "value": 500000, + "prob": 0.15 + }, + { + "value": 1000000, + "prob": 0.5 + }, + { + "value": 2000000, + "prob": 0.3 + }, + { + "value": 4000000, + "prob": 0.05 + } + ], + "[\"high_end\", \"standard\"]": [ + { + "value": 1000000, + "prob": 0.3 + }, + { + "value": 2000000, + "prob": 0.5 + }, + { + "value": 4000000, + "prob": 0.2 + } + ], + "[\"high_end\", \"premium\"]": [ + { + "value": 2000000, + "prob": 0.4 + }, + { + "value": 4000000, + "prob": 0.45 + }, + { + "value": 8000000, + "prob": 0.15 + } + ], + "[\"workstation\", \"budget\"]": [ + { + "value": 1000000, + "prob": 0.3 + }, + { + "value": 2000000, + "prob": 0.5 + }, + { + "value": 4000000, + "prob": 0.2 + } + ], + "[\"workstation\", \"standard\"]": [ + { + "value": 2000000, + "prob": 0.3 + }, + { + "value": 4000000, + "prob": 0.5 + }, + { + "value": 8000000, + "prob": 0.2 + } + ], + "[\"workstation\", \"premium\"]": [ + { + "value": 4000000, + "prob": 0.35 + }, + { + "value": 8000000, + "prob": 0.45 + }, + { + "value": 16000000, + "prob": 0.2 + } + ] + } +} \ No newline at end of file diff --git a/src/stealthfox/_fpforge/data/ff_win_distributions.json b/src/stealthfox/_fpforge/data/ff_win_distributions.json new file mode 100644 index 0000000..becf2f5 --- /dev/null +++ b/src/stealthfox/_fpforge/data/ff_win_distributions.json @@ -0,0 +1,650 @@ +{ + "_source": "browserforge fingerprint-network.zip, FF/Windows slice, 3 userAgent leaves merged", + "_ff_win_uas": [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0" + ], + "_note": "Per-node weighted distributions. Marginal over the 3 Firefox/Windows UAs present in browserforge data.", + "distributions": { + "appCodeName": [ + [ + "Mozilla", + 1.0 + ] + ], + "appVersion": [ + [ + "5.0 (Windows)", + 1.0 + ] + ], + "doNotTrack": [ + [ + "1", + 1.0 + ] + ], + "extraProperties": [ + [ + { + "vendorFlavors": [], + "globalPrivacyControl": true, + "pdfViewerEnabled": true, + "installedApps": [] + }, + 0.6111111111111112 + ], + [ + { + "vendorFlavors": [], + "globalPrivacyControl": null, + "pdfViewerEnabled": null, + "installedApps": [] + }, + 0.3888888888888889 + ] + ], + "maxTouchPoints": [ + [ + 0, + 0.5777777777777778 + ], + [ + 1, + 0.3666666666666667 + ], + [ + 10, + 0.05555555555555555 + ] + ], + "oscpu": [ + [ + "Windows NT 10.0; Win64; x64", + 1.0 + ] + ], + "webdriver": [ + [ + false, + 1.0 + ] + ], + "productSub": [ + [ + "20100101", + 1.0 + ] + ], + "hardwareConcurrency": [ + [ + 12, + 0.3333333333333333 + ], + [ + 16, + 0.2777777777777778 + ], + [ + 8, + 0.13333333333333333 + ], + [ + 24, + 0.13333333333333333 + ], + [ + 32, + 0.06666666666666667 + ], + [ + 6, + 0.05555555555555555 + ] + ], + "platform": [ + [ + "Win32", + 1.0 + ] + ], + "screen": [ + [ + { + "availTop": 0, + "availLeft": 0, + "pageXOffset": 0, + "pageYOffset": 0, + "screenX": 772, + "hasHDR": false, + "width": 1536, + "height": 960, + "availWidth": 1536, + "availHeight": 960, + "clientWidth": 0, + "clientHeight": 21, + "innerWidth": 0, + "innerHeight": 0, + "outerWidth": 751, + "outerHeight": 886, + "colorDepth": 24, + "pixelDepth": 24, + "devicePixelRatio": 1.25 + }, + 0.3333333333333333 + ], + [ + { + "availTop": 0, + "availLeft": 2176, + "pageXOffset": 0, + "pageYOffset": 0, + "screenX": 2169, + "hasHDR": false, + "width": 2176, + "height": 1224, + "availWidth": 2176, + "availHeight": 1176, + "clientWidth": 0, + "clientHeight": 19, + "innerWidth": 0, + "innerHeight": 0, + "outerWidth": 2190, + "outerHeight": 1190, + "colorDepth": 24, + "pixelDepth": 24, + "devicePixelRatio": 1.7647058823529411 + }, + 0.13333333333333333 + ], + [ + { + "availTop": 0, + "availLeft": 0, + "pageXOffset": 0, + "pageYOffset": 0, + "screenX": 318, + "hasHDR": false, + "width": 2560, + "height": 1440, + "availWidth": 2560, + "availHeight": 1440, + "clientWidth": 0, + "clientHeight": 18, + "innerWidth": 0, + "innerHeight": 0, + "outerWidth": 2121, + "outerHeight": 1191, + "colorDepth": 24, + "pixelDepth": 24, + "devicePixelRatio": 1 + }, + 0.1111111111111111 + ], + [ + { + "availTop": 0, + "availLeft": 0, + "pageXOffset": 0, + "pageYOffset": 0, + "screenX": 652, + "hasHDR": false, + "width": 2752, + "height": 1152, + "availWidth": 2752, + "availHeight": 1104, + "clientWidth": 0, + "clientHeight": 19, + "innerWidth": 0, + "innerHeight": 0, + "outerWidth": 1806, + "outerHeight": 1104, + "colorDepth": 24, + "pixelDepth": 24, + "devicePixelRatio": 1.25 + }, + 0.06666666666666667 + ], + [ + { + "availTop": 0, + "availLeft": 0, + "pageXOffset": 0, + "pageYOffset": 0, + "screenX": 1912, + "hasHDR": false, + "width": 1920, + "height": 1080, + "availWidth": 1920, + "availHeight": 1032, + "clientWidth": 0, + "clientHeight": 18, + "innerWidth": 0, + "innerHeight": 0, + "outerWidth": 1936, + "outerHeight": 1048, + "colorDepth": 24, + "pixelDepth": 24, + "devicePixelRatio": 1 + }, + 0.06666666666666667 + ], + [ + { + "availTop": 29, + "availLeft": 21, + "pageXOffset": 0, + "pageYOffset": 0, + "screenX": 15, + "hasHDR": false, + "width": 1376, + "height": 774, + "availWidth": 1355, + "availHeight": 745, + "clientWidth": 0, + "clientHeight": 21, + "innerWidth": 0, + "innerHeight": 0, + "outerWidth": 1352, + "outerHeight": 749, + "colorDepth": 24, + "pixelDepth": 24, + "devicePixelRatio": 1 + }, + 0.05555555555555555 + ], + [ + { + "availTop": 0, + "availLeft": 0, + "pageXOffset": 0, + "pageYOffset": 0, + "screenX": 58, + "hasHDR": false, + "width": 1707, + "height": 960, + "availWidth": 1707, + "availHeight": 928, + "clientWidth": 0, + "clientHeight": 19, + "innerWidth": 0, + "innerHeight": 0, + "outerWidth": 1564, + "outerHeight": 793, + "colorDepth": 24, + "pixelDepth": 24, + "devicePixelRatio": 1 + }, + 0.05555555555555555 + ], + [ + { + "availTop": 0, + "availLeft": 0, + "pageXOffset": 0, + "pageYOffset": 0, + "screenX": 34, + "hasHDR": false, + "width": 5120, + "height": 1440, + "availWidth": 5120, + "availHeight": 1392, + "clientWidth": 0, + "clientHeight": 18, + "innerWidth": 0, + "innerHeight": 0, + "outerWidth": 2462, + "outerHeight": 1399, + "colorDepth": 24, + "pixelDepth": 24, + "devicePixelRatio": 1 + }, + 0.05555555555555555 + ], + [ + { + "availTop": 0, + "availLeft": 1920, + "pageXOffset": 0, + "pageYOffset": 0, + "screenX": 1912, + "hasHDR": false, + "width": 1920, + "height": 1080, + "availWidth": 1920, + "availHeight": 1032, + "clientWidth": 0, + "clientHeight": 18, + "innerWidth": 0, + "innerHeight": 0, + "outerWidth": 1936, + "outerHeight": 1048, + "colorDepth": 24, + "pixelDepth": 24, + "devicePixelRatio": 1 + }, + 0.05555555555555555 + ], + [ + { + "availTop": 0, + "availLeft": 0, + "pageXOffset": 0, + "pageYOffset": 0, + "screenX": 95, + "hasHDR": false, + "width": 1366, + "height": 768, + "availWidth": 1366, + "availHeight": 768, + "clientWidth": 0, + "clientHeight": 18, + "innerWidth": 0, + "innerHeight": 0, + "outerWidth": 1294, + "outerHeight": 1280, + "colorDepth": 24, + "pixelDepth": 24, + "devicePixelRatio": 1 + }, + 0.03333333333333333 + ], + [ + { + "availTop": 0, + "availLeft": 0, + "pageXOffset": 0, + "pageYOffset": 0, + "screenX": 1273, + "hasHDR": false, + "width": 2560, + "height": 1440, + "availWidth": 2560, + "availHeight": 1400, + "clientWidth": 0, + "clientHeight": 18, + "innerWidth": 0, + "innerHeight": 0, + "outerWidth": 1294, + "outerHeight": 1407, + "colorDepth": 24, + "pixelDepth": 24, + "devicePixelRatio": 1 + }, + 0.03333333333333333 + ] + ], + "pluginsData": [ + [ + { + "plugins": [ + { + "name": "PDF Viewer", + "description": "Portable Document Format", + "filename": "internal-pdf-viewer", + "mimeTypes": [ + { + "type": "application/pdf", + "suffixes": "pdf", + "description": "Portable Document Format", + "enabledPlugin": "PDF Viewer" + }, + { + "type": "text/pdf", + "suffixes": "pdf", + "description": "Portable Document Format", + "enabledPlugin": "PDF Viewer" + } + ] + }, + { + "name": "Chrome PDF Viewer", + "description": "Portable Document Format", + "filename": "internal-pdf-viewer", + "mimeTypes": [ + { + "type": "application/pdf", + "suffixes": "pdf", + "description": "Portable Document Format", + "enabledPlugin": "PDF Viewer" + }, + { + "type": "text/pdf", + "suffixes": "pdf", + "description": "Portable Document Format", + "enabledPlugin": "PDF Viewer" + } + ] + }, + { + "name": "Chromium PDF Viewer", + "description": "Portable Document Format", + "filename": "internal-pdf-viewer", + "mimeTypes": [ + { + "type": "application/pdf", + "suffixes": "pdf", + "description": "Portable Document Format", + "enabledPlugin": "PDF Viewer" + }, + { + "type": "text/pdf", + "suffixes": "pdf", + "description": "Portable Document Format", + "enabledPlugin": "PDF Viewer" + } + ] + }, + { + "name": "Microsoft Edge PDF Viewer", + "description": "Portable Document Format", + "filename": "internal-pdf-viewer", + "mimeTypes": [ + { + "type": "application/pdf", + "suffixes": "pdf", + "description": "Portable Document Format", + "enabledPlugin": "PDF Viewer" + }, + { + "type": "text/pdf", + "suffixes": "pdf", + "description": "Portable Document Format", + "enabledPlugin": "PDF Viewer" + } + ] + }, + { + "name": "WebKit built-in PDF", + "description": "Portable Document Format", + "filename": "internal-pdf-viewer", + "mimeTypes": [ + { + "type": "application/pdf", + "suffixes": "pdf", + "description": "Portable Document Format", + "enabledPlugin": "PDF Viewer" + }, + { + "type": "text/pdf", + "suffixes": "pdf", + "description": "Portable Document Format", + "enabledPlugin": "PDF Viewer" + } + ] + } + ], + "mimeTypes": [ + "Portable Document Format~~application/pdf~~pdf", + "Portable Document Format~~text/pdf~~pdf" + ] + }, + 0.6111111111111112 + ], + [ + { + "plugins": [], + "mimeTypes": [] + }, + 0.3888888888888889 + ] + ], + "audioCodecs": [ + [ + { + "ogg": "probably", + "mp3": "maybe", + "wav": "probably", + "m4a": "maybe", + "aac": "maybe" + }, + 1.0 + ] + ], + "videoCodecs": [ + [ + { + "ogg": "", + "h264": "probably", + "webm": "probably" + }, + 1.0 + ] + ], + "videoCard": [ + [ + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar", + "vendor": "Google Inc. (NVIDIA)" + }, + 0.8444444444444444 + ], + [ + { + "renderer": "ANGLE (AMD, Radeon R9 200 Series Direct3D11 vs_5_0 ps_5_0), or similar", + "vendor": "Google Inc. (AMD)" + }, + 0.08888888888888889 + ], + [ + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0), or similar", + "vendor": "Google Inc. (Intel)" + }, + 0.06666666666666667 + ] + ], + "multimediaDevices": [ + [ + { + "speakers": [], + "micros": [], + "webcams": [] + }, + 1.0 + ] + ], + "fonts": [ + [ + [], + 0.3888888888888889 + ], + [ + [ + "Calibri", + "HELV", + "MS UI Gothic", + "Marlett", + "Segoe UI Light", + "Small Fonts" + ], + 0.3222222222222222 + ], + [ + [ + "Agency FB", + "Calibri", + "Century", + "Century Gothic", + "Franklin Gothic", + "HELV", + "Haettenschweiler", + "Leelawadee", + "Lucida Bright", + "Lucida Sans", + "MS Outlook", + "MS Reference Specialty", + "MS UI Gothic", + "MT Extra", + "Marlett", + "Microsoft Uighur", + "Monotype Corsiva", + "Pristina", + "Segoe UI Light", + "Small Fonts" + ], + 0.13333333333333333 + ], + [ + [ + "AvantGarde Bk BT", + "Calibri", + "Clarendon", + "Franklin Gothic", + "Futura Bk BT", + "Futura Md BT", + "GOTHAM", + "HELV", + "Humanst521 BT", + "MS UI Gothic", + "MYRIAD PRO", + "Marlett", + "Minion Pro", + "Segoe UI Light", + "Small Fonts", + "TRAJAN PRO" + ], + 0.06666666666666667 + ], + [ + [ + "Calibri", + "MS UI Gothic", + "Marlett", + "Segoe UI Light" + ], + 0.05555555555555555 + ], + [ + [ + "Agency FB", + "Calibri", + "Century", + "Century Gothic", + "Franklin Gothic", + "Futura Bk BT", + "Futura Md BT", + "GOTHAM", + "HELV", + "Haettenschweiler", + "Humanst521 BT", + "Lucida Bright", + "Lucida Sans", + "MS Outlook", + "MS Reference Specialty", + "MS UI Gothic", + "MT Extra", + "MYRIAD PRO", + "Marlett", + "Minion Pro", + "Monotype Corsiva", + "Pristina", + "Segoe UI Light", + "Small Fonts" + ], + 0.03333333333333333 + ] + ] + } +} \ No newline at end of file diff --git a/src/stealthfox/_fpforge/data/font_pool.json b/src/stealthfox/_fpforge/data/font_pool.json new file mode 100644 index 0000000..f36e370 --- /dev/null +++ b/src/stealthfox/_fpforge/data/font_pool.json @@ -0,0 +1,670 @@ +{ + "core": [ + { + "name": "arial", + "factor": 0.978 + }, + { + "name": "arial black", + "factor": 1.168 + }, + { + "name": "arial narrow", + "factor": 0.854 + }, + { + "name": "bahnschrift", + "factor": 0.951 + }, + { + "name": "bahnschrift condensed", + "factor": 1.179 + }, + { + "name": "bahnschrift light", + "factor": 0.908 + }, + { + "name": "bahnschrift light condensed", + "factor": 1.053 + }, + { + "name": "bahnschrift light semicondensed", + "factor": 1.063 + }, + { + "name": "bahnschrift semibold", + "factor": 0.926 + }, + { + "name": "bahnschrift semibold condensed", + "factor": 1.131 + }, + { + "name": "bahnschrift semibold semicondensed", + "factor": 1.043 + }, + { + "name": "bahnschrift semicondensed", + "factor": 1.098 + }, + { + "name": "bahnschrift semilight", + "factor": 0.873 + }, + { + "name": "bahnschrift semilight condensed", + "factor": 0.905 + }, + { + "name": "bahnschrift semilight semicondensed", + "factor": 1.135 + }, + { + "name": "calibri light", + "factor": 0.901 + }, + { + "name": "cambria", + "factor": 1.063 + }, + { + "name": "cambria math", + "factor": 1.041 + }, + { + "name": "candara", + "factor": 0.927 + }, + { + "name": "candara light", + "factor": 0.892 + }, + { + "name": "cascadia code", + "factor": 1.139 + }, + { + "name": "cascadia mono", + "factor": 0.95 + }, + { + "name": "comic sans ms", + "factor": 1.087 + }, + { + "name": "consolas", + "factor": 1.158 + }, + { + "name": "constantia", + "factor": 1.052 + }, + { + "name": "corbel", + "factor": 0.895 + }, + { + "name": "corbel light", + "factor": 0.876 + }, + { + "name": "courier new", + "factor": 1.187 + }, + { + "name": "ebrima", + "factor": 0.962 + }, + { + "name": "franklin gothic medium", + "factor": 1.031 + }, + { + "name": "gabriola", + "factor": 1.077 + }, + { + "name": "georgia", + "factor": 1.094 + }, + { + "name": "hololens mdl2 assets", + "factor": 1.145 + }, + { + "name": "impact", + "factor": 0.862 + }, + { + "name": "ink free", + "factor": 1.048 + }, + { + "name": "leelawadee ui semilight", + "factor": 1.061 + }, + { + "name": "lucida console", + "factor": 1.131 + }, + { + "name": "lucida sans unicode", + "factor": 1.014 + }, + { + "name": "malgun gothic", + "factor": 1.128 + }, + { + "name": "malgun gothic semilight", + "factor": 1.051 + }, + { + "name": "marlett", + "factor": 0.855 + }, + { + "name": "microsoft himalaya", + "factor": 1.022 + }, + { + "name": "microsoft jhenghei", + "factor": 1.061 + }, + { + "name": "microsoft jhenghei light", + "factor": 0.94 + }, + { + "name": "microsoft jhenghei ui", + "factor": 1.136 + }, + { + "name": "microsoft jhenghei ui light", + "factor": 0.826 + }, + { + "name": "microsoft new tai lue", + "factor": 0.953 + }, + { + "name": "microsoft phagspa", + "factor": 0.988 + }, + { + "name": "microsoft sans serif", + "factor": 0.941 + }, + { + "name": "microsoft tai le", + "factor": 0.957 + }, + { + "name": "microsoft yahei", + "factor": 1.076 + }, + { + "name": "microsoft yahei light", + "factor": 1.043 + }, + { + "name": "microsoft yahei ui", + "factor": 1.076 + }, + { + "name": "microsoft yahei ui light", + "factor": 1.094 + }, + { + "name": "microsoft yi baiti", + "factor": 1.008 + }, + { + "name": "mingliu-extb", + "factor": 1.173 + }, + { + "name": "mingliu_hkscs-extb", + "factor": 1.101 + }, + { + "name": "mongolian baiti", + "factor": 1.056 + }, + { + "name": "ms gothic", + "factor": 1.119 + }, + { + "name": "ms pgothic", + "factor": 1.074 + }, + { + "name": "mv boli", + "factor": 0.932 + }, + { + "name": "nirmala ui", + "factor": 0.916 + }, + { + "name": "nirmala ui semilight", + "factor": 1.15 + }, + { + "name": "nsimsun", + "factor": 1.119 + }, + { + "name": "palatino linotype", + "factor": 1.067 + }, + { + "name": "pmingliu-extb", + "factor": 1.16 + }, + { + "name": "sans serif collection", + "factor": 0.854 + }, + { + "name": "segoe mdl2 assets", + "factor": 0.858 + }, + { + "name": "segoe print", + "factor": 1.033 + }, + { + "name": "segoe script", + "factor": 1.058 + }, + { + "name": "segoe ui", + "factor": 0.948 + }, + { + "name": "segoe ui black", + "factor": 1.148 + }, + { + "name": "segoe ui emoji", + "factor": 0.924 + }, + { + "name": "segoe ui historic", + "factor": 0.969 + }, + { + "name": "segoe ui semibold", + "factor": 0.965 + }, + { + "name": "segoe ui semilight", + "factor": 1.055 + }, + { + "name": "segoe ui symbol", + "factor": 0.972 + }, + { + "name": "segoe ui variable", + "factor": 0.907 + }, + { + "name": "simsun", + "factor": 1.113 + }, + { + "name": "simsun-extb", + "factor": 1.095 + }, + { + "name": "sitka banner", + "factor": 1.078 + }, + { + "name": "sitka display", + "factor": 0.838 + }, + { + "name": "sitka heading", + "factor": 0.843 + }, + { + "name": "sitka small", + "factor": 1.131 + }, + { + "name": "sitka subheading", + "factor": 0.942 + }, + { + "name": "sitka text", + "factor": 1.044 + }, + { + "name": "sylfaen", + "factor": 0.971 + }, + { + "name": "symbol", + "factor": 0.866 + }, + { + "name": "tahoma", + "factor": 0.962 + }, + { + "name": "times new roman", + "factor": 0.926 + }, + { + "name": "trebuchet ms", + "factor": 0.912 + }, + { + "name": "verdana", + "factor": 1.128 + }, + { + "name": "webdings", + "factor": 0.849 + }, + { + "name": "wingdings", + "factor": 0.864 + }, + { + "name": "wingdings 2", + "factor": 1.093 + }, + { + "name": "wingdings 3", + "factor": 0.861 + }, + { + "name": "yu gothic", + "factor": 1.069 + }, + { + "name": "yu gothic light", + "factor": 1.038 + }, + { + "name": "yu gothic medium", + "factor": 1.161 + }, + { + "name": "yu gothic ui", + "factor": 0.886 + }, + { + "name": "yu gothic ui light", + "factor": 1.093 + }, + { + "name": "yu gothic ui semibold", + "factor": 0.942 + }, + { + "name": "yu gothic ui semilight", + "factor": 0.923 + } + ], + "optional": [ + { + "name": "aparajita", + "factor": 0.94 + }, + { + "name": "arabic typesetting", + "factor": 1.093 + }, + { + "name": "arial unicode ms", + "factor": 1.047 + }, + { + "name": "batang", + "factor": 0.911 + }, + { + "name": "calibri", + "factor": 0.934 + }, + { + "name": "century", + "factor": 1.078 + }, + { + "name": "century gothic", + "factor": 0.886 + }, + { + "name": "dengxian", + "factor": 0.846 + }, + { + "name": "dfkai-sb", + "factor": 0.946 + }, + { + "name": "dokchampa", + "factor": 1.162 + }, + { + "name": "estrangelo edessa", + "factor": 1.095 + }, + { + "name": "euphemia", + "factor": 1.117 + }, + { + "name": "fangsong", + "factor": 0.942 + }, + { + "name": "franklin gothic", + "factor": 0.942 + }, + { + "name": "gadugi", + "factor": 0.945 + }, + { + "name": "gautami", + "factor": 1.134 + }, + { + "name": "haettenschweiler", + "factor": 0.874 + }, + { + "name": "helv", + "factor": 0.923 + }, + { + "name": "iskoola pota", + "factor": 0.857 + }, + { + "name": "javanese text", + "factor": 1.083 + }, + { + "name": "kaiti", + "factor": 1.145 + }, + { + "name": "kalinga", + "factor": 0.953 + }, + { + "name": "kartika", + "factor": 0.882 + }, + { + "name": "khmer ui", + "factor": 1.137 + }, + { + "name": "kokila", + "factor": 0.919 + }, + { + "name": "lao ui", + "factor": 0.904 + }, + { + "name": "latha", + "factor": 0.839 + }, + { + "name": "leelawadee", + "factor": 0.982 + }, + { + "name": "leelawadee ui", + "factor": 0.992 + }, + { + "name": "levenim mt", + "factor": 1.054 + }, + { + "name": "mangal", + "factor": 1.154 + }, + { + "name": "meiryo", + "factor": 1.165 + }, + { + "name": "meiryo ui", + "factor": 1.081 + }, + { + "name": "microsoft uighur", + "factor": 0.969 + }, + { + "name": "monotype corsiva", + "factor": 0.939 + }, + { + "name": "ms mincho", + "factor": 1.112 + }, + { + "name": "ms outlook", + "factor": 0.921 + }, + { + "name": "ms pmincho", + "factor": 0.925 + }, + { + "name": "ms reference sans serif", + "factor": 0.858 + }, + { + "name": "ms reference specialty", + "factor": 0.958 + }, + { + "name": "ms ui gothic", + "factor": 1.097 + }, + { + "name": "mt extra", + "factor": 0.905 + }, + { + "name": "myanmar text", + "factor": 0.961 + }, + { + "name": "nyala", + "factor": 1.108 + }, + { + "name": "plantagenet cherokee", + "factor": 1.115 + }, + { + "name": "pmingliu", + "factor": 1.125 + }, + { + "name": "pristina", + "factor": 1.023 + }, + { + "name": "raavi", + "factor": 0.826 + }, + { + "name": "segoe fluent icons", + "factor": 1.049 + }, + { + "name": "segoe ui light", + "factor": 0.918 + }, + { + "name": "shonar bangla", + "factor": 1.141 + }, + { + "name": "shruti", + "factor": 1.172 + }, + { + "name": "simhei", + "factor": 1.141 + }, + { + "name": "simkai", + "factor": 1.1 + }, + { + "name": "small fonts", + "factor": 0.849 + }, + { + "name": "traditional arabic", + "factor": 0.893 + }, + { + "name": "tunga", + "factor": 0.825 + }, + { + "name": "urdu typesetting", + "factor": 0.869 + }, + { + "name": "utsaah", + "factor": 1.144 + }, + { + "name": "vani", + "factor": 1.078 + }, + { + "name": "vijaya", + "factor": 0.917 + }, + { + "name": "vrinda", + "factor": 0.985 + }, + { + "name": "yu mincho", + "factor": 0.867 + } + ] +} \ No newline at end of file diff --git a/src/stealthfox/_fpforge/data/prior_audio.json b/src/stealthfox/_fpforge/data/prior_audio.json new file mode 100644 index 0000000..866cedc --- /dev/null +++ b/src/stealthfox/_fpforge/data/prior_audio.json @@ -0,0 +1,17 @@ +{ + "_meta": { + "name": "audio (joint: sample_rate, output_latency_ms, max_channel_count)", + "parents": [], + "child": "audio", + "source": "Curated from common Windows WASAPI/DirectSound configs on FF desktop" + }, + "table": [ + {"value": {"rate": 44100, "latency": 40, "channels": 2}, "prob": 0.20}, + {"value": {"rate": 48000, "latency": 30, "channels": 2}, "prob": 0.25}, + {"value": {"rate": 48000, "latency": 20, "channels": 2}, "prob": 0.15}, + {"value": {"rate": 48000, "latency": 40, "channels": 6}, "prob": 0.08}, + {"value": {"rate": 48000, "latency": 60, "channels": 2}, "prob": 0.12}, + {"value": {"rate": 44100, "latency": 50, "channels": 2}, "prob": 0.10}, + {"value": {"rate": 48000, "latency": 25, "channels": 2}, "prob": 0.10} + ] +} diff --git a/src/stealthfox/_fpforge/data/priors_independent.json b/src/stealthfox/_fpforge/data/priors_independent.json new file mode 100644 index 0000000..0b2bbc0 --- /dev/null +++ b/src/stealthfox/_fpforge/data/priors_independent.json @@ -0,0 +1,14 @@ +{ + "_meta": { + "name": "Independent marginal priors (no parents)", + "source": "StatCounter GlobalStats + Firefox defaults + community data 2025-2026", + "note": "Codec prefs (av1_enabled, webm_encoder_enabled, hw_video_decoding, wmf_enabled, ffvpx_enabled) moved to cpt_codec_given_class.json — they correlate with GPU class via Firefox version / user-tier distribution." + }, + "dark_theme": { + "_note": "0=light, 1=dark. StatCounter ~40% users report dark theme preference on desktop.", + "table": [ + {"value": 0, "prob": 0.60}, + {"value": 1, "prob": 0.40} + ] + } +} diff --git a/src/stealthfox/_fpforge/data/webgl_renderer_pool.json b/src/stealthfox/_fpforge/data/webgl_renderer_pool.json new file mode 100644 index 0000000..b1b05a2 --- /dev/null +++ b/src/stealthfox/_fpforge/data/webgl_renderer_pool.json @@ -0,0 +1,1902 @@ +{ + "source": "browserforge v1.2.3 fingerprint-network.zip [stealth: NVIDIA collapsed to 3 sanitize buckets 2026-04-26]", + "total": 474, + "entries": [ + { + "renderer": "ANGLE (AMD, AMD Radeon (TM) 630 (0x00006987) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon (TM) 860M Graphics (0x00001114) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon (TM) Graphics (0x000015E7) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon (TM) Graphics (0x00001681) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon (TM) R5 M330 (0x00006660) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon (TM) R9 390 Series (0x000067B1) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon (TM) R9 Fury Series (0x00007300) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon (TM) RX 570 (0x000067DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon 610 Series (0x00006665) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon 740M Graphics (0x000015C8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon 740M Graphics (0x00001901) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon 760M Graphics (0x000015BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon 760M Graphics (0x00001900) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon 780M Graphics (0x000015BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon 780M Graphics (0x00001900) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon Graphics (0x000015BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon HD 5450 (0x000068F9) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon HD 7500M/7600M Series (0x00006841) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon HD 8600/8700M (0x00006600) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon HD 8600M Series (0x00006660) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon PRO Graphics (0x00001681) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon Pro W5500 (0x00007341) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon Pro W6800 (0x000073A3) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon R5 230 (0x00006779) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon R5 430 (0x00006611) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon R5 Graphics (0x00009874) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon R6 Graphics (0x00009874) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon R7 200 Series (0x00006610) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon R7 240 (0x00006837) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon R7 M440 (0x00006900) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon R7 M460 (0x00006900) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon R9 200 Series (0x00006810) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 550 (0x0000699F) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 5600 XT (0x0000731F) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 5700 (0x0000731F) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 5700 XT (0x0000731F) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 580 2048SP (0x00006FDF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 6400 (0x0000743F) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 6500 XT (0x0000743F) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 6600 (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 6600 XT (0x000073FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 6650 XT (0x000073EF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 6700 XT (0x000073DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 6750 XT (0x000073DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 6800 (0x000073BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 6800 XT (0x000073BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 6900 XT (0x000073BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 6950 XT (0x000073A5) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 7600 (0x00007480) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 7700 XT (0x0000747E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 7800 XT (0x0000747E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 7900 XT (0x0000744C) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 7900 XTX (0x0000744C) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 9060 XT (0x00007590) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 9070 (0x00007550) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX 9070 XT (0x00007550) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon RX590 GME (0x00006FDF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon TM Graphics (0x00001900) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 610M (0x00001506) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 610M (0x0000164E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 660M (0x00001681) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 680M (0x00001681) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 760M (0x000015BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 780M (0x000015BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 780M Graphics (0x00001900) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 8060S Graphics (0x00001586) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 840M Graphics (0x00001114) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 860M Graphics (0x00001114) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 880M Graphics (0x0000150E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) 890M Graphics (0x0000150E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics (0x000013C0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics (0x00001506) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics (0x000015BF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics (0x00001636) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics (0x00001638) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics (0x0000164C) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics (0x0000164E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics (0x00001681) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics (0x00001900) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Pro Graphics (0x00001638) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) R2 Graphics (0x00009853) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) R4 Graphics (0x00009851) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) R4 Graphics (0x000098E4) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) R5 Graphics (0x000098E4) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) R7 250 (0x00006610) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) R7 Graphics (0x0000130F) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) R7 Graphics (0x00001313) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) R7 Graphics (0x00009874) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) RX Vega 10 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) RX Vega 11 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) RX Vega 11 Graphics (0x000015DD) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Vega 10 Graphics (0x000015DD) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Vega 3 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Vega 3 Graphics (0x000015DD) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Vega 6 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Vega 6 Graphics (0x000015DD) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Vega 8 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Vega 8 Graphics (0x000015DD) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Vega 8 Mobile Graphics (0x000015DD) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD Radeon(TM) Vega 9 Graphics (0x000015D8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD RadeonT 660M (0x00001681) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, AMD RadeonT 680M (0x00001681) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, Radeon (TM) Pro WX 5100 Graphics (0x000067C7) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, Radeon (TM) RX 470 Series (0x000067DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, Radeon RX 550 Series (0x000067FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, Radeon RX 5500 XT (0x00007340) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, Radeon RX 560 Series (0x000067FF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, Radeon RX 560X (0x000067EF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, Radeon RX 570 Series (0x000067DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, Radeon RX 580 Series (0x000067DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, Radeon RX 590 Series (0x000067DF) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (AMD, Radeon RX550/550 Series (0x0000699F) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (AMD)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) 130T GPU (16GB) (0x00007D51) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) 130V GPU (8GB) (0x000064A0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) 140T GPU (16GB) (0x00007D51) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) 140T GPU (32GB) (0x00007D51) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) 140T GPU (8GB) (0x00007D51) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) 140V GPU (16GB) (0x000064A0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) 140V GPU (16GB) Direct3D9Ex vs_3_0 ps_3_0, igd9trinity64.dll)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) 140V GPU (8GB) (0x000064A0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) A380 Graphics (0x000056A5) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) Graphics (0x00007D55) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) Pro 140T GPU (16GB) (0x00007D51) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) Pro 140T GPU (32GB) (0x00007D51) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) Pro 140T GPU (48GB) (0x00007D51) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Arc(TM) Pro Graphics (0x00007D55) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) G41 Express Chipset (Microsoft Corporation - WDDM 1.1) Direct3D9Ex vs_3_0 ps_3_0, igdumd64.dll)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Graphics (0x000046D4) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Graphics (0x00007D41) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Graphics (0x00007D45) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Graphics (0x00007D67) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Graphics (0x00007DD5) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Graphics (0x0000A7AC) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Graphics (0x0000A7AD) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics (0x00000102) Direct3D11 vs_4_1 ps_4_1, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics (0x00000152) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics (0x00000402) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics (0x00000F31) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics (0x00001606) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics (0x000022B1) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 3000 (0x00000116) Direct3D11 vs_4_1 ps_4_1, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 3000 (0x00000126) Direct3D11 vs_4_1 ps_4_1, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 3000 Direct3D9Ex vs_3_0 ps_3_0, igdumd64.dll)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 4000 (0x00000166) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 4000 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 4400 (0x0000041E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 4400 (0x00000A16) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 4400 Direct3D9Ex vs_3_0 ps_3_0, igdumdim64.dll)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 4400 manual-gen9_2015-134121 (0x0000041E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 4600 (0x00000412) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 4600 (0x00000416) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 4600 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 5000 (0x00000A26) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 505 (0x00005A84) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 510 (0x00001902) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 510 (0x00001906) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 515 (0x0000191E) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 520 (0x00001916) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 520 (0x00001921) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 520 (0x00001921) Direct3D11 vs_5_0 ps_5_0, D3D11-24.20.100.6293)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 530 (0x00001912) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 530 (0x0000191B) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 5500 (0x00001616) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 5500 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 610 (0x00005906) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 620 (0x00005916) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 620 (0x00005917) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 620 (0x00005921) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 630 (0x00005912) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics 630 (0x0000591B) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11-10.18.10.4358)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D9Ex vs_3_0 ps_3_0, igdumd64.dll)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics Direct3D9Ex vs_3_0 ps_3_0, igdumdx32.dll)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics Family (0x00000A16) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics Family Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) HD Graphics P530 (0x0000191D) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Iris(R) Plus Graphics (0x00008A52) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Iris(R) Plus Graphics (0x00008A5A) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Iris(R) Plus Graphics 640 (0x00005926) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x000046A6) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x000046A8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x000046AA) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x00009A40) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x00009A49) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x0000A7A0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics (0x0000A7A1) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) Q45/Q43 Express Chipset (Microsoft Corporation - WDDM 1.1) Direct3D9Ex vs_3_0 ps_3_0, igdumd64.dll)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00004626) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00004628) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00004688) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x0000468B) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x000046A3) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x000046B3) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x000046D0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x000046D1) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x000046D2) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00004E55) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00004E71) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00008A56) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00009A60) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00009A68) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00009A78) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00009B21) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00009B41) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00009BA4) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00009BC4) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00009BCA) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x00009BCC) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x0000A720) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x0000A721) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x0000A788) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x0000A78B) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x0000A7A8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics (0x0000A7A9) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 600 (0x00003185) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 605 (0x00003184) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 610 (0x00003E90) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 610 (0x00003EA1) Direct3D11 vs_5_0 ps_5_0, D3D11-27.20.100.9466)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 615 (0x0000591C) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 620 (0x00003EA0) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 620 (0x00005917) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E91) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E92) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E98) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00009BC5) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00009BC8) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 730 (0x00004682) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 730 (0x00004692) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 730 (0x00004C8B) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 750 (0x00004C8A) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 770 (0x00004680) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 770 (0x00004690) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel, Intel(R) UHD Graphics 770 (0x0000A780) Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (Intel)" + }, + { + "renderer": "ANGLE (Intel(R) HD Graphics Family Direct3D11 vs_5_0 ps_5_0)", + "vendor": "Intel Inc." + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 480 Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + }, + { + "renderer": "ANGLE (NVIDIA, NVIDIA GeForce 8800 GTX Direct3D11 vs_5_0 ps_5_0, D3D11)", + "vendor": "Google Inc. (NVIDIA)" + } + ] +} \ No newline at end of file diff --git a/src/stealthfox/_fpforge/profile.py b/src/stealthfox/_fpforge/profile.py new file mode 100644 index 0000000..1815c83 --- /dev/null +++ b/src/stealthfox/_fpforge/profile.py @@ -0,0 +1,259 @@ +"""Public dataclass surface for fpforge.""" +from __future__ import annotations + +from dataclasses import dataclass, field, replace as _dc_replace +from typing import Any, Dict, List, Optional + +from ._sampler import sample as _sample_raw + + +@dataclass(frozen=True) +class GPUProfile: + vendor: str + renderer: str + class_tier: str # "low_end" | "mid_range" | "high_end" | "integrated_old" | "integrated_modern" + + +@dataclass(frozen=True) +class ScreenProfile: + width: int + height: int + avail_width: int + avail_height: int + dpr: float + tier: str + + +@dataclass(frozen=True) +class HardwareProfile: + concurrency: int + storage_quota_mb: int + + +@dataclass(frozen=True) +class AudioProfile: + sample_rate: int + output_latency_ms: int + max_channel_count: int + + +@dataclass(frozen=True) +class CodecProfile: + av1_enabled: bool + webm_encoder_enabled: bool + mediasource_webm: bool + mediasource_mp4: bool + webspeech_synth: bool + + +@dataclass(frozen=True) +class WebGLProfile: + msaa_samples: int + + +# ────────────────────────────────────────────────────────────────────── +# Pin map: flat dotted-path -> value. Set via `pin=` on generate_profile. +# +# Supported keys: +# "gpu.vendor", "gpu.renderer", "gpu.class_tier" +# "screen.width", "screen.height", "screen.avail_width", +# "screen.avail_height", "screen.dpr", "screen.tier" +# "hardware.concurrency", "hardware.storage_quota_mb" +# "audio.sample_rate", "audio.output_latency_ms", +# "audio.max_channel_count" +# "codec.av1_enabled", "codec.webm_encoder_enabled", +# "codec.mediasource_webm", "codec.mediasource_mp4", +# "codec.webspeech_synth" +# "webgl.msaa_samples" +# "fonts" (replaces the whole list) +# "dark_theme" +# ────────────────────────────────────────────────────────────────────── + +_PIN_GROUPS = { + "gpu": {"vendor", "renderer", "class_tier"}, + "screen": {"width", "height", "avail_width", "avail_height", "dpr", "tier"}, + "hardware": {"concurrency", "storage_quota_mb"}, + "audio": {"sample_rate", "output_latency_ms", "max_channel_count"}, + "codec": { + "av1_enabled", "webm_encoder_enabled", + "mediasource_webm", "mediasource_mp4", "webspeech_synth", + }, + "webgl": {"msaa_samples"}, +} +_PIN_TOP = {"fonts", "dark_theme"} + + +def _validate_pin_key(key: str) -> None: + if key in _PIN_TOP: + return + if "." not in key: + raise ValueError( + f"pin key {key!r} is not valid. " + f"Use 'group.field' (e.g. 'screen.width') or one of {sorted(_PIN_TOP)}." + ) + group, field_name = key.split(".", 1) + if group not in _PIN_GROUPS: + raise ValueError( + f"pin key {key!r}: unknown group {group!r}. " + f"Known groups: {sorted(_PIN_GROUPS)}." + ) + if field_name not in _PIN_GROUPS[group]: + raise ValueError( + f"pin key {key!r}: unknown field {field_name!r} in group {group!r}. " + f"Known fields: {sorted(_PIN_GROUPS[group])}." + ) + + +@dataclass(frozen=True) +class Profile: + """Coherent browser fingerprint profile sampled from a single integer seed. + + Use `generate_profile(seed)` to build one. Pin specific values at build + time with `generate_profile(seed, pin={"screen.width": 2560, ...})`. + """ + seed: int + gpu: GPUProfile + screen: ScreenProfile + hardware: HardwareProfile + audio: AudioProfile + codec: CodecProfile + webgl: WebGLProfile + fonts: List[str] + dark_theme: bool + _raw: Dict[str, Any] = field(default_factory=dict, repr=False, compare=False) + + def to_prefs_dict(self) -> Dict[str, Any]: + """Return the flat dict of raw sampler fields, as produced by the + underlying Bayesian sampler. Stable across releases for a given seed.""" + return dict(self._raw) + + +# Mapping from flat pin key -> raw sampler dict key, so `to_prefs_dict()` +# and `stealthfox.prefs.translate_profile_to_prefs` observe the pinned value. +_PIN_TO_RAW = { + "gpu.vendor": "webgl_vendor", + "gpu.renderer": "webgl_renderer", + "gpu.class_tier": "gpu_class", + "screen.width": "screen_w", + "screen.height": "screen_h", + "screen.avail_width": "screen_avail_w", + "screen.avail_height": "screen_avail_h", + "screen.dpr": "dpr", + "screen.tier": "screen_tier", + "hardware.concurrency": "hw_concurrency", + "hardware.storage_quota_mb": "storage_quota_mb", + "audio.sample_rate": "audio_sample_rate", + "audio.output_latency_ms": "audio_output_latency_ms", + "audio.max_channel_count": "audio_max_channel_count", + "codec.av1_enabled": "av1_enabled", + "codec.webm_encoder_enabled": "webm_encoder_enabled", + "codec.mediasource_webm": "mediasource_webm", + "codec.mediasource_mp4": "mediasource_mp4", + "codec.webspeech_synth": "webspeech_synth", + "webgl.msaa_samples": "msaa_samples", + "dark_theme": "dark_theme", + # "fonts" is a list — handled specially (joined into font_whitelist). +} + + +def _apply_pins_to_raw(raw: Dict[str, Any], pin: Dict[str, Any]) -> Dict[str, Any]: + """Return a copy of `raw` with the pinned sampler-level fields updated.""" + out = dict(raw) + for key, value in pin.items(): + if key == "fonts": + if not isinstance(value, (list, tuple)): + raise TypeError("pin 'fonts' must be a list/tuple of strings") + out["font_whitelist"] = ",".join(value) + continue + raw_key = _PIN_TO_RAW.get(key) + if raw_key is None: + # Shouldn't happen after validation, but guard anyway. + continue + out[raw_key] = value + return out + + +def generate_profile(seed: int, pin: Optional[Dict[str, Any]] = None) -> Profile: + """Return a deterministic Profile for the given integer seed. + + pin: optional dict of dotted-path keys (e.g. "screen.width", "gpu.renderer") + to values that are FORCED in the resulting profile. All other fields + are still sampled from the Bayesian network based on `seed`, so the + same seed + same pin map always yields the same profile. + + Example — force a specific GPU and screen while letting everything + else vary with the seed (via the public stealthfox API): + + from stealthfox import Stealthfox + + with Stealthfox( + seed=42, + pin={ + "gpu.renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 Direct3D11)", + "gpu.vendor": "Google Inc. (NVIDIA)", + "gpu.class_tier": "high_end", + "screen.width": 2560, + "screen.height": 1440, + }, + ) as browser: + ... + + Warning: pinning breaks Bayesian coherence across the pinned fields + (if you pin a high-end GPU but leave screen unpinned, you may get a + 1080p screen that would be unusual for that GPU class). Pin related + fields together when coherence matters. + + Supported keys: see the module-level _PIN_GROUPS / _PIN_TOP tables + or run `help(generate_profile)` after import. + """ + if pin: + for key in pin: + _validate_pin_key(key) + + raw = _sample_raw(int(seed)) + if pin: + raw = _apply_pins_to_raw(raw, pin) + + # Font whitelist is stored as a comma-separated string in raw; split it. + font_wl = raw.get("font_whitelist", "") + if isinstance(font_wl, str): + fonts = [f.strip() for f in font_wl.split(",") if f.strip()] + else: + fonts = list(font_wl) if font_wl else [] + + return Profile( + seed=int(raw["stealth_seed"]), + gpu=GPUProfile( + vendor=raw["webgl_vendor"], + renderer=raw["webgl_renderer"], + class_tier=raw["gpu_class"], + ), + screen=ScreenProfile( + width=int(raw["screen_w"]), + height=int(raw["screen_h"]), + avail_width=int(raw["screen_avail_w"]), + avail_height=int(raw["screen_avail_h"]), + dpr=float(raw["dpr"]), + tier=str(raw.get("screen_tier", "")), + ), + hardware=HardwareProfile( + concurrency=int(raw["hw_concurrency"]), + storage_quota_mb=int(raw["storage_quota_mb"]), + ), + audio=AudioProfile( + sample_rate=int(raw["audio_sample_rate"]), + output_latency_ms=int(raw["audio_output_latency_ms"]), + max_channel_count=int(raw["audio_max_channel_count"]), + ), + codec=CodecProfile( + av1_enabled=bool(raw["av1_enabled"]), + webm_encoder_enabled=bool(raw["webm_encoder_enabled"]), + mediasource_webm=bool(raw["mediasource_webm"]), + mediasource_mp4=bool(raw["mediasource_mp4"]), + webspeech_synth=bool(raw["webspeech_synth"]), + ), + webgl=WebGLProfile(msaa_samples=int(raw["msaa_samples"])), + fonts=fonts, + dark_theme=bool(raw["dark_theme"]), + _raw=raw, + ) diff --git a/src/stealthfox/_headless.py b/src/stealthfox/_headless.py new file mode 100644 index 0000000..b92ec79 --- /dev/null +++ b/src/stealthfox/_headless.py @@ -0,0 +1,228 @@ +"""Invisible-but-headed browser windows. + +Playwright's ``headless=True`` flips Firefox onto a different code path — +no widget tree, software-only rendering, distinct timing — and anti-bot +systems can spot the divergence. Running the browser *headed* on a +virtual display gives us the real rendering pipeline while keeping the +windows off the user's screen. + +Linux: spawns its own ``Xvfb`` instance, points ``DISPLAY`` at it. +Windows: creates a hidden desktop via ``CreateDesktop`` and binds the +calling thread to it, so Playwright's child processes inherit it. +""" +from __future__ import annotations + +import os +import secrets +import subprocess +import sys +import time +from typing import Optional + + +# Inherited from WSLg / GNOME / etc. these env vars make Firefox prefer a +# Wayland compositor over the X11 DISPLAY we set, so the window leaks onto +# the real desktop. Strip them all before starting. +_WAYLAND_LEAK_VARS = ( + "WAYLAND_DISPLAY", + "XDG_RUNTIME_DIR", + "XDG_SESSION_TYPE", + "PULSE_SERVER", + "WSL2_GUI_APPS_ENABLED", +) + + +class _LinuxVirtualDisplay: + """Standalone Xvfb instance owned by this Stealthfox session.""" + + def __init__(self, width: int = 1920, height: int = 1080) -> None: + self._geometry = f"{width}x{height}x24" + self._proc: Optional[subprocess.Popen] = None + self._display: Optional[str] = None + self._saved_env: dict[str, Optional[str]] = {} + + def start(self) -> None: + if not _binary_on_path("Xvfb"): + raise RuntimeError( + "stealthfox headless=True requires Xvfb. " + "Install it: sudo apt install xvfb" + ) + # Retry: when many workers start in parallel they can pick the same + # display number before any has created its lockfile. Xvfb on the + # losing side exits immediately — try again with a fresh number. + last_err: Optional[Exception] = None + for _ in range(10): + display = self._pick_display() + try: + self._spawn(display) + self._wait_until_ready(display) + self._display = display + self._apply_env(display) + return + except RuntimeError as e: + last_err = e + if self._proc is not None and self._proc.poll() is None: + self._proc.kill() + self._proc = None + raise RuntimeError(f"Xvfb failed to start after 10 attempts: {last_err}") + + def _spawn(self, display: str) -> None: + self._proc = subprocess.Popen( + [ + "Xvfb", display, + "-screen", "0", self._geometry, + "+extension", "GLX", + "+extension", "RENDER", + "-nolisten", "unix", + "-listen", "tcp", + "-ac", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + ) + + def _pick_display(self) -> str: + for n in range(99, 400): + if not os.path.exists(f"/tmp/.X{n}-lock"): + return f":{n}" + raise RuntimeError("no free X display number in :99–:399") + + def _wait_until_ready(self, display: str) -> None: + # We start Xvfb with -nolisten unix → no /tmp/.X11-unix socket appears. + # Xvfb creates /tmp/.X{n}-lock immediately though — wait for that. + lockfile = f"/tmp/.X{display[1:]}-lock" + deadline = time.monotonic() + 3.0 + assert self._proc is not None + while time.monotonic() < deadline: + if self._proc.poll() is not None: + raise RuntimeError(f"Xvfb {display} exited immediately") + if os.path.exists(lockfile): + return + time.sleep(0.02) + raise RuntimeError(f"Xvfb {display} did not become ready in 3s") + + def _apply_env(self, display: str) -> None: + keys = ("DISPLAY", "MOZ_ENABLE_WAYLAND", "GDK_BACKEND") + _WAYLAND_LEAK_VARS + for k in keys: + self._saved_env[k] = os.environ.get(k) + for k in _WAYLAND_LEAK_VARS: + os.environ.pop(k, None) + os.environ["DISPLAY"] = display + os.environ["MOZ_ENABLE_WAYLAND"] = "0" + os.environ["GDK_BACKEND"] = "x11" + + def stop(self) -> None: + for k, v in self._saved_env.items(): + if v is None: + os.environ.pop(k, None) + else: + os.environ[k] = v + self._saved_env.clear() + + if self._proc is not None and self._proc.poll() is None: + self._proc.terminate() + try: + self._proc.wait(timeout=3) + except subprocess.TimeoutExpired: + self._proc.kill() + self._proc.wait(timeout=2) + self._proc = None + self._display = None + + +class _WindowsVirtualDesktop: + """A hidden Windows desktop the calling thread is bound to. + + Playwright's child processes (node driver → firefox.exe) inherit the + desktop because their ``STARTUPINFO.lpDesktop`` is NULL — Windows uses + the calling thread's desktop in that case. + + pywin32 ships ``CreateDesktop`` in ``win32service`` but doesn't expose + ``SetThreadDesktop`` / ``GetThreadDesktop`` as module functions. We + call them directly via ctypes against ``user32.dll``. + """ + + def __init__(self) -> None: + self._desktop = None # PyHDESK from win32service.CreateDesktop + self._original_handle = 0 # raw HDESK int of the previous desktop + + def start(self) -> None: + try: + import win32con # type: ignore + import win32service # type: ignore + except ImportError as e: + raise RuntimeError( + "stealthfox headless=True on Windows requires pywin32. " + "Install it: pip install pywin32" + ) from e + + import ctypes + from ctypes import wintypes + user32 = ctypes.windll.user32 + kernel32 = ctypes.windll.kernel32 + + # Save the current desktop handle so we can restore it on stop(). + get_thread_desktop = user32.GetThreadDesktop + get_thread_desktop.argtypes = [wintypes.DWORD] + get_thread_desktop.restype = wintypes.HANDLE + self._original_handle = get_thread_desktop(kernel32.GetCurrentThreadId()) + + name = f"sf_{secrets.token_hex(4)}" + self._desktop = win32service.CreateDesktop( + name, 0, win32con.GENERIC_ALL, None + ) + + # Bind the calling thread to the new desktop. Children spawned + # afterwards (Playwright driver → firefox.exe) inherit it because + # their STARTUPINFO.lpDesktop is NULL. + set_thread_desktop = user32.SetThreadDesktop + set_thread_desktop.argtypes = [wintypes.HANDLE] + set_thread_desktop.restype = wintypes.BOOL + if not set_thread_desktop(int(self._desktop)): + err = ctypes.get_last_error() + raise RuntimeError( + f"SetThreadDesktop failed (GetLastError={err}). " + "The thread cannot have any windows or hooks; close them first." + ) + + def stop(self) -> None: + import ctypes + from ctypes import wintypes + user32 = ctypes.windll.user32 + + if self._original_handle: + try: + set_thread_desktop = user32.SetThreadDesktop + set_thread_desktop.argtypes = [wintypes.HANDLE] + set_thread_desktop.restype = wintypes.BOOL + set_thread_desktop(self._original_handle) + except Exception: + pass + self._original_handle = 0 + + if self._desktop is not None: + try: + self._desktop.CloseDesktop() + except Exception: + pass + self._desktop = None + + +def make_virtual_display(): + """Return a started/stoppable virtual-display object for this platform. + + Stealthfox supports Windows x86_64 and Linux x86_64 only. + """ + if sys.platform == "win32": + return _WindowsVirtualDesktop() + if sys.platform.startswith("linux"): + return _LinuxVirtualDisplay() + raise RuntimeError( + f"stealthfox supports Windows and Linux only (got {sys.platform!r})" + ) + + +def _binary_on_path(name: str) -> bool: + import shutil + return shutil.which(name) is not None diff --git a/src/stealthfox/_proxy.py b/src/stealthfox/_proxy.py new file mode 100644 index 0000000..85a4e77 --- /dev/null +++ b/src/stealthfox/_proxy.py @@ -0,0 +1,56 @@ +"""Proxy translation shared by sync and async launchers. + +SOCKS proxies are driven entirely by the patched Firefox prefs (the +``nsProtocolProxyService`` patch reads ``network.proxy.socks_username`` +and ``socks_password``). HTTP/HTTPS proxies go through Playwright's own +``proxy=`` kwarg so it can negotiate Basic auth. +""" +from __future__ import annotations + +from typing import Any, Dict, Optional + + +_SOCKS_SCHEMES = ("socks5://", "socks4://", "socks://") + + +def configure_proxy( + proxy: Optional[Dict[str, str]], + prefs: Dict[str, Any], +) -> Optional[Dict[str, str]]: + """Mutate ``prefs`` for SOCKS auth; return what to pass to Playwright. + + * ``None`` proxy → returns ``None``. + * SOCKS proxy → writes the auth prefs and returns ``None`` (Playwright + gets nothing; Firefox does the rest). + * HTTP / HTTPS proxy → returns the dict unchanged for Playwright. + """ + if not proxy: + return None + + server = (proxy.get("server") or "").strip() + if not server or server.lower() == "direct://": + return None + if not _is_socks_scheme(server): + return proxy + + host_port = _strip_scheme(server) + if ":" not in host_port: + return None # malformed, drop silently + + host, port_str = host_port.rsplit(":", 1) + prefs["network.proxy.type"] = 1 + prefs["network.proxy.socks"] = host + prefs["network.proxy.socks_port"] = int(port_str) + prefs["network.proxy.socks_version"] = 4 if server.lower().startswith("socks4://") else 5 + prefs["network.proxy.socks_username"] = proxy.get("username") or "" + prefs["network.proxy.socks_password"] = proxy.get("password") or "" + prefs["network.proxy.socks_remote_dns"] = True + return None + + +def _is_socks_scheme(server: str) -> bool: + return server.lower().startswith(_SOCKS_SCHEMES) + + +def _strip_scheme(server: str) -> str: + return server.split("://", 1)[1] if "://" in server else server diff --git a/src/stealthfox/async_api.py b/src/stealthfox/async_api.py new file mode 100644 index 0000000..8a275a9 --- /dev/null +++ b/src/stealthfox/async_api.py @@ -0,0 +1,178 @@ +"""Async Playwright façade — mirrors sync_api but with async/await.""" +from __future__ import annotations + +import asyncio +import secrets +from typing import Any, Dict, Optional, Union + +from playwright.async_api import Browser, Playwright, async_playwright + +from ._fpforge import Profile, generate_profile +from ._headless import make_virtual_display +from ._proxy import configure_proxy as _configure_proxy_shared +from .download import ensure_binary +from .launcher import _CHROME_H, _CHROME_W, _TASKBAR_H, _tz_env +from .prefs import translate_profile_to_prefs + + +def _patch_new_page_sleep(ctx: Any) -> None: + """Wrap ctx.new_page() to add a brief settle after tab creation. + + FF150 with Fission emits an about:newtab navigation ~100ms after a tab + is created. If goto() is called immediately, it races with that internal + navigation and raises "Navigation interrupted by about:newtab". A short + sleep breaks the race without requiring every call-site to know about it. + """ + original_new_page = ctx.new_page + + async def patched_new_page(**kw): + page = await original_new_page(**kw) + await asyncio.sleep(0.4) + return page + + ctx.new_page = patched_new_page # type: ignore[assignment] + + +class Stealthfox: + """Async context manager — see stealthfox.Stealthfox for the sync variant.""" + + def __init__( + self, + seed: Optional[int] = None, + *, + pin: Optional[Dict[str, Any]] = None, + headless: bool = False, + proxy: Optional[Dict[str, str]] = None, + extra_args: Optional[list[str]] = None, + humanize: Union[bool, float] = True, + locale: str = "en-US", + timezone: str = "", + extra_prefs: Optional[Dict[str, Any]] = None, + binary_path: Optional[str] = None, + ) -> None: + # See sync launcher: `zoom.stealth.fpp.hw_seed` is int32_t — clamp. + self.seed: int = int(seed) if seed is not None else secrets.randbits(31) + self._pin = pin + self._headless = headless + self._proxy = proxy + self._extra_args = list(extra_args or []) + self._humanize = humanize + self._locale = locale + self._timezone = timezone + self._extra_prefs = extra_prefs + self._binary_path = binary_path + self._profile: Profile = generate_profile(self.seed, pin=self._pin) + self._pw: Optional[Playwright] = None + self._browser: Optional[Browser] = None + self._virtual_display: Any = None + + async def __aenter__(self) -> Browser: + import sys as _sys + executable = self._binary_path or ensure_binary() + prefs = translate_profile_to_prefs( + self._profile, + locale=self._locale, + timezone=self._timezone, + extra_prefs=self._extra_prefs, + virtual_display=bool(self._headless and _sys.platform == "win32"), + ) + prefs["stealthfox.humanize"] = bool(self._humanize) + if self._humanize: + cap = 1.5 if self._humanize is True else float(self._humanize) + prefs["stealthfox.humanize.maxTime"] = str(cap) + playwright_proxy = _configure_proxy_shared(self._proxy, prefs) + pw_headless = self._resolve_headless() + env = self._build_env() + try: + self._pw = await async_playwright().start() + self._browser = await self._pw.firefox.launch( + executable_path=str(executable), + headless=pw_headless, + firefox_user_prefs=prefs, + proxy=playwright_proxy, + args=self._extra_args, + env=env, + ) + except BaseException: + await self._teardown() + raise + self._patch_new_context_defaults(self._browser) + return self._browser + + def _patch_new_context_defaults(self, browser: Browser) -> None: + original = browser.new_context + defaults = self._default_context_kwargs() + + async def patched(**kw): + merged = dict(defaults) + merged.update(kw) + ctx = await original(**merged) + _patch_new_page_sleep(ctx) + return ctx + + browser.new_context = patched # type: ignore[assignment] + + def _default_context_kwargs(self) -> Dict[str, Any]: + p = self._profile + kwargs: Dict[str, Any] = { + "viewport": {"width": p.screen.width - _CHROME_W, + "height": p.screen.height - _TASKBAR_H - _CHROME_H}, + "screen": {"width": p.screen.width, "height": p.screen.height}, + "device_scale_factor": p.screen.dpr, + "color_scheme": "dark" if p.dark_theme else "light", + } + # Pass timezone via Playwright per-realm override (works for every + # IANA name, including no-DST zones that Windows ICU silently drops + # on the global pref path). + if self._timezone: + kwargs["timezone_id"] = self._timezone + if self._locale: + kwargs["locale"] = self._locale + return kwargs + + async def __aexit__(self, *exc: Any) -> None: + await self._teardown() + + async def _teardown(self) -> None: + if self._browser is not None: + try: + await self._browser.close() + except Exception: + pass + self._browser = None + if self._pw is not None: + try: + await self._pw.stop() + except Exception: + pass + self._pw = None + if self._virtual_display is not None: + try: + self._virtual_display.stop() + except Exception: + pass + self._virtual_display = None + + def _build_env(self) -> Dict[str, str]: + import os as _os + env = _os.environ.copy() + if self._timezone: + env["TZ"] = _tz_env(self._timezone) + # Propagate STEALTHFOX_WEBRTC_PUBLIC_IP if the process set it — read + # by nICEr's nr_stealth_bridge to inject a synthetic srflx candidate + # matching the proxy egress IP. This avoids the StaticPref IPC + # propagation timing issue between parent and socket processes. + if _os.environ.get("STEALTHFOX_WEBRTC_PUBLIC_IP"): + env["STEALTHFOX_WEBRTC_PUBLIC_IP"] = _os.environ["STEALTHFOX_WEBRTC_PUBLIC_IP"] + return env + + def _resolve_headless(self) -> bool: + if not self._headless: + return False + vd = make_virtual_display() + vd.start() + self._virtual_display = vd + return False + + +__all__ = ["Stealthfox"] diff --git a/src/stealthfox/cli.py b/src/stealthfox/cli.py new file mode 100644 index 0000000..3842125 --- /dev/null +++ b/src/stealthfox/cli.py @@ -0,0 +1,68 @@ +"""Command-line interface for stealthfox.""" +from __future__ import annotations + +import argparse +import shutil +import sys + +from . import __version__ +from .constants import BINARY_VERSION, FIREFOX_UPSTREAM_VERSION +from .download import cache_root, ensure_binary + + +def _cmd_fetch(_args: argparse.Namespace) -> int: + path = ensure_binary() + print(path) + return 0 + + +def _cmd_path(_args: argparse.Namespace) -> int: + try: + path = ensure_binary() + except Exception as e: + print(f"error: {e}", file=sys.stderr) + return 1 + print(path) + return 0 + + +def _cmd_version(_args: argparse.Namespace) -> int: + print(f"stealthfox {__version__}") + print(f"BINARY_VERSION={BINARY_VERSION} (Firefox {FIREFOX_UPSTREAM_VERSION})") + return 0 + + +def _cmd_clear_cache(_args: argparse.Namespace) -> int: + root = cache_root() + if root.exists(): + shutil.rmtree(root) + print(f"removed: {root}") + else: + print(f"nothing to remove: {root}") + return 0 + + +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(prog="stealthfox", description="stealthfox CLI") + sub = p.add_subparsers(dest="cmd", required=True) + + sub.add_parser("fetch", help="download the patched Firefox binary") + sub.add_parser("path", help="print the absolute path to the cached binary") + sub.add_parser("version", help="print wrapper and binary versions") + sub.add_parser("clear-cache", help="remove all cached binaries") + return p + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + dispatch = { + "fetch": _cmd_fetch, + "path": _cmd_path, + "version": _cmd_version, + "clear-cache": _cmd_clear_cache, + } + return dispatch[args.cmd](args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/stealthfox/constants.py b/src/stealthfox/constants.py new file mode 100644 index 0000000..4ad672c --- /dev/null +++ b/src/stealthfox/constants.py @@ -0,0 +1,48 @@ +"""Compile-time constants that pin the wrapper to a specific Firefox build. + +BINARY_VERSION is bumped every time new Firefox patches are released. It is +deliberately decoupled from the Python package version so that pure-Python +bugfixes don't force a multi-hour Firefox rebuild. +""" +from __future__ import annotations + +# Bump this when a new patched Firefox build is released on GitHub. +BINARY_VERSION: str = "firefox-1" + +# Underlying Firefox version (for display only; does not drive downloads). +FIREFOX_UPSTREAM_VERSION: str = "150.0.1" + +# The base filename prefix used inside archives. +BINARY_BASENAME: str = f"firefox-{FIREFOX_UPSTREAM_VERSION}-stealth" + + +def ARCHIVE_NAME(platform_key: str, machine: str) -> str: + """Return the platform-specific archive filename. + + platform_key: sys.platform ("win32", "linux") + machine: platform.machine() ("AMD64", "x86_64", ...) + """ + pk = platform_key.lower() + m = machine.lower() + if m in {"amd64", "x86_64"}: + arch = "x86_64" + else: + raise NotImplementedError(f"unsupported arch: {machine}") + + if pk == "win32": + return f"{BINARY_BASENAME}-win-{arch}.zip" + if pk == "linux": + return f"{BINARY_BASENAME}-linux-{arch}.tar.gz" + raise NotImplementedError(f"unsupported platform: {platform_key}") + + +# Binary entry point relative path inside the extracted archive root. +BINARY_ENTRY_REL = { + "win32": "firefox.exe", + "linux": "firefox", +} + +# GitHub release URL template. The "TODO" owner is resolved at publication time. +RELEASE_URL_TEMPLATE = ( + "https://github.com/feder-cr/stealthfox/releases/download/{tag}/{asset}" +) diff --git a/src/stealthfox/data/font-map.json b/src/stealthfox/data/font-map.json new file mode 100644 index 0000000..9c46b95 --- /dev/null +++ b/src/stealthfox/data/font-map.json @@ -0,0 +1,1846 @@ +{ + "win11": { + "core": [ + "Arial", + "Bahnschrift", + "Calibri", + "Cambria", + "Cambria Math", + "Candara", + "Comic Sans MS", + "Consolas", + "Constantia", + "Corbel", + "Courier New", + "Ebrima", + "Franklin Gothic Medium", + "Gabriola", + "Gadugi", + "Georgia", + "Impact", + "Ink Free", + "Javanese Text", + "Leelawadee UI", + "MS UI Gothic", + "MV Boli", + "Marlett", + "Microsoft Himalaya", + "Microsoft JhengHei UI", + "Microsoft New Tai Lue", + "Microsoft PhagsPa", + "Microsoft Sans Serif", + "Microsoft Tai Le", + "Microsoft YaHei UI", + "Microsoft Yi Baiti", + "Mongolian Baiti", + "Myanmar Text", + "Palatino Linotype", + "Segoe Fluent Icons", + "Segoe MDL2 Assets", + "Segoe Print", + "Segoe Script", + "Segoe UI", + "Segoe UI Emoji", + "Segoe UI Historic", + "Segoe UI Symbol", + "Segoe UI Variable", + "SimSun-ExtB", + "Sitka", + "Sylfaen", + "Symbol", + "Tahoma", + "Times New Roman", + "Trebuchet MS", + "Verdana", + "Webdings", + "Wingdings", + "Yu Gothic UI", + "宋体", + "微軟正黑體", + "微软雅黑", + "新宋体", + "新細明體-ExtB", + "游ゴシック", + "細明體-ExtB", + "細明體_HKSCS-ExtB", + "細明體_MSCS-ExtB", + "맑은 고딕", + "MS ゴシック", + "MS Pゴシック" + ], + "am": [ + "Nyala" + ], + "am-ET": [ + "Nyala" + ], + "ar": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-AE": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-BH": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-DJ": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-DZ": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-EG": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-ER": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-IL": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-IQ": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-JO": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-KM": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-KW": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-LB": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-LY": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-MA": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-MR": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-OM": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-PS": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-QA": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-SA": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-SD": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-SO": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-SS": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-SY": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-TD": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-TN": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ar-YE": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "arc-Syrc": [ + "Estrangelo Edessa" + ], + "arz-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "as": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "as-IN": [ + "Shonar Bangla", + "Vrinda" + ], + "bh-Deva": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "bn": [ + "Shonar Bangla", + "Vrinda" + ], + "bn-BD": [ + "Shonar Bangla", + "Vrinda" + ], + "bn-IN": [ + "Shonar Bangla", + "Vrinda" + ], + "bpy-Beng": [ + "Shonar Bangla", + "Vrinda" + ], + "brx": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "brx-Deva": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "brx-IN": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "byn": [ + "Nyala" + ], + "byn-ER": [ + "Nyala" + ], + "byn-Ethi": [ + "Nyala" + ], + "chr-Cher": [ + "Plantagenet Cherokee" + ], + "chr-Cher-US": [ + "Plantagenet Cherokee" + ], + "ckb-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "cmn-Hans": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "cmn-Hant": [ + "DFKai-SB", + "新細明體", + "細明體", + "細明體_HKSCS", + "細明體_MSCS" + ], + "eu": [ + "Arial Nova", + "Georgia Pro", + "Gill Sans Nova", + "Neue Haas Grotesk Text Pro", + "Rockwell Nova", + "Verdana Pro" + ], + "fa": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "fa-AF": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "fa-IR": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "gan-Hans": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "glk-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "gu": [ + "Shruti" + ], + "gu-IN": [ + "Shruti" + ], + "ha-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "hak-Hans": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "hak-Hant": [ + "DFKai-SB", + "新細明體", + "細明體", + "細明體_HKSCS", + "細明體_MSCS" + ], + "he": [ + "Aharoni", + "David", + "FrankRuehl", + "Gisha", + "Levenim MT", + "Miriam", + "Miriam Fixed", + "Narkisim", + "Rod" + ], + "he-IL": [ + "Aharoni", + "David", + "FrankRuehl", + "Gisha", + "Levenim MT", + "Miriam", + "Miriam Fixed", + "Narkisim", + "Rod" + ], + "hi": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "hi-IN": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "iu-Cans": [ + "Euphemia" + ], + "iu-Cans-CA": [ + "Euphemia" + ], + "ja": [ + "BIZ UDPゴシック", + "BIZ UDP明朝", + "BIZ UDゴシック", + "BIZ UD明朝", + "Meiryo UI", + "UD デジタル 教科書体 N", + "UD デジタル 教科書体 NK", + "UD デジタル 教科書体 NP", + "メイリオ", + "游明朝", + "MS 明朝", + "MS P明朝" + ], + "ja-JP": [ + "BIZ UDPゴシック", + "BIZ UDP明朝", + "BIZ UDゴシック", + "BIZ UD明朝", + "Meiryo UI", + "UD デジタル 教科書体 N", + "UD デジタル 教科書体 NK", + "UD デジタル 教科書体 NP", + "メイリオ", + "游明朝", + "MS 明朝", + "MS P明朝" + ], + "km": [ + "DaunPenh", + "Khmer UI", + "MoolBoran" + ], + "km-KH": [ + "DaunPenh", + "Khmer UI", + "MoolBoran" + ], + "kn": [ + "Tunga" + ], + "kn-IN": [ + "Tunga" + ], + "ko": [ + "굴림", + "굴림체", + "궁서", + "궁서체", + "돋움", + "돋움체", + "바탕", + "바탕체" + ], + "ko-KR": [ + "굴림", + "굴림체", + "궁서", + "궁서체", + "돋움", + "돋움체", + "바탕", + "바탕체" + ], + "ks-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ks-Arab-IN": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ks-Deva": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "ku-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ku-Arab-IQ": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "lo": [ + "DokChampa", + "Lao UI" + ], + "lo-LA": [ + "DokChampa", + "Lao UI" + ], + "lzh-Hant": [ + "DFKai-SB", + "新細明體", + "細明體", + "細明體_HKSCS", + "細明體_MSCS" + ], + "mai": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "ml": [ + "Kartika" + ], + "ml-IN": [ + "Kartika" + ], + "mr": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "mr-IN": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "mzn-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ne": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "ne-IN": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "ne-NP": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "new-Deva": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "or": [ + "Kalinga" + ], + "or-IN": [ + "Kalinga" + ], + "pa": [ + "Raavi" + ], + "pa-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "pa-Arab-PK": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "pa-Guru": [ + "Raavi" + ], + "pa-IN": [ + "Raavi" + ], + "pi-Deva": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "pnb-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "prs": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "prs-AF": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "prs-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ps": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ps-AF": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "sa": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "sa-Deva": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "sa-IN": [ + "Aparajita", + "Kokila", + "Mangal", + "Sanskrit Text", + "Utsaah" + ], + "sd-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "sd-Arab-PK": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "si": [ + "Iskoola Pota" + ], + "si-LK": [ + "Iskoola Pota" + ], + "syr": [ + "Estrangelo Edessa" + ], + "syr-SY": [ + "Estrangelo Edessa" + ], + "syr-Syrc": [ + "Estrangelo Edessa" + ], + "ta": [ + "Latha", + "Vijaya" + ], + "ta-IN": [ + "Latha", + "Vijaya" + ], + "ta-LK": [ + "Latha", + "Vijaya" + ], + "ta-MY": [ + "Latha", + "Vijaya" + ], + "ta-SG": [ + "Latha", + "Vijaya" + ], + "te": [ + "Gautami", + "Vani" + ], + "te-IN": [ + "Gautami", + "Vani" + ], + "th": [ + "Angsana New", + "AngsanaUPC", + "Browallia New", + "BrowalliaUPC", + "Cordia New", + "CordiaUPC", + "DilleniaUPC", + "EucrosiaUPC", + "FreesiaUPC", + "IrisUPC", + "JasmineUPC", + "KodchiangUPC", + "Leelawadee", + "LilyUPC" + ], + "th-TH": [ + "Angsana New", + "AngsanaUPC", + "Browallia New", + "BrowalliaUPC", + "Cordia New", + "CordiaUPC", + "DilleniaUPC", + "EucrosiaUPC", + "FreesiaUPC", + "IrisUPC", + "JasmineUPC", + "KodchiangUPC", + "Leelawadee", + "LilyUPC" + ], + "ti": [ + "Nyala" + ], + "ti-ER": [ + "Nyala" + ], + "ti-ET": [ + "Nyala" + ], + "tig": [ + "Nyala" + ], + "tig-ER": [ + "Nyala" + ], + "tig-Ethi": [ + "Nyala" + ], + "tk-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ug": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ug-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ug-CN": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ur": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ur-IN": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ur-PK": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "uz-Arab": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "uz-Arab-AF": [ + "Aldhabi", + "Andalus", + "Arabic Typesetting", + "Microsoft Uighur", + "Sakkal Majalla", + "Simplified Arabic", + "Simplified Arabic Fixed", + "Traditional Arabic", + "Urdu Typesetting" + ], + "ve-Ethi": [ + "Nyala" + ], + "wal": [ + "Nyala" + ], + "wal-ET": [ + "Nyala" + ], + "wal-Ethi": [ + "Nyala" + ], + "wuu-Hans": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "yi": [ + "Aharoni", + "David", + "FrankRuehl", + "Gisha", + "Levenim MT", + "Miriam", + "Miriam Fixed", + "Narkisim", + "Rod" + ], + "yue-Hans": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "zh-CN": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "zh-HK": [ + "DFKai-SB", + "新細明體", + "細明體", + "細明體_HKSCS", + "細明體_MSCS" + ], + "zh-Hans": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "zh-Hant": [ + "DFKai-SB", + "新細明體", + "細明體", + "細明體_HKSCS", + "細明體_MSCS" + ], + "zh-MO": [ + "DFKai-SB", + "新細明體", + "細明體", + "細明體_HKSCS", + "細明體_MSCS" + ], + "zh-SG": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "zh-TW": [ + "DFKai-SB", + "新細明體", + "細明體", + "細明體_HKSCS", + "細明體_MSCS" + ], + "zh-gan-Hans": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "zh-hak-Hans": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "zh-hak-Hant": [ + "DFKai-SB", + "新細明體", + "細明體", + "細明體_HKSCS", + "細明體_MSCS" + ], + "zh-lzh-Hant": [ + "DFKai-SB", + "新細明體", + "細明體", + "細明體_HKSCS", + "細明體_MSCS" + ], + "zh-wuu-Hans": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "zh-yue-Hans": [ + "仿宋", + "楷体", + "等线", + "黑体" + ], + "zh-yue-Hant": [ + "DFKai-SB", + "新細明體", + "細明體", + "細明體_HKSCS", + "細明體_MSCS" + ] + }, + "ubuntu": { + "core": [ + "C059", + "D050000L", + "DejaVu Sans", + "DejaVu Sans Mono", + "DejaVu Serif", + "Droid Sans Fallback", + "FreeMono", + "FreeSans", + "FreeSerif", + "Liberation Mono", + "Liberation Sans", + "Liberation Sans Narrow", + "Liberation Serif", + "Nimbus Mono PS", + "Nimbus Roman", + "Nimbus Sans", + "Nimbus Sans Narrow", + "Noto Color Emoji", + "Noto Kufi Arabic", + "Noto Looped Lao", + "Noto Looped Thai", + "Noto Mono", + "Noto Music", + "Noto Naskh Arabic", + "Noto Nastaliq Urdu", + "Noto Rashi Hebrew", + "Noto Sans", + "Noto Sans Adlam", + "Noto Sans Adlam Unjoined", + "Noto Sans Anatolian Hieroglyphs", + "Noto Sans Arabic", + "Noto Sans Armenian", + "Noto Sans Avestan", + "Noto Sans Balinese", + "Noto Sans Bamum", + "Noto Sans Bassa Vah", + "Noto Sans Batak", + "Noto Sans Bengali", + "Noto Sans Bhaiksuki", + "Noto Sans Brahmi", + "Noto Sans Buginese", + "Noto Sans Buhid", + "Noto Sans CJK HK", + "Noto Sans CJK JP", + "Noto Sans CJK KR", + "Noto Sans CJK SC", + "Noto Sans CJK TC", + "Noto Sans Canadian Aboriginal", + "Noto Sans Carian", + "Noto Sans Caucasian Albanian", + "Noto Sans Chakma", + "Noto Sans Cham", + "Noto Sans Cherokee", + "Noto Sans Coptic", + "Noto Sans Cuneiform", + "Noto Sans Cypriot", + "Noto Sans Deseret", + "Noto Sans Devanagari", + "Noto Sans Display", + "Noto Sans Duployan", + "Noto Sans Egyptian Hieroglyphs", + "Noto Sans Elbasan", + "Noto Sans Elymaic", + "Noto Sans Ethiopic", + "Noto Sans Georgian", + "Noto Sans Glagolitic", + "Noto Sans Gothic", + "Noto Sans Grantha", + "Noto Sans Gujarati", + "Noto Sans Gunjala Gondi", + "Noto Sans Gurmukhi", + "Noto Sans Hanifi Rohingya", + "Noto Sans Hanunoo", + "Noto Sans Hatran", + "Noto Sans Hebrew", + "Noto Sans Imperial Aramaic", + "Noto Sans Indic Siyaq Numbers", + "Noto Sans Inscriptional Pahlavi", + "Noto Sans Inscriptional Parthian", + "Noto Sans Javanese", + "Noto Sans Kaithi", + "Noto Sans Kannada", + "Noto Sans Kayah Li", + "Noto Sans Kharoshthi", + "Noto Sans Khmer", + "Noto Sans Khojki", + "Noto Sans Khudawadi", + "Noto Sans Lao", + "Noto Sans Lepcha", + "Noto Sans Limbu", + "Noto Sans Linear A", + "Noto Sans Linear B", + "Noto Sans Lisu", + "Noto Sans Lycian", + "Noto Sans Lydian", + "Noto Sans Mahajani", + "Noto Sans Malayalam", + "Noto Sans Mandaic", + "Noto Sans Manichaean", + "Noto Sans Marchen", + "Noto Sans Masaram Gondi", + "Noto Sans Math", + "Noto Sans Mayan Numerals", + "Noto Sans Medefaidrin", + "Noto Sans Meetei Mayek", + "Noto Sans Mende Kikakui", + "Noto Sans Meroitic", + "Noto Sans Miao", + "Noto Sans Modi", + "Noto Sans Mongolian", + "Noto Sans Mono", + "Noto Sans Mono CJK HK", + "Noto Sans Mono CJK JP", + "Noto Sans Mono CJK KR", + "Noto Sans Mono CJK SC", + "Noto Sans Mono CJK TC", + "Noto Sans Mro", + "Noto Sans Multani", + "Noto Sans Myanmar", + "Noto Sans NKo", + "Noto Sans Nabataean", + "Noto Sans New Tai Lue", + "Noto Sans Newa", + "Noto Sans Nushu", + "Noto Sans Ogham", + "Noto Sans Ol Chiki", + "Noto Sans Old Hungarian", + "Noto Sans Old Italic", + "Noto Sans Old North Arabian", + "Noto Sans Old Permic", + "Noto Sans Old Persian", + "Noto Sans Old Sogdian", + "Noto Sans Old South Arabian", + "Noto Sans Old Turkic", + "Noto Sans Oriya", + "Noto Sans Osage", + "Noto Sans Osmanya", + "Noto Sans Pahawh Hmong", + "Noto Sans Palmyrene", + "Noto Sans Pau Cin Hau", + "Noto Sans PhagsPa", + "Noto Sans Phoenician", + "Noto Sans Psalter Pahlavi", + "Noto Sans Rejang", + "Noto Sans Runic", + "Noto Sans Samaritan", + "Noto Sans Saurashtra", + "Noto Sans Sharada", + "Noto Sans Shavian", + "Noto Sans Siddham", + "Noto Sans SignWriting", + "Noto Sans Sinhala", + "Noto Sans Sogdian", + "Noto Sans Sora Sompeng", + "Noto Sans Soyombo", + "Noto Sans Sundanese", + "Noto Sans Syloti Nagri", + "Noto Sans Symbols", + "Noto Sans Symbols2", + "Noto Sans Syriac", + "Noto Sans Tagalog", + "Noto Sans Tagbanwa", + "Noto Sans Tai Le", + "Noto Sans Tai Tham", + "Noto Sans Tai Viet", + "Noto Sans Takri", + "Noto Sans Tamil", + "Noto Sans Tamil Supplement", + "Noto Sans Telugu", + "Noto Sans Thaana", + "Noto Sans Thai", + "Noto Sans Tifinagh", + "Noto Sans Tifinagh APT", + "Noto Sans Tifinagh Adrar", + "Noto Sans Tifinagh Agraw Imazighen", + "Noto Sans Tifinagh Ahaggar", + "Noto Sans Tifinagh Air", + "Noto Sans Tifinagh Azawagh", + "Noto Sans Tifinagh Ghat", + "Noto Sans Tifinagh Hawad", + "Noto Sans Tifinagh Rhissa Ixa", + "Noto Sans Tifinagh SIL", + "Noto Sans Tifinagh Tawellemmet", + "Noto Sans Tirhuta", + "Noto Sans Ugaritic", + "Noto Sans Vai", + "Noto Sans Wancho", + "Noto Sans Warang Citi", + "Noto Sans Yi", + "Noto Sans Zanabazar Square", + "Noto Serif", + "Noto Serif Ahom", + "Noto Serif Armenian", + "Noto Serif Balinese", + "Noto Serif Bengali", + "Noto Serif CJK HK", + "Noto Serif CJK JP", + "Noto Serif CJK KR", + "Noto Serif CJK SC", + "Noto Serif CJK TC", + "Noto Serif Devanagari", + "Noto Serif Display", + "Noto Serif Dogra", + "Noto Serif Ethiopic", + "Noto Serif Georgian", + "Noto Serif Grantha", + "Noto Serif Gujarati", + "Noto Serif Gurmukhi", + "Noto Serif Hebrew", + "Noto Serif Hmong Nyiakeng", + "Noto Serif Kannada", + "Noto Serif Khmer", + "Noto Serif Khojki", + "Noto Serif Lao", + "Noto Serif Malayalam", + "Noto Serif Myanmar", + "Noto Serif Sinhala", + "Noto Serif Tamil", + "Noto Serif Tamil Slanted", + "Noto Serif Tangut", + "Noto Serif Telugu", + "Noto Serif Thai", + "Noto Serif Tibetan", + "Noto Serif Yezidi", + "Noto Traditional Nushu", + "OpenSymbol", + "P052", + "Standard Symbols PS", + "URW Bookman", + "URW Gothic", + "Ubuntu", + "Ubuntu Mono", + "Ubuntu Sans", + "Ubuntu Sans Mono", + "Z003" + ], + "am": [ + "Abyssinica SIL" + ], + "ar": [ + "KacstArt", + "KacstBook", + "KacstDecorative", + "KacstDigital", + "KacstFarsi", + "KacstLetter", + "KacstNaskh", + "KacstOffice", + "KacstOne", + "KacstPen", + "KacstPoster", + "KacstQurn", + "KacstScreen", + "KacstTitle", + "KacstTitleL", + "mry_KacstQurn" + ], + "as": [ + "Lohit Assamese" + ], + "bn": [ + "Jamrul", + "Likhan", + "Lohit Bengali", + "Mitra ", + "Mukti", + "অনি" + ], + "gu": [ + "Kalapi", + "Lohit Gujarati", + "Rasa", + "Rekha", + "Samyak Gujarati", + "Samyak Malayalam", + "Samyak Tamil", + "Yrsa", + "aakar", + "padmmaa" + ], + "hi": [ + "Annapurna SIL", + "Chandas", + "Lohit Devanagari", + "Nakula", + "Pagul", + "Sahadeva", + "Samanata", + "Samyak Devanagari", + "Sarai", + "गार्गी", + "नालिमाटी" + ], + "km": [ + "Khmer OS", + "Khmer OS Battambang", + "Khmer OS Bokor", + "Khmer OS Content", + "Khmer OS Fasthand", + "Khmer OS Freehand", + "Khmer OS Metal Chrieng", + "Khmer OS Muol", + "Khmer OS Muol Light", + "Khmer OS Muol Pali", + "Khmer OS Siemreap", + "Khmer OS System" + ], + "kn": [ + "Gubbi", + "Lohit Kannada", + "Navilu" + ], + "lo": [ + "Phetsarath OT" + ], + "ml": [ + "AnjaliOldLipi", + "Chilanka", + "Dyuthi", + "Gayathri", + "Karumbi", + "Keraleeyam", + "Lohit Malayalam", + "Manjari", + "Meera", + "Rachana", + "RaghuMalayalamSans", + "Samyak Gujarati", + "Samyak Malayalam", + "Samyak Tamil", + "Suruma", + "Uroob" + ], + "my": [ + "Padauk", + "Padauk Book" + ], + "ne": [ + "Annapurna SIL" + ], + "or": [ + "Lohit Odia", + "ori1Uni" + ], + "pa": [ + "Lohit Gurmukhi", + "Saab" + ], + "si": [ + "LKLUG" + ], + "ta": [ + "Lohit Tamil", + "Lohit Tamil Classical", + "Samyak Gujarati", + "Samyak Malayalam", + "Samyak Tamil" + ], + "te": [ + "Dhurjati", + "Gidugu", + "Gurajada", + "LakkiReddy", + "Lohit Telugu", + "Mallanna", + "Mandali", + "NATS", + "NTR", + "Peddana", + "Ponnala", + "Pothana2000", + "Potti Sreeramulu", + "Ramabhadra", + "Ramaraja", + "RaviPrakash", + "Sree Krushnadevaraya", + "Suranna", + "Suravaram", + "Syamala Ramana", + "TenaliRamakrishna", + "Timmana", + "Vemana2000" + ], + "th": [ + "Garuda", + "Kinnari", + "Laksaman", + "Loma", + "Norasi", + "Purisa", + "Sawasdee", + "Tlwg Mono", + "Tlwg Typewriter", + "Tlwg Typist", + "Tlwg Typo", + "Umpush", + "Waree" + ] + }, + "extras": { + "core": [ + "Abadi MT Condensed Light", + "Adobe Caslon Pro", + "Adobe Garamond Pro", + "Agency FB", + "Aharoni", + "Algerian", + "Angsana New", + "AngsanaUPC", + "Arial MT", + "Baskerville Old Face", + "Bauhaus 93", + "Bell MT", + "Berlin Sans FB", + "Bernard MT Condensed", + "Bitstream Charter", + "Bitstream Vera Sans Mono", + "Blackadder ITC", + "Bodoni MT", + "Bodoni MT Poster Compressed", + "Book Antiqua", + "Bookman Old Style", + "Bookshelf Symbol 7", + "Bradley Hand ITC", + "Britannic Bold", + "Broadway", + "Browallia New", + "BrowalliaUPC", + "Californian FB", + "Calisto MT", + "Castellar", + "Centaur", + "Century", + "Century Gothic", + "Century Schoolbook", + "Chiller", + "Cochin", + "Colonna MT", + "Cooper Black", + "Copperplate", + "Copperplate Gothic", + "Copperplate Gothic Bold", + "Copperplate Gothic Light", + "Cordia New", + "CordiaUPC", + "Courier", + "Curlz MT", + "David", + "Edwardian Script ITC", + "Elephant", + "Engravers MT", + "EngraversGothic BT", + "Eras Bold ITC", + "Eras Demi ITC", + "Eras Light ITC", + "Eras Medium ITC", + "EucrosiaUPC", + "Eurostile", + "Felix Titling", + "Footlight MT Light", + "Forte", + "Forum", + "Franklin Gothic", + "FreesiaUPC", + "Freestyle Script", + "French Script MT", + "Futura", + "Futura Condensed", + "Geo", + "Gigi", + "Gill Sans", + "Gill Sans MT", + "Gill Sans MT Condensed", + "Gill Sans MT Ext Condensed Bold", + "Gill Sans Ultra", + "Gill Sans Ultra Bold", + "Gill Sans Ultra Bold Condensed", + "Gloucester MT Extra Condensed", + "Goudy Old Style", + "Goudy Stout", + "Graduate", + "Haettenschweiler", + "Harlow Solid Italic", + "Harrington", + "Helvetica", + "Helvetica Light", + "High Tower Text", + "Hoefler Text", + "Imprint MT Shadow", + "Informal Roman", + "IrisUPC", + "Japan", + "JasmineUPC", + "Jokerman", + "Juice ITC", + "Kaufmann BT", + "KodchiangUPC", + "Kristen ITC", + "Kunstler Script", + "Levenim MT", + "LilyUPC", + "Lucida Bright", + "Lucida Console", + "Lucida Fax", + "Lucida Sans", + "Lucida Sans Typewriter", + "Lucida Sans Unicode", + "MS Outlook", + "MS Reference Sans Serif", + "MS Reference Specialty", + "MT Extra", + "Maiandra GD", + "Matura MT Script Capitals", + "Metal", + "Minion Pro", + "Mistral", + "Modern No. 20", + "Monotype Corsiva", + "Myriad Pro", + "Niagara Engraved", + "Niagara Solid", + "OCR A Extended", + "Old English Text MT", + "Onyx", + "Optima", + "Palace Script MT", + "Parchment", + "Perpetua", + "Playbill", + "Poor Richard", + "Pristina", + "Rage Italic", + "Ravie", + "Rockwell Condensed", + "Rockwell Extra", + "Rockwell Extra Bold", + "Script MT", + "Script MT Bold", + "Segoe Script", + "Segoe UI", + "Showcard Gothic", + "Simplified Arabic", + "Sindbad", + "Skia", + "Snap ITC", + "Stencil", + "Tempus Sans ITC", + "Times", + "Times New Roman", + "Traditional Arabic", + "Trajan Pro", + "Tw Cen MT", + "Tw Cen MT Condensed", + "Tw Cen MT Condensed Extra Bold", + "TypoUpright BT", + "Viner Hand ITC", + "Vladimir Script", + "Wide Latin" + ] + } +} \ No newline at end of file diff --git a/src/stealthfox/download.py b/src/stealthfox/download.py new file mode 100644 index 0000000..7e2cf64 --- /dev/null +++ b/src/stealthfox/download.py @@ -0,0 +1,151 @@ +"""Download and cache the patched Firefox binary from GitHub Releases.""" +from __future__ import annotations + +import hashlib +import os +import platform +import re +import sys +import tarfile +import tempfile +import zipfile +from pathlib import Path + +import platformdirs +import requests + +from .constants import ( + ARCHIVE_NAME, + BINARY_ENTRY_REL, + BINARY_VERSION, + RELEASE_URL_TEMPLATE, +) + + +def _github_token() -> str | None: + return os.environ.get("STEALTHFOX_GITHUB_TOKEN") or os.environ.get("GITHUB_TOKEN") + + +def _parse_owner_repo(template: str) -> tuple[str, str]: + """Extract (owner, repo) from RELEASE_URL_TEMPLATE.""" + m = re.match(r"https://github\.com/([^/]+)/([^/]+)/releases/", template) + if not m: + raise RuntimeError(f"cannot parse owner/repo from {template!r}") + return m.group(1), m.group(2) + + +def cache_root() -> Path: + """Directory where all cached binaries live.""" + return Path(platformdirs.user_cache_dir("stealthfox")) + + +def cache_dir_for_version(version: str = BINARY_VERSION) -> Path: + return cache_root() / version + + +def _resolve_asset_url(tag: str, asset_name: str) -> str: + """Return a downloadable URL for the asset. + + For private repos the direct `releases/download//` URL returns + 404 even with a token, so we resolve via the API: list assets for the + release tag, find the one matching `asset_name`, and use its API URL with + `Accept: application/octet-stream` (which 302-redirects to a signed URL). + For public repos the direct URL still works without a token. + """ + token = _github_token() + if not token: + return RELEASE_URL_TEMPLATE.format(tag=tag, asset=asset_name) + owner, repo = _parse_owner_repo(RELEASE_URL_TEMPLATE) + api = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{tag}" + r = requests.get(api, headers={"Authorization": f"token {token}"}, timeout=30) + r.raise_for_status() + for a in r.json().get("assets", []): + if a.get("name") == asset_name: + return a["url"] + raise RuntimeError(f"asset {asset_name!r} not found in release {tag!r}") + + +def _download_file(url: str, dst: Path, chunk_size: int = 1 << 16) -> None: + dst.parent.mkdir(parents=True, exist_ok=True) + headers: dict[str, str] = {} + token = _github_token() + if token and url.startswith("https://api.github.com/"): + headers["Authorization"] = f"token {token}" + headers["Accept"] = "application/octet-stream" + with requests.get(url, stream=True, timeout=60, headers=headers) as r: + r.raise_for_status() + with open(dst, "wb") as f: + for chunk in r.iter_content(chunk_size): + if chunk: + f.write(chunk) + + +def _sha256_file(path: Path) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1 << 16), b""): + h.update(chunk) + return h.hexdigest() + + +def _parse_checksums(text: str) -> dict[str, str]: + out: dict[str, str] = {} + for line in text.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + if len(parts) >= 2: + out[parts[-1]] = parts[0] + return out + + +def _extract(archive: Path, dst: Path) -> None: + dst.mkdir(parents=True, exist_ok=True) + if archive.suffix == ".zip": + with zipfile.ZipFile(archive) as zf: + zf.extractall(dst) + elif archive.name.endswith(".tar.gz") or archive.suffix in {".tgz", ".gz"}: + with tarfile.open(archive, "r:gz") as tf: + tf.extractall(dst) + else: + raise RuntimeError(f"unknown archive format: {archive}") + + +def ensure_binary(version: str = BINARY_VERSION) -> Path: + """Return a path to a runnable Firefox executable. Download if needed.""" + plat = sys.platform + mach = platform.machine() + asset = ARCHIVE_NAME(plat, mach) + entry_rel = BINARY_ENTRY_REL.get(plat) + if entry_rel is None: + raise NotImplementedError(f"no binary entry for platform {plat}") + + version_dir = cache_dir_for_version(version) + entry = version_dir / entry_rel + if entry.exists(): + return entry + + url_archive = _resolve_asset_url(version, asset) + url_sums = _resolve_asset_url(version, "checksums.txt") + + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + archive_path = tmp / asset + _download_file(url_archive, archive_path) + sums_path = tmp / "checksums.txt" + _download_file(url_sums, sums_path) + sums = _parse_checksums(sums_path.read_text()) + expected = sums.get(asset) + if expected is None: + raise RuntimeError(f"no SHA256 for {asset} in checksums.txt") + actual = _sha256_file(archive_path) + if actual.lower() != expected.lower(): + raise RuntimeError( + f"SHA256 mismatch for {asset}: got {actual}, expected {expected}" + ) + _extract(archive_path, version_dir) + + if not entry.exists(): + raise RuntimeError(f"binary not found after extraction: {entry}") + return entry diff --git a/src/stealthfox/launcher.py b/src/stealthfox/launcher.py new file mode 100644 index 0000000..61c40b7 --- /dev/null +++ b/src/stealthfox/launcher.py @@ -0,0 +1,307 @@ +"""Sync Playwright launcher for stealthfox.""" +from __future__ import annotations + +import secrets +from typing import Any, Dict, Optional, Union + +from playwright.sync_api import Browser, Playwright, sync_playwright + +from ._fpforge import Profile, generate_profile +from ._headless import make_virtual_display +from ._proxy import configure_proxy as _configure_proxy_shared +from .download import ensure_binary +from .prefs import translate_profile_to_prefs + + +def _patch_sync_new_page_sleep(ctx: Any) -> None: + """Wrap ctx.new_page() to add a brief settle after tab creation. + + FF150 with Fission emits an about:newtab navigation ~100ms after a tab + is created. If goto() is called immediately, it races with that internal + navigation and raises "Navigation interrupted by about:newtab". A short + sleep breaks the race without requiring every call-site to know about it. + """ + import time as _time + original_new_page = ctx.new_page + + def patched_new_page(**kw): + page = original_new_page(**kw) + _time.sleep(0.4) + return page + + ctx.new_page = patched_new_page # type: ignore[assignment] + + +# Window-chrome and taskbar offsets measured empirically on a headed +# Firefox 150 (no compositor). Used to derive the default new_context +# viewport so it fits inside the spoofed screen without out-of-bounds. +_CHROME_W = 14 +_CHROME_H = 91 +_TASKBAR_H = 40 + +# IANA → POSIX TZ mapping. Linux glibc accepts IANA names directly via +# /usr/share/zoneinfo, but Windows MSVCRT only understands the POSIX form +# ("EST5EDT") — convert here so ``TZ`` works on both platforms when the +# binary runs on Windows. Common US zones cover the vast majority of +# residential proxies; everything else falls through to its IANA name. +_IANA_TO_POSIX_TZ = { + "America/New_York": "EST5EDT", + "America/Detroit": "EST5EDT", + "America/Indiana/Indianapolis": "EST5EDT", + "America/Kentucky/Louisville": "EST5EDT", + "America/Chicago": "CST6CDT", + "America/Denver": "MST7MDT", + "America/Los_Angeles": "PST8PDT", + # Arizona (except Navajo Nation) does NOT observe DST. Mapping it to + # MST7MDT made libc apply DST → Date.getTimezoneOffset() returned -360 + # in summer (Denver-like) instead of -420 (true Phoenix), and FP Pro + # deduced vpn_origin_timezone="America/Denver" → timezone_mismatch. + "America/Phoenix": "MST7", + "America/Anchorage": "AKST9AKDT", + # Hawaii does not observe DST. + "Pacific/Honolulu": "HST10", +} + + +def _tz_env(timezone: str) -> str: + """Return the value to set in ``TZ`` for the given IANA zone.""" + return _IANA_TO_POSIX_TZ.get(timezone, timezone) + + +class Stealthfox: + """Context manager launching a patched Firefox with a deterministic profile. + + Usage: + + from stealthfox import Stealthfox + + # random seed (different fingerprint each call) + with Stealthfox() as browser: + page = browser.new_page() + page.goto("https://example.com") + + # explicit seed → same profile every time + with Stealthfox(seed=42) as browser: + ... + + # human-like cursor motion (Bezier trajectory on every mousemove) + with Stealthfox(humanize=True) as browser: + ... + + Optional ``pin`` forces specific fingerprint fields while the rest still + varies with ``seed``:: + + with Stealthfox(seed=42, pin={"screen.width": 2560}) as browser: + ... + + After construction, the chosen seed is available as ``self.seed`` — useful + to reproduce a random run later. + """ + + def __init__( + self, + seed: Optional[int] = None, + *, + pin: Optional[Dict[str, Any]] = None, + headless: bool = False, + proxy: Optional[Dict[str, str]] = None, + extra_args: Optional[list[str]] = None, + humanize: Union[bool, float] = True, + locale: str = "en-US", + timezone: str = "", + extra_prefs: Optional[Dict[str, Any]] = None, + binary_path: Optional[str] = None, + ) -> None: + """ + Args: + seed: Integer seed driving the Bayesian fingerprint sampler. + Same seed → same fingerprint. ``None`` = fresh random. + pin: Force specific fingerprint fields (see docs/pinning.md). + headless: When ``True``, browser renders on a hidden virtual + display (Xvfb on Linux, ``CreateDesktop`` on Windows) so + Firefox stays in *headed* mode (real rendering pipeline, + coherent fingerprint) without showing windows. + proxy: ``{"server": "...", "username": "...", "password": "..."}``. + ``socks5://`` / ``socks4://`` go through the patched + ``nsProtocolProxyService``; ``http(s)://`` go through + Playwright's own ``proxy=`` kwarg. + extra_args: Extra command-line args forwarded to Firefox. + humanize: Every mouse move is expanded by the patched Juggler + into a Bezier trajectory with ~10 ms between waypoints. + Default ``True`` (~1.5 s max motion). ``False`` disables; + a float caps the motion in seconds. + locale: BCP-47 tag (e.g. ``"en-US"``). Drives the + ``Accept-Language`` header and ``navigator.language``. + timezone: IANA timezone (e.g. ``"America/New_York"``). Empty + means use the host TZ. + extra_prefs: Optional dict of Firefox prefs overlayed on top + of the generated profile — useful for niche tweaks + without monkey-patching the package. + """ + # Constrain to int31 — Firefox's `zoom.stealth.fpp.hw_seed` and + # related stealth prefs are declared as ``int32_t`` in + # ``StaticPrefList.yaml``. A 32-bit seed risks the high bit being + # interpreted as negative on the C++ side, where the noise hooks + # bail out on ``seed <= 0`` — which produces bit-identical audio + # / canvas fingerprints across half the sessions. + self.seed: int = int(seed) if seed is not None else secrets.randbits(31) + self._pin = pin + self._headless = headless + self._proxy = proxy + self._extra_args = list(extra_args or []) + self._humanize = humanize + self._locale = locale + self._timezone = timezone + self._extra_prefs = extra_prefs + self._binary_path = binary_path + self._profile: Profile = generate_profile(self.seed, pin=self._pin) + self._pw: Optional[Playwright] = None + self._browser: Optional[Browser] = None + self._virtual_display: Any = None + + def __enter__(self) -> Browser: + executable = self._binary_path or ensure_binary() + prefs = self._build_prefs() + playwright_proxy = _configure_proxy_shared(self._proxy, prefs) + pw_headless = self._resolve_headless() + env = self._build_env() + + try: + self._pw = sync_playwright().start() + self._browser = self._pw.firefox.launch( + executable_path=str(executable), + headless=pw_headless, + firefox_user_prefs=prefs, + proxy=playwright_proxy, + args=self._extra_args, + env=env, + ) + except BaseException: + # Python doesn't call __exit__ when __enter__ raises — clean up + # the virtual display + Playwright manually so we don't leak Xvfb + # / desktop handles into the user's process. + self._teardown() + raise + self._patch_new_context_defaults(self._browser) + return self._browser + + def _patch_new_context_defaults(self, browser: Browser) -> None: + """Wrap ``browser.new_context`` so its defaults derive from the + profile (viewport, screen, DPR, color-scheme). Users get a + coherent context for free; explicit kwargs still override. + """ + original = browser.new_context + defaults = self._default_context_kwargs() + + def patched(**kw): + merged = dict(defaults) + merged.update(kw) # user-supplied wins + ctx = original(**merged) + _patch_sync_new_page_sleep(ctx) + return ctx + + browser.new_context = patched # type: ignore[assignment] + + def _default_context_kwargs(self) -> Dict[str, Any]: + p = self._profile + kwargs: Dict[str, Any] = { + "viewport": {"width": p.screen.width - _CHROME_W, + "height": p.screen.height - _TASKBAR_H - _CHROME_H}, + "screen": {"width": p.screen.width, "height": p.screen.height}, + "device_scale_factor": p.screen.dpr, + "color_scheme": "dark" if p.dark_theme else "light", + } + # Pass timezone via Playwright's per-realm override (docShell.overrideTimezone + # → JS::SetRealmTimezoneOverride). The juggler.timezone.override pref path + # uses JS::SetTimeZoneOverride globally, which is broken on Windows ICU for + # no-DST IANA names (America/Phoenix, Pacific/Honolulu, ...) — those silently + # fall back to the host system TZ. The per-realm path works for every zone. + if self._timezone: + kwargs["timezone_id"] = self._timezone + if self._locale: + kwargs["locale"] = self._locale + return kwargs + + def __exit__(self, *exc: Any) -> None: + self._teardown() + + def _teardown(self) -> None: + if self._browser is not None: + try: + self._browser.close() + except Exception: + pass + self._browser = None + if self._pw is not None: + try: + self._pw.stop() + except Exception: + pass + self._pw = None + if self._virtual_display is not None: + try: + self._virtual_display.stop() + except Exception: + pass + self._virtual_display = None + + # ── helpers ───────────────────────────────────────────────────────── + + def _build_prefs(self) -> Dict[str, Any]: + """Fingerprint prefs plus humanize toggle (always set explicitly).""" + import sys as _sys + prefs = translate_profile_to_prefs( + self._profile, + locale=self._locale, + timezone=self._timezone, + extra_prefs=self._extra_prefs, + virtual_display=bool(self._headless and _sys.platform == "win32"), + ) + prefs["stealthfox.humanize"] = bool(self._humanize) + if self._humanize: + prefs["stealthfox.humanize.maxTime"] = str(self._humanize_max_seconds()) + return prefs + + def _build_env(self) -> Dict[str, str]: + """Env vars passed to the Firefox subprocess. + + ``TZ`` tunes the libc clock the content process reads for + ``Date`` / ``Intl.DateTimeFormat`` so the JS-visible timezone + matches ``self._timezone`` regardless of the host TZ. + ``STEALTHFOX_WEBRTC_PUBLIC_IP`` is propagated when the calling + process has set it — read by nICEr's nr_stealth_bridge to inject + a synthetic srflx candidate matching the proxy egress IP, avoiding + the StaticPref IPC propagation timing issue between parent and + socket processes. + """ + import os as _os + env = _os.environ.copy() + if self._timezone: + env["TZ"] = _tz_env(self._timezone) + # Propagate STEALTHFOX_WEBRTC_PUBLIC_IP if the process set it — read + # by nICEr's nr_stealth_bridge to inject a synthetic srflx candidate + # matching the proxy egress IP. This avoids the StaticPref IPC + # propagation timing issue between parent and socket processes. + if _os.environ.get("STEALTHFOX_WEBRTC_PUBLIC_IP"): + env["STEALTHFOX_WEBRTC_PUBLIC_IP"] = _os.environ["STEALTHFOX_WEBRTC_PUBLIC_IP"] + return env + + def _resolve_headless(self) -> bool: + """Translate the user's ``headless`` flag. + + When ``True``, we keep Firefox in headed mode (real rendering + pipeline → coherent fingerprint) and hide the windows on a fresh + Xvfb (Linux) or hidden Windows desktop. + """ + if not self._headless: + return False + vd = make_virtual_display() + vd.start() + self._virtual_display = vd + return False + + def _humanize_max_seconds(self) -> float: + if self._humanize is True: + return 1.5 + return float(self._humanize) + diff --git a/src/stealthfox/prefs.py b/src/stealthfox/prefs.py new file mode 100644 index 0000000..43ece27 --- /dev/null +++ b/src/stealthfox/prefs.py @@ -0,0 +1,568 @@ +"""Translate an internal Profile into the Firefox prefs dict that the +patched Firefox binary expects. + +The output dict keys map 1:1 to ``user.js`` preferences. Playwright passes +them via ``firefox_user_prefs=``. The patched binary propagates them to all +content processes over IPC; C++ patches read the ``zoom.stealth.*`` +namespace. + +The translation is split into: + + * ``_BASELINE`` — global stealth policy (RFP off, WebRTC leaks blocked, + safebrowsing disabled, debugger detach, …) plus Windows-canonical + constants that don't depend on the Profile (system colors palette, + WebGL extensions whitelist, speech voices, navigator identity). + * ``translate_profile_to_prefs`` — overlays the Profile fields plus the + user-supplied ``locale`` and ``timezone``. +""" +from __future__ import annotations + +import sys +from typing import Any, Dict, Optional + +from ._fpforge import Profile + + +# ────────────────────────────────────────────────────────────────────── +# Navigator identity — locked to Firefox 150 Windows so the binary +# reports the same UA / platform / oscpu regardless of the host OS. +# ────────────────────────────────────────────────────────────────────── + +_NAVIGATOR_OVERRIDES: Dict[str, str] = { + "general.useragent.override": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:150.0) " + "Gecko/20100101 Firefox/150.0.1", + "general.platform.override": "Win32", + "general.oscpu.override": "Windows NT 10.0; Win64; x64", + # general.buildID.override removed 2026-04-28: the previous value + # "20181001000000" was a 2018 buildID stuck on a 2026-built Firefox 150 + # binary (real BuildID=20260426192818 from application.ini). The 7.5-yr + # discrepancy is the kind of internal-consistency check Google reCAPTCHA + # can use to flag bot/spoofed browsers. Deleting the override lets + # Firefox emit its compiled-in buildID, which auto-tracks the binary. + # A/B knockout 2026-04-28 (n=30): F2 delete +0.083 RC vs BASE; n=100 + # confirm: +0.021; overnight isolated: +0.155 single-variant. Variable + # signal, but the underlying data error is unambiguous. + "general.appversion.override": "5.0 (Windows)", +} + + +# ────────────────────────────────────────────────────────────────────── +# System colors — FP Pro probes getComputedStyle(div) with CSS system +# keywords (ButtonFace, Menu, Highlight, …) and hashes the result into +# signal s142. On Linux, Firefox resolves these via GTK theme → GTK +# RGB values diverge from Windows Win32 palette → server-side anomaly +# even with Windows UA. Pinning the palette to Win10 default closes +# the gap (see project_css_system_colors.md memory). +# ────────────────────────────────────────────────────────────────────── + +_WIN_LIGHT_COLORS: Dict[str, str] = { + "ui.activeborder": "#B4B4B4", + "ui.activecaption": "#99B4D1", + "ui.appworkspace": "#ABABAB", + "ui.background": "#000000", + "ui.buttonface": "#F0F0F0", + "ui.buttonhighlight": "#FFFFFF", + "ui.buttonshadow": "#A0A0A0", + "ui.buttontext": "#000000", + "ui.buttonborder": "#000000", + "ui.captiontext": "#000000", + "ui.graytext": "#6D6D6D", + "ui.highlight": "#0078D7", + "ui.highlighttext": "#FFFFFF", + "ui.inactiveborder": "#F4F7FC", + "ui.inactivecaption": "#BFCDDB", + "ui.inactivecaptiontext": "#434E54", + "ui.infobackground": "#FFFFE1", + "ui.infotext": "#000000", + "ui.menu": "#F9F9FB", + "ui.menutext": "#000000", + "ui.scrollbar": "#C8C8C8", + "ui.threeddarkshadow": "#696969", + "ui.threedface": "#F0F0F0", + "ui.threedhighlight": "#FFFFFF", + "ui.threedlightshadow": "#E3E3E3", + "ui.threedshadow": "#A0A0A0", + "ui.window": "#FFFFFF", + "ui.windowframe": "#646464", + "ui.windowtext": "#000000", + "ui.mark": "#FFFF00", + "ui.marktext": "#000000", + "ui.accentcolor": "#0078D4", + "ui.accentcolortext": "#FFFFFF", + "ui.selecteditem": "#0078D7", + "ui.selecteditemtext": "#FFFFFF", + "ui.-moz-hyperlinktext": "#0066CC", + "ui.-moz-activehyperlinktext": "#EE0000", + "ui.-moz-visitedhyperlinktext": "#551A8B", +} + + +# ────────────────────────────────────────────────────────────────────── +# WebGL extensions — Windows ANGLE canonical lists. Empty string = +# fall back to native Mesa/ANGLE; non-empty = `getSupportedExtensions` +# returns this list verbatim and `IsSupported()` rejects anything else. +# ────────────────────────────────────────────────────────────────────── + +_WEBGL1_EXTENSIONS = ",".join([ + "ANGLE_instanced_arrays", + "EXT_blend_minmax", + "EXT_color_buffer_half_float", + "EXT_float_blend", + "EXT_frag_depth", + "EXT_sRGB", + "EXT_shader_texture_lod", + "EXT_texture_compression_bptc", + "EXT_texture_compression_rgtc", + "EXT_texture_filter_anisotropic", + "OES_element_index_uint", + "OES_fbo_render_mipmap", + "OES_standard_derivatives", + "OES_texture_float", + "OES_texture_float_linear", + "OES_texture_half_float", + "OES_texture_half_float_linear", + "OES_vertex_array_object", + "WEBGL_color_buffer_float", + "WEBGL_compressed_texture_s3tc", + "WEBGL_compressed_texture_s3tc_srgb", + "WEBGL_debug_renderer_info", + "WEBGL_debug_shaders", + "WEBGL_depth_texture", + "WEBGL_draw_buffers", + "WEBGL_lose_context", + "WEBGL_provoking_vertex", +]) + +_WEBGL2_EXTENSIONS = ",".join([ + "EXT_color_buffer_float", + "EXT_color_buffer_half_float", + "EXT_float_blend", + "EXT_texture_compression_bptc", + "EXT_texture_compression_rgtc", + "EXT_texture_filter_anisotropic", + "OES_draw_buffers_indexed", + "OES_texture_float_linear", + "OES_texture_half_float_linear", + "OVR_multiview2", + "WEBGL_compressed_texture_s3tc", + "WEBGL_compressed_texture_s3tc_srgb", + "WEBGL_debug_renderer_info", + "WEBGL_debug_shaders", + "WEBGL_lose_context", + "WEBGL_provoking_vertex", +]) + + +# ────────────────────────────────────────────────────────────────────── +# Speech voices — Windows canonical "Microsoft *" set. Format: +# "NAME|LANG|DEFAULT|LOCAL,...". Non-empty value drives the +# speechSynthesis.getVoices() patch; empty disables it. +# ────────────────────────────────────────────────────────────────────── + +_WIN_VOICES = ",".join([ + "Microsoft David - English (United States)|en-US|1|1", + "Microsoft Zira - English (United States)|en-US|0|1", + "Microsoft Mark - English (United States)|en-US|0|1", + "Microsoft David Desktop - English (United States)|en-US|0|1", + "Microsoft Zira Desktop - English (United States)|en-US|0|1", +]) + + +# ────────────────────────────────────────────────────────────────────── +# Linux font compensation — Linux Firefox uses DejaVu / Liberation +# fonts which have wider/narrower glyphs than Windows Arial / Segoe. +# These per-generic factors are prepended to ``zoom.stealth.font.metrics`` +# on Linux only; Windows-native rendering already matches the canonical +# widths so we pass an empty string (any factor !=1 would distort real +# metrics). +# ────────────────────────────────────────────────────────────────────── + +_LINUX_GENERIC_FONT_FACTORS = ( + # Calibrated to bring DejaVu/Liberation widths in line with what Windows + # FP Pro probes report for native Segoe/Times. Linux base measurements + # (font_preferences) and Windows targets: + # serif: 162 → 149 factor 0.920 + # sans: 162 → 144 factor 0.889 + # monospace:121 → 121 factor 1.000 + # system: 162 → 147 factor 0.910 + "serif|0.920,sans-serif|0.889,monospace|1.000," + "system-ui|0.910,cursive|0.932,fantasy|0.812," +) + + +# ────────────────────────────────────────────────────────────────────── +# Baseline — applied to every session regardless of Profile. +# ────────────────────────────────────────────────────────────────────── + +_BASELINE: Dict[str, Any] = { + # Turn off Firefox's own resistFingerprinting; we do our own via patches. + "privacy.resistFingerprinting": False, + "privacy.resistFingerprinting.letterboxing": False, + + # FF150 fingerprintingProtection — enabled by default (or remotely via + # Mozilla webcompat overrides). FP Pro detects the side-effects and + # flips `privacy_settings: true`. On FF146 these were all off → False. + # Force off so FP Pro reports privacy_settings:false (matches FF146). + "privacy.fingerprintingProtection": False, + "privacy.fingerprintingProtection.pbmode": False, + "privacy.fingerprintingProtection.remoteOverrides.enabled": False, + + # WebRTC: enabled, no public IP leak. + # obfuscate_host_addresses=false: our C++ injection handles candidate + # selection; mDNS causes mDNS-IPC to hang in sandboxed content processes. + # disableIPv6=true keeps IPv6 out of gathering (less entropy, no IPv6 leak). + "media.peerconnection.enabled": True, + "media.peerconnection.ice.no_host": False, + "media.peerconnection.ice.default_address_only": False, + "media.peerconnection.ice.obfuscate_host_addresses": False, + "media.peerconnection.ice.disableIPv6": True, + "media.peerconnection.ice.proxy_only": False, + "media.peerconnection.ice.relay_only": False, + "media.peerconnection.use_document_iceservers": True, + + # Proxy — route DNS through SOCKS proxies to avoid local DNS leaks. + "network.proxy.socks_remote_dns": True, + "network.proxy.failover_direct": False, + + # Safebrowsing — chatty and fingerprintable. + "browser.safebrowsing.malware.enabled": False, + "browser.safebrowsing.phishing.enabled": False, + "browser.safebrowsing.downloads.enabled": False, + "browser.safebrowsing.downloads.remote.enabled": False, + + # First-run / welcome UI noise. + "browser.startup.page": 0, + "browser.shell.checkDefaultBrowser": False, + "browser.aboutwelcome.enabled": False, + "browser.startup.upgradeDialog.enabled": False, + "termsofuse.acceptedVersion": 999, + + # Disable about:newtab auto-load — TopSitesFeed.sys.mjs auto-fetches when + # a tab opens, triggering a cross-process BC swap that hijacks the first + # page.goto() (NS_BINDING_ABORTED on creepjs/peet/sannysoft/fppro). + "browser.newtabpage.enabled": False, + "browser.newtab.preload": False, + "browser.newtabpage.activity-stream.feeds.topsites": False, + "browser.newtabpage.activity-stream.feeds.section.topstories": False, + "browser.newtabpage.activity-stream.enabled": False, + + # Disable Firefox internal services that hit the network on startup. + # Through a residential SOCKS5 proxy these compete with the test + # navigation and trigger NS_BINDING_FAILED (server-side rate-limit / + # connection drops). Domains observed in MOZ_LOG: push.services, + # firefox.settings.services, detectportal, ohttp-gateway, location. + "browser.aboutConfig.showWarning": False, + "network.captive-portal-service.enabled": False, + "network.connectivity-service.enabled": False, + "dom.push.enabled": False, + "dom.push.connection.enabled": False, + "geo.enabled": False, + "geo.provider.network.url": "", + "browser.region.network.url": "", + "browser.region.update.enabled": False, + "services.settings.server": "", + "browser.search.geoSpecificDefaults": False, + "browser.contentblocking.report.lockwise.enabled": False, + "browser.contentblocking.report.monitor.enabled": False, + "extensions.systemAddon.update.enabled": False, + "extensions.update.enabled": False, + "extensions.getAddons.cache.enabled": False, + "browser.discovery.enabled": False, + "browser.ping-centre.telemetry": False, + "app.normandy.enabled": False, + "dom.private-attribution.submission.enabled": False, + "browser.translations.enable": False, + "browser.search.update": False, + + # HTTP/3 + speculative + Alt-Svc disabled. SOCKS5 proxy doesn't + # support UDP ASSOCIATE so HTTP/3 fails. Speculative connections + # under load cause early channel cancel (NS_BINDING_FAILED). + "network.http.http3.enable": False, + "network.http.http3.enabled": False, + "network.http.altsvc.enabled": False, + "network.http.altsvc.oe": False, + "network.http.speculative-parallel-limit": 0, + "network.predictor.enabled": False, + "network.dns.disablePrefetch": True, + "network.dns.disablePrefetchFromHTTPS": True, + "network.dns.echconfig.enabled": False, + "network.dns.use_https_rr_as_altsvc": False, + + # === A/B VARIANT B: Fission disabled === + # Force single content-process model (e10s only, no BC outer/inner split). + # Diagnostic for the FF150 BC-swap theory: if peet_ws/fppro/sannysoft + # work with this off, the Juggler FF146 baseline breaks specifically on + # cross-process navigation tracking. + "fission.autostart": False, + "fission.autostart.session": False, + "dom.ipc.processCount.webIsolated": 1, + + + # Telemetry & data reporting. + "datareporting.healthreport.uploadEnabled": False, + "datareporting.policy.dataSubmissionEnabled": False, + "toolkit.telemetry.enabled": False, + "toolkit.telemetry.unified": False, + "app.shield.optoutstudies.enabled": False, + + # Update channels. + "app.update.enabled": False, + "app.update.auto": False, + + # Speech synth: enabled (the C++ patch fabricates voices from the + # comma list above) regardless of the host OS. + "media.webspeech.synth.enabled": True, + "zoom.stealth.voices.list": _WIN_VOICES, + + # WebGL extensions whitelist — non-empty pre-empts native enumeration. + "zoom.stealth.webgl.extensions": _WEBGL1_EXTENSIONS, + "zoom.stealth.webgl2.extensions": _WEBGL2_EXTENSIONS, + # WebGL numeric param overrides — kept empty (A/B test 2026-04-22 showed + # mismatches between the values we shipped and ANGLE's real envelope + # raised FP Pro's ML tampering score). Slot kept for future experiments. + "zoom.stealth.webgl.int_params": "", + "zoom.stealth.webgl.int2_params": "", + "zoom.stealth.webgl.shader_precisions": "", + "zoom.stealth.webgl.float_params": "", + + # DevTools anti-detection. + "zoom.stealth.debugger.force_detach": True, + + # Canvas substitution — additive ±1 noise over the OS base pattern; + # set to True to replace pixels with hash(seed, idx) instead. + "zoom.stealth.canvas.substitute_pixels": False, + + # Navigator identity (locked to Windows Firefox 150). + **_NAVIGATOR_OVERRIDES, +} + + +# ────────────────────────────────────────────────────────────────────── +# Linux-only Xvfb workarounds — the Linux Firefox build under Xvfb +# cannot run WebRender (`ConnectToCompositor` retries forever). We +# disable WebRender + force WebGL through the GL software path so +# webgl_basics / webgl_extensions still report. +# ────────────────────────────────────────────────────────────────────── + +_LINUX_XVFB_WORKAROUNDS: Dict[str, Any] = { + "gfx.webrender.all": False, + "gfx.webrender.force-disabled": True, + "webgl.force-enabled": True, + # webgl.software-rendering-enabled / webgl.force-layers-readback removed in FF150. +} + +# ────────────────────────────────────────────────────────────────────── +# Windows virtual-desktop workarounds — when headless=True on Windows, +# Firefox runs on a CreateDesktop virtual desktop. The hardware GPU is +# inaccessible from the virtual desktop, so the GPU process crashes when +# it tries to initialize the D3D11 compositor with hardware acceleration. +# +# Approach: force D3D11 WARP (CPU software renderer) for the GPU process. +# layers.d3d11.force-warp=True → compositor uses WARP → GPU process stable. +# webgl.angle.force-warp=True → ANGLE uses WARP → WebGL context creates. +# +# CRITICAL: do NOT set webgl.out-of-process=False. That moves WebGL from the +# GPU process to the sandboxed content process. The content process sandbox +# blocks D3D11 access entirely → ANGLE crashes the content process → +# canvas.getContext('webgl') throws instead of returning null. +# +# gfx.canvas.accelerated=False: default is true, disabling avoids any +# hardware GPU dependency for 2D canvas in the content process. +# ────────────────────────────────────────────────────────────────────── + +_WIN_VIRT_DESKTOP_WORKAROUNDS: Dict[str, Any] = { + # FF150 regression vs FF146 on CreateDesktop alt-desktop: + # The GPU process sandbox (level=1, default since FF110) tries to parent + # its compositor window to the parent process's window. Our worker spawns + # Firefox on a CreateDesktop-created alt desktop — parent and GPU process + # do not share the same desktop/HWND namespace, so window parenting fails + # silently. WebRender falls back to "Software D3D11" and OOP-WebGL never + # publishes a hardware ANGLE renderer → getContext('webgl') returns a + # context but extensions/parameters/$hash all come back null/empty (FF146 + # had a more permissive sandbox, so the same setup worked there). + # Bugzilla refs: 1798091, 1524591, 1229829. Lowering the GPU sandbox to 0 + # restores hardware compositor + functional WebGL on alt desktops. + "security.sandbox.gpu.level": 0, +} + + +# ────────────────────────────────────────────────────────────────────── +# Public helpers +# ────────────────────────────────────────────────────────────────────── + +def _accept_language(locale: str) -> str: + lang = locale.replace("_", "-") + base = lang.split("-")[0] + return f"{lang}, {base}" if base != lang else lang + + +def _font_metrics_for_platform(profile_metrics: str) -> str: + """Return ``zoom.stealth.font.metrics`` value. + + Windows: empty string. The C++ width-scale hook is a no-op and + Firefox renders Arial/Segoe/Calibri/etc. at their native canonical + widths. Applying the Bayesian-sampled per-font factors on a Windows + build would *distort* real metrics and surface as a font_preferences + width anomaly to FP Pro / reCAPTCHA. + + Linux: prepend generic-family compensation factors so DejaVu / + Liberation render at the widths Windows JS expects, then append the + per-font factors that make each fabricated family detectable by + width-diff probes. + """ + if not profile_metrics: + return "" + if sys.platform.startswith("linux"): + return _LINUX_GENERIC_FONT_FACTORS + profile_metrics + return "" # Windows: NEVER apply width-scale factors. + + +def translate_profile_to_prefs( + profile: Profile, + *, + locale: str = "en-US", + timezone: str = "", + extra_prefs: Optional[Dict[str, Any]] = None, + virtual_display: bool = False, +) -> Dict[str, Any]: + """Return a complete prefs dict ready for Playwright's firefox_user_prefs=. + + Args: + profile: Bayesian-sampled fingerprint (from ``generate_profile``). + locale: BCP-47 tag, e.g. ``"en-US"``. + timezone: IANA timezone name, e.g. ``"America/New_York"``. + extra_prefs: Optional overlay applied LAST. + virtual_display: When True on Windows, apply GPU-disabling workarounds + to prevent the GPU process from crashing on virtual + desktops that have no D3D11 backend. + """ + prefs: Dict[str, Any] = dict(_BASELINE) + + # GPU / WebGL renderer/vendor. + # On Linux we spoof to a Windows ANGLE renderer string (profile.gpu.renderer) + # so cross-platform sessions report a consistent Windows GPU identity. + # On Windows, spoofing a different GPU creates a renderer/parameters hash + # mismatch: FP Pro hashes all 81 CN-set getParameter() values including + # enum 7937 (RENDERER). Setting GTX 980 while ANGLE returns Intel Arc A750 + # parameters produces an OOD (hash 23d0a74b vs vanilla 66544db) that FP Pro + # ML scores at ~0.70 (confirmed: direct SF146 vs vanilla on same machine). + # Fix: leave renderer/vendor empty on Windows → ANGLE reports native hardware + # (SanitizeRenderer path at ClientWebGLContext.cpp:2592-2595) → consistent. + if sys.platform.startswith("linux"): + prefs["zoom.stealth.webgl.renderer"] = profile.gpu.renderer + prefs["zoom.stealth.webgl.vendor"] = profile.gpu.vendor + _renderer_lo = (profile.gpu.renderer or "").lower() + else: + prefs["zoom.stealth.webgl.renderer"] = "" + prefs["zoom.stealth.webgl.vendor"] = "" + _renderer_lo = "intel" # test hardware is Intel Arc A750 + + # MSAA: on Windows, pin to 4 (Firefox default for ANGLE) so gl.SAMPLES is + # constant across all sessions. Different MSAA values cause different CN-set + # parameters hashes even with the same renderer → detectable variation. + # Vanilla Intel Arc A750 parameters hash (66544db8) verified at msaa=4. + _msaa = profile.webgl.msaa_samples if sys.platform.startswith("linux") else 4 + prefs["zoom.stealth.webgl.msaa"] = _msaa + prefs["webgl.msaa-samples"] = _msaa + prefs["webgl.msaa-force"] = _msaa > 0 + + # Canvas pixel-noise density per vendor. Intel has lower natural + # rendering variance than NVIDIA/AMD, so the default 1/8 noise rate + # over-amplifies the FP Pro tampering ML signal. Drop to 1/16 for Intel + # to keep tampering_ml below the detection threshold while still + # breaking the canvas geometry hash. + if "intel" in _renderer_lo: + prefs["zoom.stealth.canvas.noise_skip_mask"] = 15 # 1/16, ~6.25% + else: + prefs["zoom.stealth.canvas.noise_skip_mask"] = 7 # 1/8, ~12.5% + + # Screen + prefs["zoom.stealth.screen.width"] = profile.screen.width + prefs["zoom.stealth.screen.height"] = profile.screen.height + prefs["zoom.stealth.screen.avail_width"] = profile.screen.avail_width + prefs["zoom.stealth.screen.avail_height"] = profile.screen.avail_height + prefs["zoom.stealth.screen.dpr"] = profile.screen.dpr + prefs["layout.css.devPixelsPerPx"] = str(profile.screen.dpr) + + # Hardware + prefs["zoom.stealth.hw_concurrency"] = profile.hardware.concurrency + prefs["zoom.stealth.storage.quota_mb"] = profile.hardware.storage_quota_mb + + # Audio + prefs["zoom.stealth.audio.sample_rate"] = profile.audio.sample_rate + prefs["zoom.stealth.audio.output_latency_ms"] = profile.audio.output_latency_ms + prefs["zoom.stealth.audio.max_channel_count"] = profile.audio.max_channel_count + + # Codec + prefs["media.av1.enabled"] = profile.codec.av1_enabled + prefs["media.encoder.webm.enabled"] = profile.codec.webm_encoder_enabled + prefs["media.mediasource.webm.enabled"] = profile.codec.mediasource_webm + prefs["media.mediasource.mp4.enabled"] = profile.codec.mediasource_mp4 + + # Fonts + prefs["zoom.stealth.font.whitelist"] = ",".join(profile.fonts) + prefs["zoom.stealth.font.metrics"] = _font_metrics_for_platform( + profile._raw.get("font_metrics", "") or "" + ) + + # UI / dark mode + Windows colors palette (only when light theme). + prefs["ui.systemUsesDarkTheme"] = int(profile.dark_theme) + if not profile.dark_theme: + prefs.update(_WIN_LIGHT_COLORS) + + # Locale prefs. + locale = locale or "en-US" + lang = locale.replace("_", "-") + prefs["intl.accept_languages"] = _accept_language(locale) + prefs["general.useragent.locale"] = lang + prefs["intl.locale.requested"] = lang + prefs["privacy.spoof_english"] = 0 + + if timezone: + prefs["zoom.stealth.timezone"] = timezone + prefs["juggler.timezone.override"] = timezone + + # Cross-process seed (canvas noise + DWrite gamma share this). + prefs["zoom.stealth.fpp.hw_seed"] = profile.seed + prefs["zoom.stealth.seed"] = profile.seed + + # Synthetic host ICE candidate — injected by C++ when addr_ct==0 (SOCKS5 + # proxy suppresses all local addresses so Firefox can't gather host cands). + # LAN IP is seed-derived so it's consistent per session and looks like a + # real home router assignment (192.168.x.x range). + _s = profile.seed + _lan_ip = f"192.168.{(_s >> 8) % 254 + 1}.{_s % 254 + 1}" + prefs["zoom.stealth.webrtc.host_ip"] = _lan_ip + + # On Windows, native ANGLE extension list already matches real Windows users. + # The baseline hard-codes a curated _WEBGL1/2_EXTENSIONS list designed for + # Linux Mesa → clear it so Windows sessions report the native extension set + # (hash matches real Intel Arc A750 vanilla captures). + if not sys.platform.startswith("linux"): + prefs["zoom.stealth.webgl.extensions"] = "" + prefs["zoom.stealth.webgl2.extensions"] = "" + + # Linux Xvfb workarounds (no-op on Windows). + if sys.platform.startswith("linux"): + for k, v in _LINUX_XVFB_WORKAROUNDS.items(): + prefs.setdefault(k, v) + + # Windows virtual-desktop workarounds (headless=True on Windows). + if virtual_display and sys.platform == "win32": + for k, v in _WIN_VIRT_DESKTOP_WORKAROUNDS.items(): + prefs.setdefault(k, v) + + # Caller overlay LAST so users can override anything we set. A value of + # None is treated as a sentinel meaning "delete this pref entirely from + # the final dict" — useful for A/B harnesses that need to test what + # happens when an override is unset (vs set to empty string, which for + # some prefs like general.useragent.override means literally empty UA). + if extra_prefs: + for k, v in extra_prefs.items(): + if v is None: + prefs.pop(k, None) + else: + prefs[k] = v + + return prefs diff --git a/src/stealthfox/sync_api.py b/src/stealthfox/sync_api.py new file mode 100644 index 0000000..589b558 --- /dev/null +++ b/src/stealthfox/sync_api.py @@ -0,0 +1,4 @@ +"""Synchronous API — re-exports Stealthfox for parity with async_api.""" +from .launcher import Stealthfox + +__all__ = ["Stealthfox"] diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..3fab607 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,22 @@ +import subprocess +import sys + + +def test_version_subcommand(): + r = subprocess.run( + [sys.executable, "-m", "stealthfox", "version"], + capture_output=True, text=True, check=True, + ) + assert "firefox-" in r.stdout + assert "stealthfox" in r.stdout.lower() + + +def test_help_subcommand(): + r = subprocess.run( + [sys.executable, "-m", "stealthfox", "--help"], + capture_output=True, text=True, + ) + assert r.returncode == 0 + assert "fetch" in r.stdout + assert "path" in r.stdout + assert "clear-cache" in r.stdout diff --git a/tests/test_constants.py b/tests/test_constants.py new file mode 100644 index 0000000..d5ac54d --- /dev/null +++ b/tests/test_constants.py @@ -0,0 +1,29 @@ +from stealthfox.constants import BINARY_VERSION, BINARY_BASENAME, ARCHIVE_NAME + + +def test_binary_version_format(): + assert BINARY_VERSION.startswith("firefox-") + assert BINARY_VERSION.split("-", 1)[1].isdigit() + + +def test_archive_name_windows(): + name = ARCHIVE_NAME("win32", "AMD64") + assert name.endswith(".zip") + assert "win-x86_64" in name + + +def test_archive_name_linux(): + name = ARCHIVE_NAME("linux", "x86_64") + assert name.endswith(".tar.gz") + assert "linux-x86_64" in name + + +def test_archive_name_unsupported_raises(): + import pytest + with pytest.raises(NotImplementedError): + ARCHIVE_NAME("darwin", "arm64") + + +def test_binary_basename_format(): + assert "firefox" in BINARY_BASENAME.lower() + assert "stealth" in BINARY_BASENAME.lower() diff --git a/tests/test_download.py b/tests/test_download.py new file mode 100644 index 0000000..d01e947 --- /dev/null +++ b/tests/test_download.py @@ -0,0 +1,71 @@ +import hashlib +from pathlib import Path + +import pytest +import responses + +from stealthfox.download import ensure_binary +from stealthfox.constants import BINARY_VERSION + + +def _make_zip(path: Path, inner_name: str, payload: bytes) -> bytes: + import io + import zipfile + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w") as zf: + zf.writestr(inner_name, payload) + data = buf.getvalue() + path.write_bytes(data) + return data + + +@responses.activate +def test_ensure_binary_downloads_and_verifies(tmp_path, monkeypatch): + """Full path: cache miss -> HTTP GET -> SHA256 check -> extract -> return path.""" + cache = tmp_path / "cache" + monkeypatch.setattr("stealthfox.download.cache_root", lambda: cache) + + archive_path = tmp_path / "archive.zip" + archive_bytes = _make_zip(archive_path, "firefox.exe", b"PEX!") + archive_sha = hashlib.sha256(archive_bytes).hexdigest() + from stealthfox.constants import ARCHIVE_NAME + asset = ARCHIVE_NAME("win32", "AMD64") + + url_archive = f"https://github.com/feder-cr/stealthfox/releases/download/{BINARY_VERSION}/{asset}" + url_sums = f"https://github.com/feder-cr/stealthfox/releases/download/{BINARY_VERSION}/checksums.txt" + + responses.add(responses.GET, url_archive, body=archive_bytes, status=200, + content_type="application/zip") + responses.add(responses.GET, url_sums, + body=f"{archive_sha} {asset}\n", status=200) + + monkeypatch.setattr("sys.platform", "win32") + import platform + monkeypatch.setattr(platform, "machine", lambda: "AMD64") + + path = ensure_binary() + assert Path(path).exists() + assert Path(path).name == "firefox.exe" + + +@responses.activate +def test_ensure_binary_rejects_sha_mismatch(tmp_path, monkeypatch): + cache = tmp_path / "cache" + monkeypatch.setattr("stealthfox.download.cache_root", lambda: cache) + archive_path = tmp_path / "archive.zip" + archive_bytes = _make_zip(archive_path, "firefox.exe", b"PEX!") + wrong_sha = "0" * 64 + from stealthfox.constants import ARCHIVE_NAME + asset = ARCHIVE_NAME("win32", "AMD64") + + url_archive = f"https://github.com/feder-cr/stealthfox/releases/download/{BINARY_VERSION}/{asset}" + url_sums = f"https://github.com/feder-cr/stealthfox/releases/download/{BINARY_VERSION}/checksums.txt" + responses.add(responses.GET, url_archive, body=archive_bytes, status=200) + responses.add(responses.GET, url_sums, body=f"{wrong_sha} {asset}\n", status=200) + + monkeypatch.setattr("sys.platform", "win32") + import platform + monkeypatch.setattr(platform, "machine", lambda: "AMD64") + + with pytest.raises(RuntimeError, match="SHA256"): + ensure_binary() diff --git a/tests/test_prefs.py b/tests/test_prefs.py new file mode 100644 index 0000000..2d32ea0 --- /dev/null +++ b/tests/test_prefs.py @@ -0,0 +1,35 @@ +from stealthfox._fpforge import generate_profile +from stealthfox.prefs import translate_profile_to_prefs + + +def test_translate_includes_gpu_renderer(): + p = generate_profile(seed=42) + prefs = translate_profile_to_prefs(p) + assert prefs["zoom.stealth.webgl.renderer"] == p.gpu.renderer + assert prefs["zoom.stealth.webgl.vendor"] == p.gpu.vendor + + +def test_translate_includes_screen(): + p = generate_profile(seed=42) + prefs = translate_profile_to_prefs(p) + assert prefs["zoom.stealth.screen.width"] == p.screen.width + assert prefs["zoom.stealth.screen.height"] == p.screen.height + + +def test_translate_is_deterministic_per_seed(): + a = translate_profile_to_prefs(generate_profile(seed=42)) + b = translate_profile_to_prefs(generate_profile(seed=42)) + assert a == b + + +def test_translate_varies_across_seeds(): + a = translate_profile_to_prefs(generate_profile(seed=1)) + b = translate_profile_to_prefs(generate_profile(seed=2)) + assert a != b + + +def test_translate_has_stealth_baseline_constants(): + p = generate_profile(seed=42) + prefs = translate_profile_to_prefs(p) + assert prefs.get("privacy.resistFingerprinting") is False + assert "media.peerconnection.enabled" in prefs