mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-04 16:00:21 +00:00
The browser has not noised the canvas since #528, and no patch reads canvas:seed (#721). The launcher still drew one on every launch and sent it through CAMOU_CONFIG, and NewContext called a setCanvasSeed that does not exist. They no longer do. For users this changes nothing on any browser since #528: the value was ignored. A config that still passes canvas:seed gets the usual "Skipping unknown patch" notice instead of silence. On a browser from before #528, the launcher no longer turns canvas noise on, which is the behaviour #528 chose. The same audit found more keys declared in settings/properties.json that no patch or Juggler file reads, so setting them did nothing: - canvas:aaOffset, canvas:aaCapOffset - memorysaver, pdfViewerEnabled, webrtc:localipv4/6 - navigator.onLine, navigator.cookieEnabled, navigator.languages - navigator.appCodeName, appName, product, productSub. Firefox reports these constants itself, so fpgen.yml no longer maps them. - webGl:parameters:blockIfNotDefined and its WebGL2 twin test_config_schema now checks this direction too: every declared key must be read by the browser, unless it is listed with a reason. Three are listed: locale:script and navigator.doNotTrack, which the launcher applies itself, and navigator.buildID (#780). The build-tester grading followed the same wrong premise. It tracked canvas collisions as an unfixed per-context leak. A canvas that is rendered rather than noised follows the fonts and GPU, as it does on real machines, so canvas collisions are now counted with the other device-level values. The tribal rule that recorded it as an open question is now a settled one, canvas-is-not-noised, with an automated check. Closes #721. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
5.5 KiB
Python
148 lines
5.5 KiB
Python
"""
|
|
Guard: every config key the browser reads must be declared in the schema.
|
|
|
|
Regression guard for the `media:spoof_codecs` gap in PR #562. The C++ side read
|
|
the key via MaskConfig::GetBool("media:spoof_codecs"), but nothing ever added it
|
|
to settings/properties.json, and validate_config() drops any key it does not
|
|
recognise -- printing "Skipping unknown patch media:spoof_codecs" and moving on.
|
|
The documented usage,
|
|
|
|
AsyncCamoufox(config={"media:spoof_codecs": True})
|
|
|
|
therefore did nothing at all: the key never reached the browser, so the feature
|
|
could not be switched on through the supported path.
|
|
|
|
This is the read-but-undeclared direction of a mistake the project has made
|
|
before in the other direction -- canvas:seed (#721) and navigator.maxTouchPoints
|
|
(#696) were both declared in the schema while nothing consumed them. A patch and
|
|
a schema entry are two halves of one change; this test fails the build when only
|
|
one half lands.
|
|
|
|
Run with:
|
|
cd pythonlib && python -m pytest tests/test_config_schema.py -v
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO = Path(__file__).resolve().parents[2]
|
|
PROPERTIES = REPO / "settings" / "properties.json"
|
|
|
|
# MaskConfig::GetBool("k") / GetString("k") / GetUint32("k") / HasKey("k") ...
|
|
MASKCONFIG_READ = re.compile(r'MaskConfig::(?:Get|Has)\w*\(\s*"([^"]+)"')
|
|
|
|
# Keys read through a variable or built at runtime rather than a string literal.
|
|
# Add here (with a reason) only when the read genuinely cannot name its key.
|
|
ALLOWED_UNDECLARED: set = set()
|
|
|
|
# Declared keys the browser never reads, each with the reason it is declared
|
|
# anyway. Everything else in properties.json must be read by a patch or by
|
|
# Juggler: a key nothing reads does nothing, silently.
|
|
NOT_READ_BY_THE_BROWSER = {
|
|
"locale:script": "the launcher joins it with locale:language/region into the UI locale",
|
|
"navigator.doNotTrack": "the launcher applies it as privacy.donottrackheader.enabled (#760)",
|
|
"navigator.buildID": "declared ahead of the patch that reads it (#780)",
|
|
}
|
|
|
|
# How Juggler and the patches name a key: a quoted string literal.
|
|
QUOTED = '"{key}"', "'{key}'"
|
|
|
|
|
|
def _sources():
|
|
for pattern in ("patches/**/*.patch", "additions/**/*"):
|
|
for path in REPO.glob(pattern):
|
|
if path.is_file():
|
|
yield path
|
|
|
|
|
|
def _declared_keys() -> set:
|
|
return {entry["property"] for entry in json.loads(PROPERTIES.read_text())}
|
|
|
|
|
|
def _keys_read() -> dict:
|
|
"""Map config key -> sorted list of files that read it."""
|
|
found: dict = {}
|
|
for path in _sources():
|
|
try:
|
|
text = path.read_text(errors="ignore")
|
|
except OSError:
|
|
continue
|
|
for match in MASKCONFIG_READ.finditer(text):
|
|
found.setdefault(match.group(1), set()).add(
|
|
str(path.relative_to(REPO))
|
|
)
|
|
return {k: sorted(v) for k, v in found.items()}
|
|
|
|
|
|
def test_properties_json_is_wellformed():
|
|
entries = json.loads(PROPERTIES.read_text())
|
|
assert entries, "settings/properties.json is empty"
|
|
for entry in entries:
|
|
assert "property" in entry and "type" in entry, f"malformed entry: {entry}"
|
|
|
|
|
|
def test_every_key_the_browser_reads_is_declared():
|
|
declared = _declared_keys()
|
|
read = _keys_read()
|
|
assert read, "found no MaskConfig reads -- the scanner regexp has gone stale"
|
|
|
|
undeclared = {
|
|
key: files
|
|
for key, files in read.items()
|
|
if key not in declared and key not in ALLOWED_UNDECLARED
|
|
}
|
|
if undeclared:
|
|
lines = [
|
|
"config keys are read by the browser but missing from "
|
|
"settings/properties.json,",
|
|
"so validate_config() silently drops them and the feature cannot be "
|
|
"enabled through the Python API:",
|
|
"",
|
|
]
|
|
for key, files in sorted(undeclared.items()):
|
|
lines.append(f" {key}")
|
|
for f in files:
|
|
lines.append(f" read in {f}")
|
|
pytest.fail("\n".join(lines))
|
|
|
|
|
|
def test_every_declared_key_is_read_by_the_browser():
|
|
"""The other direction: canvas:seed (#721) was declared, generated by both
|
|
launchers and sent on every launch for three releases after the patch that
|
|
read it was removed (#528)."""
|
|
text = "".join(path.read_text(errors="ignore") for path in _sources())
|
|
unread = sorted(
|
|
key
|
|
for key in _declared_keys()
|
|
if key not in NOT_READ_BY_THE_BROWSER
|
|
and not any(form.format(key=key) in text for form in QUOTED)
|
|
)
|
|
assert not unread, (
|
|
"settings/properties.json declares keys that no patch or Juggler file "
|
|
f"reads, so setting them does nothing: {unread}. Remove them, or add them "
|
|
"to NOT_READ_BY_THE_BROWSER with the reason they are declared."
|
|
)
|
|
|
|
|
|
def test_keys_not_read_by_the_browser_are_really_unread():
|
|
"""An exemption must lapse once the browser starts reading the key."""
|
|
text = "".join(path.read_text(errors="ignore") for path in _sources())
|
|
now_read = [
|
|
key for key in NOT_READ_BY_THE_BROWSER
|
|
if any(form.format(key=key) in text for form in QUOTED)
|
|
]
|
|
assert not now_read, f"remove from NOT_READ_BY_THE_BROWSER, the browser reads them now: {now_read}"
|
|
|
|
|
|
@pytest.mark.parametrize("key", ["media:spoof_codecs"])
|
|
def test_known_previously_missing_keys_stay_declared(key):
|
|
"""Pin the specific keys this guard was written for, so a schema edit that
|
|
drops one fails loudly here rather than only in the general scan above."""
|
|
assert key in _declared_keys(), (
|
|
f"{key} is read by the browser but is not declared in "
|
|
f"settings/properties.json -- see PR #562"
|
|
)
|