fix(panel): a rule that reads as the strictest line in a template now enforces something - #68
Merged
Merged
Conversation
… enforces something `parseRules` split the rule string on `|` before it looked for `regex:`, so `required|regex:/^(paper|purpur)$/` was torn into `regex:/^(paper` and `purpur)$/`. The first would not compile, and the catch around it returned **true** — accepting every value. The second became an unrecognised rule name and was dropped. The strictest-looking line in a template therefore enforced nothing, and said nothing about it. Pterodactyl eggs write alternations constantly, so every imported template carrying one has been running unvalidated. The file's own comment said `regex:` "keeps its argument whole: it contains commas, pipes and colons that must not be split" — true of commas and colons, false of the pipe, which is the character an alternation is made of. Both halves are fixed, because either alone leaves a hole. The rule string is now scanned rather than split: a `regex:/` is consumed through its matching closing delimiter and its flags, honouring backslash escapes and character classes, where a `/` or a `|` inside `[...]` is an ordinary character. And an expression that will not compile refuses the value instead of accepting it, blaming the template rather than the value, with an error log naming the variable and the rule. The refusal fires even when there is no value to refuse. A variable that is normally left empty would otherwise keep its broken rule secret until the day somebody finally filled it in. Two consequences worth stating rather than discovering: A delimited regex containing a pipe now *works*, which is a tightening — anyone who imported an egg with an alternation has had that variable unvalidated, and a value saved earlier may be refused on the next save. That is the intended direction and the one visible break. An uppercase flag is no longer swallowed as a flag. Taking it would keep the rule whole and have the check read `/^(a|b)$/I` as a bare pattern — which compiles, matches almost nothing, and refuses every value while blaming the value. Stopping at the delimiter lets the fragment fail to compile, which is the loud answer. One case is knowingly left: an unterminated `regex:/` that borrows a `/` from a later rule, where the region between them happens to compile, is indistinguishable from a correctly delimited expression. Catching it would mean refusing any expression spanning a `|` followed by something that looks like a rule name, which breaks the legitimate `regex:/^(a|max:5)$/` to rescue a template that is already malformed. The bundled catalogue is unaffected: every regex it ships is a pipe-free character class, written that way to work around this very bug. Factorio's comment explaining the workaround is corrected, so nobody keeps coding around a bug that is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QL3QL3ReEa9Sk68mxJW6Fu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
parseRulessplit the rule string on|before it looked forregex:. So a Pterodactyl-shaped rule:was torn into
regex:/^(paperandpurpur)$/. The first will not compile, and thecatcharound it returnedtrue— accepting every value. The second became an unrecognised rule name and was dropped.The strictest-looking line in a template enforced nothing, and said nothing about it. Pterodactyl eggs write alternations constantly, so every imported template carrying one has been running with that variable unvalidated.
The file's own comment said
regex:"keeps its argument whole: it contains commas, pipes and colons that must not be split" — true of commas and colons, false of the pipe, which is the character an alternation is made of.Both halves, because either alone leaves a hole
Scan, don't split. A
regex:/is consumed through its matching closing delimiter and its flags, honouring backslash escapes and character classes — a/or a|inside[...]is an ordinary character. Everything else still cuts on the pipe.An expression that will not compile refuses the value, blaming the template rather than the value, with an error log naming the variable and the rule. The old fail-open is what turned the tearing into silence.
The refusal fires even when there is no value to refuse:
validateValueshort-circuits on an empty value when norequiredrule stands beside it, so a variable normally left empty would otherwise keep its broken rule secret until the day somebody finally filled it in. The check now runs on that path for its log line alone.Consequences, stated rather than discovered
A delimited regex containing a pipe now works — and that is a tightening. Anyone who imported an egg with an alternation has had that variable unvalidated; a value they saved earlier may be refused on the next save. Intended direction, and the one visible break.
Rules written after a delimited regex now apply.
regex:/^(a|b)$/|max:10used to losemax:10entirely.An uppercase flag is no longer swallowed. Taking it looks more forgiving and is worse: it keeps the rule whole, and the check then reads
/^(a|b)$/Ias a bare pattern — which compiles, matches almost nothing, and refuses every value while blaming the value. Stopping at the delimiter lets the fragment fail to compile, which is the loud answer.One case knowingly left open
An unterminated
regex:/that borrows a/from a later rule, where the region between them happens to compile, is indistinguishable from a correctly delimited expression —regex:/^(a|b)$|in:x/|max:5reads as one valid regex and quietly swallowsin:x.Catching it would mean refusing any expression that spans a
|followed by something resembling a rule name, which breaks the legitimateregex:/^(a|max:5)$/in order to rescue a template that is already malformed. Trading a malformed-only widening for a well-formed-only breakage is the wrong way round. It stays, and it is written down in the code.Verification
split('|')appears nowhere else: the importer passes the rule string through verbatim, the front end only prints it, andvalidateValuehas one production caller.lint,typecheck,test,format:checkgreen — 1307 tests, the rules spec alone going from 24 to 85.Also
Factorio's
FACTORIO_VERSIONcomment explained at length that its expression is a character class because an alternation would be torn and would then accept everything. That is no longer true, and left standing it would keep future template authors working around a bug that is gone. The rule string is unchanged; only the reasoning is.Found, not fixed
ServersService.resolveVariablesvalidates nothing at all — server creation takes the caller's value for every editable variable and writes it straight through without reading the rule string. Admin-only, and every later edit does validate, so it is a follow-up rather than part of this.🤖 Generated with Claude Code
https://claude.ai/code/session_01QL3QL3ReEa9Sk68mxJW6Fu