mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-04 16:00:21 +00:00
With a spoofed locale, Intl.DisplayNames(['en'], {type: 'region'}).of(code)
returned the spoofed region's name for every code, and an explicitly named
Intl.Locale took the spoofed subtags. The cause, Language()/Region() being
overridden on every mozilla::intl::Locale, was already removed from
patches/locale-spoofing.patch in #779, so v156.0.1-beta.32 and later are
fixed; 152.0.4-beta.30/31 still have it.
Nothing tested it, so add a spoofing guard. With locale="tr-TR" it passes on
v156.0.1-beta.33 and fails on 152.0.4-beta.30, where every code comes back
as "Türkiye" and Intl.Locale('ja-Jpan-JP').minimize() is "tr-Latn-TR".
Reported-by: neuronaline (https://github.com/daijro/camoufox/issues/824)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
5.1 KiB
Python
147 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Patch-guard gates: tests/patches/*.py, one standalone guard per shipped behaviour.
|
|
|
|
Whether the patches *apply* is the build's job (scripts/patch.py fails it).
|
|
These check that what they do still works in the built browser -- the most
|
|
direct evidence that a Firefox bump did not quietly neuter a patch that still
|
|
applies cleanly, which is the failure mode a compile check cannot catch.
|
|
|
|
The guards fall into three groups, each its own suite and CI job:
|
|
|
|
spoofing a spoofed value still reaches the page and holds together
|
|
automation Playwright stays invisible to the page and never deadlocks it
|
|
parity what a page, or the OS, can observe matches stock Firefox
|
|
|
|
Each guard is a standalone script exiting 0 or 1. Policy allows zero failures.
|
|
Every guard belongs to exactly one group (ci/tests checks this), so a new one
|
|
cannot be left out of CI.
|
|
|
|
Run:
|
|
python3 -m ci.run_patch_guards --binary /path/to/camoufox-bin
|
|
python3 -m ci.run_patch_guards --binary /path/to/camoufox-bin --group automation
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
from . import results as evidence
|
|
from ._util import EVIDENCE_DIR, REPO_ROOT, log, run
|
|
|
|
GUARD_DIR = REPO_ROOT / "tests" / "patches"
|
|
|
|
|
|
GROUPS: Dict[str, Tuple[str, ...]] = {
|
|
"spoofing": (
|
|
"animation-timing",
|
|
"fingerprint-setter-seal",
|
|
"locale-explicit-names",
|
|
"media-devices-coherence",
|
|
"spoofed-voice-speaks",
|
|
"startup-prefs",
|
|
"system-ui-font-spoofing",
|
|
"touchscreen-digitizer",
|
|
"worker-config-reads",
|
|
),
|
|
"automation": (
|
|
"addons-install-once",
|
|
"force-scope-access",
|
|
"humanize-edge-deadlock",
|
|
"humanize-mouse-trajectory",
|
|
"humanize-pacing",
|
|
"input-ack-backstop",
|
|
"isolated-evaluate",
|
|
"main-world-eval",
|
|
"main-world-init-script",
|
|
"mouse-boundary-sweep",
|
|
"near-edge-mouse-deadlock",
|
|
"noop-mousemove-deadlock",
|
|
"trusted-events",
|
|
"visible-automation-cues",
|
|
),
|
|
"parity": (
|
|
"contentaccessible-parity",
|
|
"gfx-probes-packaged",
|
|
"hardware-acceleration-policy",
|
|
"popup-blocker-parity",
|
|
"search-service-init",
|
|
"stock-parity-probes",
|
|
"viewport-no-rdm",
|
|
"windows-exe-manifest",
|
|
),
|
|
}
|
|
|
|
|
|
def gate_name(group: Optional[str]) -> str:
|
|
"""patch_guards for the whole set, patch_guards_<group> for one group."""
|
|
return f"patch_guards_{group}" if group else "patch_guards"
|
|
|
|
|
|
def guards() -> List[Path]:
|
|
"""Every guard script. helpers.py is a library, not a guard."""
|
|
return sorted(p for p in GUARD_DIR.glob("*.py") if p.name != "helpers.py")
|
|
|
|
|
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--binary", type=Path)
|
|
parser.add_argument("--evidence-dir", type=Path, default=EVIDENCE_DIR)
|
|
parser.add_argument("--timeout", type=int, default=600, help="per guard")
|
|
parser.add_argument("--only", nargs="*", help="run only these guard names")
|
|
parser.add_argument("--group", choices=sorted(GROUPS), help="run one group (default: all)")
|
|
args = parser.parse_args(argv)
|
|
|
|
from ._pytest import built_binary
|
|
|
|
result = evidence.GateResult(gate=gate_name(args.group))
|
|
binary = args.binary or built_binary()
|
|
if not binary.exists():
|
|
result.note(f"no built binary at {binary}")
|
|
result.finish(evidence.ERROR).save(args.evidence_dir)
|
|
return 1
|
|
|
|
env = {
|
|
"CAMOUFOX_EXECUTABLE_PATH": str(binary),
|
|
# The guards drive the browser through the Python package, which resolves
|
|
# the binary from this variable rather than a packaged install.
|
|
"PYTHONPATH": os.pathsep.join(
|
|
filter(None, [str(REPO_ROOT / "pythonlib"), os.environ.get("PYTHONPATH", "")])
|
|
),
|
|
}
|
|
|
|
selected = [
|
|
g for g in guards()
|
|
if (not args.only or g.stem in args.only)
|
|
and (not args.group or g.stem in GROUPS[args.group])
|
|
]
|
|
if not selected:
|
|
result.note("no guards found -- tests/patches/ is empty or the filter matched nothing")
|
|
result.finish(evidence.ERROR).save(args.evidence_dir)
|
|
return 1
|
|
|
|
failed: List[str] = []
|
|
for guard in selected:
|
|
proc = run([sys.executable, str(guard)], cwd=REPO_ROOT, env=env, timeout=args.timeout)
|
|
outcome = evidence.PASS if proc.ok else evidence.FAIL
|
|
result.record(f"patches/{guard.name}", outcome)
|
|
if not proc.ok:
|
|
failed.append(guard.name)
|
|
tail = proc.combined().strip().splitlines()[-6:]
|
|
result.note(f"{guard.name} failed ({proc.code}): " + " | ".join(t.strip() for t in tail))
|
|
else:
|
|
log(f" ✓ {guard.name}")
|
|
|
|
passed = len(selected) - len(failed)
|
|
result.note(f"{passed}/{len(selected)} guards passed")
|
|
status = evidence.PASS if not failed else evidence.FAIL
|
|
result.finish(status).save(args.evidence_dir)
|
|
return 0 if status == evidence.PASS else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|