webcheck: reject doubled unicode escapes in index.html #568

Merged
Cordy merged 5 commits from fix-escguard into main 2026-09-16 23:19:13 +00:00
Owner

The static guard proposed on #552. Test-only — no behaviour change, so no CHANGELOG entry and no release.

What it catches

"·" is a middle dot. "\\u00b7" is a backslash followed by the five characters u00b7, and the browser renders exactly that. The two are nearly identical in a diff and identical in a code review at speed.

This has bitten the same feature four times, three of which reached a release:

v0.6.206 [._\\s-] initials split on the letter "s" instead of whitespace — Fall 1 → "FA"
v0.6.208 computed i18n key caught by TestI18nKeysDefined
v0.6.210 \\u00b7 printed as literal text under every directory account
v0.6.211 same mangling, in the tooling caught by a count assertion, pre-write

Only the second had an automated net under it. The third was caught by a human reading the screen — which is not a control.

Why it keeps happening

It is structural, not careless. index.html is edited by one-shot scripts that pass replacement text through a shell heredoc and a Python string on its way into JavaScript. Every layer has an opinion about backslashes and only the running browser reveals which one won. Writing the fix correctly is not enough; the fix has to survive the transport.

The rule

No \\uXXXX anywhere in index.html. The one legitimate use — a string later handed to new RegExp, where the regex engine performs the second decoding — is allowed with an explicit line marker:

const re = new RegExp("\\u00b7"); // webcheck:allow-double-escape

A marker rather than "outside a regex literal" on purpose: correctly finding regex literals in JavaScript means resolving the / ambiguity, which is real parsing work and would itself be a source of bugs. A greppable, reviewable opt-out is the honest trade — and it makes the rare legitimate case visible rather than indistinguishable from the defect.

doubledEscapes reports file line number and matched text for every occurrence, because a bare count is useless in a 12,000-line page.

On the test data

The fixtures use Go raw strings, so backslashes are taken literally and the test file cannot fall to the very problem it exists to catch. Worth stating, given the subject.

The detector itself is emitted by a Python heredoc — the exact layering under scrutiny — so its regex is assembled from chr(92) rather than typed, and no line in that generator contains a backslash Python could reinterpret.

Verification

Red witnessed first: undefined: doubledEscapes at three call sites, build failed. Then 13 detector cases (correct escape left alone, doubled form caught, real character ignored, multiple per line and per file, uppercase hex, a doubled backslash that is not an escape left alone, the marker working and scoped to its own line, empty input), a line-number assertion, and the real-file check over index.html — which passes, so this lands as a clean baseline rather than a backlog. Full Go suite and all web tests green.

The static guard proposed on #552. Test-only — no behaviour change, so no CHANGELOG entry and no release. ## What it catches `"·"` is a middle dot. `"\\u00b7"` is a backslash followed by the five characters `u00b7`, and the browser renders exactly that. The two are nearly identical in a diff and identical in a code review at speed. This has bitten the same feature four times, three of which reached a release: | | | | |---|---|---| | v0.6.206 | `[._\\s-]` | initials split on the letter "s" instead of whitespace — `Fall 1` → "FA" | | v0.6.208 | computed i18n key | caught by `TestI18nKeysDefined` | | v0.6.210 | `\\u00b7` | printed as literal text under every directory account | | v0.6.211 | same mangling, in the tooling | caught by a count assertion, pre-write | Only the second had an automated net under it. The third was caught by a human reading the screen — which is not a control. ## Why it keeps happening It is structural, not careless. `index.html` is edited by one-shot scripts that pass replacement text through a shell heredoc **and** a Python string on its way into JavaScript. Every layer has an opinion about backslashes and only the running browser reveals which one won. Writing the fix correctly is not enough; the fix has to survive the transport. ## The rule No `\\uXXXX` anywhere in `index.html`. The one legitimate use — a string later handed to `new RegExp`, where the regex engine performs the second decoding — is allowed with an explicit line marker: ```js const re = new RegExp("\\u00b7"); // webcheck:allow-double-escape ``` A marker rather than "outside a regex literal" on purpose: correctly finding regex literals in JavaScript means resolving the `/` ambiguity, which is real parsing work and would itself be a source of bugs. A greppable, reviewable opt-out is the honest trade — and it makes the rare legitimate case *visible* rather than indistinguishable from the defect. `doubledEscapes` reports **file line number and matched text** for every occurrence, because a bare count is useless in a 12,000-line page. ## On the test data The fixtures use Go raw strings, so backslashes are taken literally and the test file cannot fall to the very problem it exists to catch. Worth stating, given the subject. The detector itself is emitted by a Python heredoc — the exact layering under scrutiny — so its regex is assembled from `chr(92)` rather than typed, and no line in that generator contains a backslash Python could reinterpret. ## Verification Red witnessed first: `undefined: doubledEscapes` at three call sites, build failed. Then 13 detector cases (correct escape left alone, doubled form caught, real character ignored, multiple per line and per file, uppercase hex, a doubled backslash that is *not* an escape left alone, the marker working and scoped to its own line, empty input), a line-number assertion, and the real-file check over `index.html` — which passes, so this lands as a clean baseline rather than a backlog. Full Go suite and all web tests green.
Cordy merged commit ba3140a814 into main 2026-09-16 23:19:13 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#568
No description provided.