From ac90b35b499bd36b3872efae6c109087ad8c87f6 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 12 Oct 2026 12:51:21 +0200 Subject: [PATCH] guard.ts: cover every branch for a rule Go's regexp reads otherwise, and hold both matchers to one table (issue 539) Review 30 found guard.ts missing branches the forge protects: Go reads \d, \w, \pL, a class opening with ] or ^, and [[:digit:]] otherwise than JavaScript. A rule using any of them now covers every branch, so the guard refuses more than the forge, never less. test/rule-cases.json holds the forge's answers, read by the Go and TypeScript tests alike. --- modules/gitea/cmd/forge/forge_test.go | 39 ++-- modules/gitea/guard.ts | 23 ++- modules/gitea/test/guard.test.ts | 21 +- modules/gitea/test/rule-cases.json | 285 ++++++++++++++++++++++++++ 4 files changed, 336 insertions(+), 32 deletions(-) create mode 100644 modules/gitea/test/rule-cases.json diff --git a/modules/gitea/cmd/forge/forge_test.go b/modules/gitea/cmd/forge/forge_test.go index bee66856..7d9464df 100644 --- a/modules/gitea/cmd/forge/forge_test.go +++ b/modules/gitea/cmd/forge/forge_test.go @@ -398,24 +398,27 @@ func TestACommitAskThatCannotBeReadIsRefused(t *testing.T) { } } -func TestRuleCoversIsTheGuardsRule(t *testing.T) { - for _, c := range []struct { - rule, branch string - want bool - }{ - {"main", "main", true}, {"main", "mainx", false}, {"release/*", "release/1.0", true}, - {"release/*", "release/a/b", false}, {"release/**", "release/a/b", true}, {"v?", "v1", true}, - {"a.b", "axb", false}, - // As the forge decides it (novox/hq issue 539): classes, alternatives, escapes, a plain name in any case. - {"release-[ab]", "release-a", true}, {"release-[ab]", "release-c", false}, {"release-[!ab]", "release-c", true}, - {"v[0-9].x", "v7.x", true}, {"v[0-9].x", "vA.x", false}, {"{main,master}", "master", true}, - {"{main,master}", "mainline", false}, {"rel/{a,b}/*", "rel/b/1", true}, {"rel/{a,b}/*", "rel/c/1", false}, - {"Main", "main", true}, {"main", "MAIN", true}, {"Feat/*", "feat/x", false}, {`a\*`, "a*", true}, - {`a\*`, "ab", false}, {"a+b", "a+b", true}, {"a+b", "aab", false}, {"{open", "{open", true}, - {"[open", "[open", true}, {"[open", "o", false}, - } { - if got := ruleCovers(c.rule, c.branch); got != c.want { - t.Errorf("%s covers %s: %v", c.rule, c.branch, got) +// The forge's own answers (Gitea 1.27's Match), one table for this bundle and guard.ts alike (novox/hq issue 539). +// `forge` is the forge's answer, which this port must give; `guard` is where guard.ts deliberately covers more. +func TestRuleCoversAnswersAsTheForgeDoes(t *testing.T) { + raw, err := os.ReadFile("../../test/rule-cases.json") + if err != nil { + t.Fatal(err) + } + var cases []struct { + Rule string `json:"rule"` + Branch string `json:"branch"` + Forge bool `json:"forge"` + } + if err := json.Unmarshal(raw, &cases); err != nil { + t.Fatal(err) + } + if len(cases) < 100 { + t.Fatalf("only %d cases", len(cases)) + } + for _, c := range cases { + if got := ruleCovers(c.Rule, c.Branch); got != c.Forge { + t.Errorf("%q covers %q: %v, the forge says %v", c.Rule, c.Branch, got, c.Forge) } } } diff --git a/modules/gitea/guard.ts b/modules/gitea/guard.ts index be34ad0d..daee4265 100644 --- a/modules/gitea/guard.ts +++ b/modules/gitea/guard.ts @@ -170,20 +170,37 @@ export function judgeApiCall(method: string, rawPath: string, body: unknown): Ap * covers the branch of that name in any case; any other name is a glob with `/` as its separator — `*` any run * within a segment, `**` any run across them, `?` one character of a segment, `[abc]`, `[a-z]` and `[!abc]` one * character, `{a,b}` either, `\` the next character itself — matched case-sensitively; a glob the forge cannot - * compile covers its own text exactly. The forge bundle's ruleCovers (cmd/forge/rules.go) is the same rule. + * compile covers its own text exactly. The forge bundle's ruleCovers (cmd/forge/rules.go) is the same rule, and + * both are held to one table of answers (test/rule-cases.json). + * + * The forge hands the compiled glob to Go's regexp, which reads some things JavaScript's reads otherwise: a + * backslash before a letter or digit (`\d`, `\w`, `\pL`, `\b`, `\A` are classes or anchors there), a class + * that opens with `]` (`[]a]` holds `]` and `a` there, an empty class here) or with `^` (a negation there), + * and a POSIX class (`[[:digit:]]`). + * A rule using any of them is taken to cover every branch: refusing a write the forge would take is safe, + * taking one it guards is not. */ export function ruleCovers(rule: string, branch: string): boolean { if (!/[*?\\[\]{}]/.test(rule)) return rule.toLowerCase() === branch.toLowerCase(); + if (goReadsOtherwise(rule)) return true; const re = globRegExp(rule); return re ? re.test(branch) : rule === branch; } +/** Whether a glob holds what Go's regexp reads otherwise than JavaScript's (see ruleCovers): fail closed. */ +function goReadsOtherwise(rule: string): boolean { + return /\\[\p{L}\p{N}]/u.test(rule) || /\[!?[\]^]/.test(rule) || /\[[^\]]*\[:/.test(rule); +} + /** The forge's glob as a regular expression, compiled as its modules/glob compiles it with `/` as separator. */ function globRegExp(glob: string): RegExp | null { const g = Array.from(glob); let out = ""; let pos = 0; + // A character as itself: escaped when it is syntax to a regular expression, and only then — the `u` flag + // refuses any other escape, and `-` may be escaped only inside a class. const lit = (c: string) => c.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&"); + const litInClass = (c: string) => c.replace(/[\]\\[^-]/g, "\\$&"); const chars = (): boolean => { out += "["; if (g[pos] === "!") { @@ -198,8 +215,8 @@ function globRegExp(glob: string): RegExp | null { } if (c === "\\") { if (pos >= g.length) return false; - out += "\\" + g[pos++]; - } else out += c === "[" ? "\\[" : c; + out += litInClass(g[pos++]); + } else out += c === "[" || c === "^" ? "\\" + c : c; } return false; }; diff --git a/modules/gitea/test/guard.test.ts b/modules/gitea/test/guard.test.ts index 8c08a6a3..3e069b63 100644 --- a/modules/gitea/test/guard.test.ts +++ b/modules/gitea/test/guard.test.ts @@ -7,6 +7,7 @@ import { test, after, beforeEach } from "node:test"; import assert from "node:assert/strict"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; +import { readFileSync } from "node:fs"; import { GiteaClient } from "../dist/client.js"; import { ConfiguredToken } from "../dist/token.js"; @@ -147,17 +148,15 @@ test("gitea_api refuses a file written to the default branch or one a rule guard }); test("a protection rule covers a branch as the forge decides it: classes, alternatives, a plain name in any case (issue 539)", async () => { - const cases: Array<[string, string, boolean]> = [ - ["main", "main", true], ["main", "mainx", false], ["release/*", "release/1.0", true], ["release/*", "release/a/b", false], - ["release/**", "release/a/b", true], ["v?", "v1", true], ["a.b", "axb", false], - ["release-[ab]", "release-a", true], ["release-[ab]", "release-c", false], ["release-[!ab]", "release-c", true], - ["v[0-9].x", "v7.x", true], ["v[0-9].x", "vA.x", false], ["{main,master}", "master", true], - ["{main,master}", "mainline", false], ["rel/{a,b}/*", "rel/b/1", true], ["rel/{a,b}/*", "rel/c/1", false], - ["Main", "main", true], ["main", "MAIN", true], ["Feat/*", "feat/x", false], ["a\\*", "a*", true], - ["a\\*", "ab", false], ["a+b", "a+b", true], ["a+b", "aab", false], ["{open", "{open", true], - ["[open", "[open", true], ["[open", "o", false], - ]; - for (const [rule, branch, want] of cases) assert.equal(ruleCovers(rule, branch), want, `${rule} covers ${branch}`); + // One table with the forge bundle (cmd/forge/rules.go): `forge` is the forge's answer; `guard` is where this + // guard deliberately covers more — a rule Go's regexp reads otherwise than JavaScript's — and never less. + const cases = JSON.parse(readFileSync(new URL("./rule-cases.json", import.meta.url), "utf8")) as + Array<{ rule: string; branch: string; forge: boolean; guard?: boolean }>; + assert.ok(cases.length >= 100); + for (const c of cases) { + assert.ok(c.guard === undefined || (c.guard && !c.forge), `${c.rule}: the guard may only cover more than the forge`); + assert.equal(ruleCovers(c.rule, c.branch), c.guard ?? c.forge, `${c.rule} covers ${c.branch}`); + } const put = (branch: string) => tool("gitea_api").run({ path: "/repos/novox/mesh-catalog/contents/README.md", method: "PUT", body: { content: "e30=", sha: "x", branch } }); state.rules.push({ rule_name: "release-[ab]" }, { rule_name: "{stable,lts}/*" }); diff --git a/modules/gitea/test/rule-cases.json b/modules/gitea/test/rule-cases.json new file mode 100644 index 00000000..25f84d38 --- /dev/null +++ b/modules/gitea/test/rule-cases.json @@ -0,0 +1,285 @@ +[ + {"rule":"main","branch":"main","forge":true,"why":"a plain name covers its branch"}, + {"rule":"main","branch":"mainx","forge":false}, + {"rule":"main","branch":"MAIN","forge":true,"why":"a plain name covers in any case (EqualFold)"}, + {"rule":"Main","branch":"main","forge":true}, + {"rule":"release/*","branch":"release/1.0","forge":true,"why":"* stays within a segment"}, + {"rule":"release/*","branch":"release/a/b","forge":false}, + {"rule":"release/**","branch":"release/a/b","forge":true,"why":"** crosses segments"}, + {"rule":"v?","branch":"v1","forge":true}, + {"rule":"v?","branch":"v/","forge":false,"why":"? is not the separator"}, + {"rule":"a.b","branch":"axb","forge":false,"why":". is itself"}, + {"rule":"Feat/*","branch":"feat/x","forge":false,"why":"a glob is matched with case"}, + {"rule":"release-[ab]","branch":"release-a","forge":true,"why":"a class"}, + {"rule":"release-[ab]","branch":"release-c","forge":false}, + {"rule":"release-[!ab]","branch":"release-c","forge":true,"why":"a negated class"}, + {"rule":"release-[!ab]","branch":"release-a","forge":false}, + {"rule":"v[0-9].x","branch":"v7.x","forge":true,"why":"a range"}, + {"rule":"v[0-9].x","branch":"vA.x","forge":false}, + {"rule":"{main,master}","branch":"master","forge":true,"why":"alternatives"}, + {"rule":"{main,master}","branch":"mainline","forge":false}, + {"rule":"rel/{a,b}/*","branch":"rel/b/1","forge":true}, + {"rule":"rel/{a,b}/*","branch":"rel/c/1","forge":false}, + {"rule":"a\\*","branch":"a*","forge":true,"why":"an escape"}, + {"rule":"a\\*","branch":"ab","forge":false}, + {"rule":"a+b","branch":"a+b","forge":true,"why":"+ is itself"}, + {"rule":"a+b","branch":"aab","forge":false}, + {"rule":"{open","branch":"{open","forge":true,"why":"a glob the forge cannot compile covers its own text"}, + {"rule":"[open","branch":"[open","forge":true}, + {"rule":"[open","branch":"o","forge":false}, + {"rule":"release-\\d","branch":"release-1","forge":true,"why":"Go reads \\d as a digit"}, + {"rule":"release-\\d","branch":"release-d","forge":false,"guard":true}, + {"rule":"v\\w*","branch":"v2","forge":true}, + {"rule":"main\\s","branch":"main","forge":false,"guard":true}, + {"rule":"[]a]","branch":"a","forge":true,"why":"a class opening with ] holds ]"}, + {"rule":"[]a]","branch":"]","forge":true}, + {"rule":"[]a]","branch":"b","forge":false,"guard":true}, + {"rule":"rel-[\\d]","branch":"rel-1","forge":true}, + {"rule":"v[\\pL]","branch":"va","forge":true,"why":"\\p is a Unicode class"}, + {"rule":"v\\pL","branch":"va","forge":true}, + {"rule":"[[:alpha:]]","branch":"a","forge":true,"why":"a POSIX class"}, + {"rule":"rel-[[:digit:]]","branch":"rel-1","forge":true}, + {"rule":"rel-[[:digit:]]","branch":"rel-x","forge":false,"guard":true}, + {"rule":"[^+]*","branch":"-[1","forge":true,"why":"a class opening with ^ is negated in Go"}, + {"rule":"d[^](","branch":"d[^](","forge":true}, + {"rule":"]?","branch":":,1},","forge":false}, + {"rule":"Ab,","branch":"Ab,","forge":true}, + {"rule":"\\:[}-|","branch":",{","forge":false}, + {"rule":"^,*.d::","branch":"/x","forge":false}, + {"rule":"","branch":"A*:","forge":false}, + {"rule":"!|A{","branch":":.{]","forge":false}, + {"rule":"","branch":"]éb","forge":false}, + {"rule":"/{)w","branch":"/{)w","forge":true}, + {"rule":"*^w]","branch":"}!.","forge":false}, + {"rule":"*),^?*d","branch":"*),^?*d","forge":true}, + {"rule":"A)^}!","branch":"","forge":false}, + {"rule":"}^d?","branch":"1\\.","forge":false}, + {"rule":"","branch":"","forge":true}, + {"rule":"$?[","branch":"$?[","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"+","branch":"+","forge":true}, + {"rule":"","branch":"},[","forge":false}, + {"rule":"xA[d:a)","branch":"]1.!bx","forge":false}, + {"rule":"^/}(","branch":"","forge":false}, + {"rule":"-|x.x1?","branch":"-|x.x1?","forge":true}, + {"rule":"x+)A]-A","branch":"x+)A]-A","forge":true}, + {"rule":"x*a","branch":"x*a","forge":true}, + {"rule":"$xb","branch":"}é","forge":false}, + {"rule":"wAx?/a,","branch":"","forge":false}, + {"rule":"[,","branch":"[,","forge":true}, + {"rule":"}!","branch":"}!","forge":true}, + {"rule":"*:|\\w","branch":"","forge":false,"guard":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"é","branch":"é","forge":true}, + {"rule":"..(","branch":"{\\{+","forge":false}, + {"rule":"*|{","branch":"1.}-","forge":false}, + {"rule":"!+,","branch":"}++-*","forge":false}, + {"rule":"x\\({","branch":"*","forge":false}, + {"rule":"","branch":"","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"?-{+-","branch":"?-{+-","forge":true}, + {"rule":"/-{é|$","branch":"b!.[","forge":false}, + {"rule":"a-+dxA","branch":"1AA,xA","forge":false}, + {"rule":"]|(","branch":"]|(","forge":true}, + {"rule":"}","branch":"{!x{d","forge":false}, + {"rule":"/{*](","branch":"!]","forge":false}, + {"rule":"($+{-}|","branch":"/","forge":false}, + {"rule":"(-b.-1}","branch":"*A*+,1","forge":false}, + {"rule":"1[d|)wa","branch":"+}[!}}","forge":false}, + {"rule":"w","branch":":.","forge":false}, + {"rule":"?}\\*","branch":"","forge":false}, + {"rule":"?$*","branch":"?$*","forge":true}, + {"rule":"","branch":"!/{béa","forge":false}, + {"rule":"","branch":"A1","forge":false}, + {"rule":"","branch":"","forge":true}, + {"rule":"!a$1/b{","branch":"!a$1/b{","forge":true}, + {"rule":":dww/|d","branch":":dww/|d","forge":true}, + {"rule":"!|*}","branch":"xéxé{b","forge":false}, + {"rule":"1é||","branch":"a+.","forge":false}, + {"rule":"/+],","branch":"!-\\[{é","forge":false}, + {"rule":"a}(","branch":"a}(","forge":true}, + {"rule":"|A?.","branch":"}.","forge":false}, + {"rule":"d$a^d","branch":"*{","forge":false}, + {"rule":"-ba$+A","branch":"-ba$+A","forge":true}, + {"rule":"-.|,?a!","branch":"-.|,?a!","forge":true}, + {"rule":"A","branch":"A","forge":true}, + {"rule":"w","branch":"w","forge":true}, + {"rule":".!1!+a","branch":".!1!+a","forge":true}, + {"rule":"+$w:}","branch":"+$w:}","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":")]","branch":")]","forge":true}, + {"rule":"a})^,}1","branch":"a})^,}1","forge":true}, + {"rule":":[:!:","branch":":[:!:","forge":true}, + {"rule":"w?w:]1!","branch":"\\.]-","forge":false}, + {"rule":"*$/","branch":"*$/","forge":true}, + {"rule":"é](","branch":"é](","forge":true}, + {"rule":"x$a/","branch":"x$a/","forge":true}, + {"rule":"*)b+[^","branch":"*)b+[^","forge":true}, + {"rule":"/]/?","branch":"/]/?","forge":true}, + {"rule":"{x,A","branch":"!Ab","forge":false}, + {"rule":"","branch":"","forge":true}, + {"rule":"-,","branch":"xé}A[.","forge":false}, + {"rule":"w)Ad","branch":"w)Ad","forge":true}, + {"rule":":??","branch":":??","forge":true}, + {"rule":"x/xé|","branch":"x/xé|","forge":true}, + {"rule":"","branch":"1!","forge":false}, + {"rule":"!![é[","branch":"x","forge":false}, + {"rule":"wb$","branch":"wb$","forge":true}, + {"rule":")","branch":")","forge":true}, + {"rule":"Aw:)(1","branch":"{b\\,","forge":false}, + {"rule":"","branch":"","forge":true}, + {"rule":"1*}?d","branch":"1*}?d","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"?-b.","branch":"?-b.","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"d","branch":",aé1","forge":false}, + {"rule":"+","branch":"+{","forge":false}, + {"rule":"]d","branch":"]}:/bA","forge":false}, + {"rule":"*?+:|","branch":"!é.","forge":false}, + {"rule":"}$d","branch":"}$d","forge":true}, + {"rule":"/","branch":"/","forge":true}, + {"rule":"a$+b)b","branch":"d}","forge":false}, + {"rule":"?$[/A","branch":"","forge":false}, + {"rule":"é){","branch":"é){","forge":true}, + {"rule":"$(d","branch":"{é}","forge":false}, + {"rule":"b(","branch":"b(","forge":true}, + {"rule":"^^+]","branch":"^^+]","forge":true}, + {"rule":",)","branch":",)","forge":true}, + {"rule":"d-.A-","branch":"d-.A-","forge":true}, + {"rule":"$","branch":"\\/","forge":false}, + {"rule":"}+[\\|\\$","branch":"x[!b","forge":false}, + {"rule":"(d1aé!é","branch":"A[-","forge":false}, + {"rule":"x","branch":"1","forge":false}, + {"rule":"x","branch":"dx","forge":false}, + {"rule":"]|)]","branch":"]|)]","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"[.*?","branch":"[.*?","forge":true}, + {"rule":"[?","branch":"[?","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"),/","branch":"a1,,1","forge":false}, + {"rule":"}+","branch":"}+","forge":true}, + {"rule":"$d\\,d","branch":"$d\\,d","forge":false}, + {"rule":"d1","branch":"d1","forge":true}, + {"rule":"-!^.:","branch":"-!^.:","forge":true}, + {"rule":"x","branch":"+].A11","forge":false}, + {"rule":"","branch":"-]","forge":false}, + {"rule":"}]}.","branch":"","forge":false}, + {"rule":"^{wx","branch":"]1a!","forge":false}, + {"rule":"^","branch":"*]x,","forge":false}, + {"rule":"(:x*x","branch":"","forge":false}, + {"rule":"|[/*a/","branch":"}","forge":false}, + {"rule":"","branch":"","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"1\\^","branch":"!é!x{","forge":false}, + {"rule":"(.b,AA+","branch":"(.b,AA+","forge":true}, + {"rule":"xx","branch":"","forge":false}, + {"rule":"d)^):","branch":"/}!","forge":false}, + {"rule":"|","branch":",é*]*[","forge":false}, + {"rule":"d*d:*1","branch":"\\b,","forge":false}, + {"rule":":]","branch":"[","forge":false}, + {"rule":"([|!\\^é","branch":"([|!\\^é","forge":true}, + {"rule":"$[w)x-*","branch":"","forge":false}, + {"rule":")","branch":")","forge":true}, + {"rule":".d^)?","branch":"x\\é","forge":false}, + {"rule":"1|{w","branch":"1|{w","forge":true}, + {"rule":"é$,(]$$","branch":"é$,(]$$","forge":true}, + {"rule":"/","branch":"/","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"+","branch":"","forge":false}, + {"rule":"]","branch":"","forge":false}, + {"rule":"+:a:","branch":"+:a:","forge":true}, + {"rule":"/*","branch":"d.}","forge":false}, + {"rule":"|/ba1-","branch":"|/ba1-","forge":true}, + {"rule":"","branch":"+","forge":false}, + {"rule":"A","branch":"A","forge":true}, + {"rule":",-!]","branch":",-!]","forge":true}, + {"rule":"$*d!)","branch":"","forge":false}, + {"rule":")é+x.d","branch":")é+x.d","forge":true}, + {"rule":"éa$\\[1","branch":"1/,","forge":false}, + {"rule":"}?1|]d","branch":"}?1|]d","forge":true}, + {"rule":"[","branch":"[","forge":true}, + {"rule":"-1.*+","branch":"-1.*+","forge":true}, + {"rule":"Aw|{","branch":"Aw|{","forge":true}, + {"rule":"adAAAa","branch":"A","forge":false}, + {"rule":"","branch":"","forge":true}, + {"rule":"d/^{:","branch":"d/^{:","forge":true}, + {"rule":"é:(,^","branch":"é:(,^","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"!ébé({+","branch":"!ébé({+","forge":true}, + {"rule":"x","branch":"x","forge":true}, + {"rule":"|","branch":",:*","forge":false}, + {"rule":"","branch":"","forge":true}, + {"rule":"x|","branch":"x|","forge":true}, + {"rule":"a","branch":"aaxéb,","forge":false}, + {"rule":"/w-),1é","branch":"/w-),1é","forge":true}, + {"rule":"|*)^,","branch":"|*)^,","forge":true}, + {"rule":"){é.!b.","branch":"a-}:","forge":false}, + {"rule":"(\\","branch":"(\\","forge":true}, + {"rule":".w(\\","branch":"+","forge":false}, + {"rule":"/d[","branch":"/d[","forge":true}, + {"rule":"(}$x!}","branch":"a,bb","forge":false}, + {"rule":"w{","branch":"w{","forge":true}, + {"rule":"[+a\\A(+","branch":"[+a\\A(+","forge":true}, + {"rule":"(1^{)bb","branch":"(1^{)bb","forge":true}, + {"rule":"^1","branch":"é-}","forge":false}, + {"rule":"*,+}1):","branch":"*,+}1):","forge":true}, + {"rule":"[A[(.x,","branch":"]db!*","forge":false}, + {"rule":"","branch":"Ad}","forge":false}, + {"rule":".","branch":".","forge":true}, + {"rule":"w!é{x","branch":"é.{","forge":false}, + {"rule":"$\\^","branch":"bx]+","forge":false}, + {"rule":"1)","branch":"+*d","forge":false}, + {"rule":"x)}/])","branch":"]","forge":false}, + {"rule":",a}.1}","branch":",a}.1}","forge":true}, + {"rule":"-a{(","branch":"-a{(","forge":true}, + {"rule":"1","branch":"1","forge":true}, + {"rule":"","branch":"","forge":true}, + {"rule":"}[é$","branch":"]d-!","forge":false}, + {"rule":"xé{é+?","branch":"xé{é+?","forge":true}, + {"rule":"/A1!","branch":"xAA\\::","forge":false}, + {"rule":"1][b-.(","branch":"{/-b![","forge":false}, + {"rule":"(d}(","branch":"(d}(","forge":true}, + {"rule":"{*}","branch":"!ébd","forge":true}, + {"rule":"()^^)","branch":"()^^)","forge":true}, + {"rule":"1(1A{$","branch":"*,[","forge":false}, + {"rule":"A:!^\\","branch":"A:!^\\","forge":true}, + {"rule":"-]:b]","branch":"-]:b]","forge":true}, + {"rule":",","branch":",","forge":true}, + {"rule":"[-:]","branch":"[-:]","forge":false}, + {"rule":"","branch":"é:b1:d","forge":false}, + {"rule":"1\\!]?d","branch":"","forge":false}, + {"rule":"","branch":"bb!]+","forge":false}, + {"rule":"/da","branch":"b]d[","forge":false}, + {"rule":",]","branch":",]","forge":true}, + {"rule":"[","branch":"[","forge":true}, + {"rule":"a]{A-","branch":"-+","forge":false}, + {"rule":"A?A^1","branch":"A?A^1","forge":true}, + {"rule":"\\-$","branch":"","forge":false}, + {"rule":"+","branch":"*[}[","forge":false}, + {"rule":",b?{dw","branch":",b?{dw","forge":true}, + {"rule":"d[$b1","branch":"/","forge":false}, + {"rule":"})|xAA","branch":"é,!","forge":false}, + {"rule":"]--","branch":"]--","forge":true}, + {"rule":"],/b","branch":"],/b","forge":true}, + {"rule":"/","branch":"/","forge":true}, + {"rule":"*aé","branch":"*aé","forge":true}, + {"rule":"{","branch":"{","forge":true}, + {"rule":":aé","branch":"","forge":false}, + {"rule":".\\}","branch":".\\}","forge":false}, + {"rule":"wA]*","branch":"\\]!d{+","forge":false}, + {"rule":"a/1x$/","branch":"[1!:/A","forge":false}, + {"rule":"[.wb}d.","branch":"-!*..a","forge":false}, + {"rule":".[{()a","branch":"+}","forge":false}, + {"rule":"]/!:,","branch":"{Aéa","forge":false}, + {"rule":"é{ba[-","branch":"],,1d/","forge":false}, + {"rule":"(w*","branch":"1}.!","forge":false}, + {"rule":"{?","branch":"1","forge":false}, + {"rule":")","branch":",+!","forge":false}, + {"rule":":","branch":"{/+A][","forge":false}, + {"rule":".1{)","branch":"a\\bA","forge":false}, + {"rule":"w-1+x1-","branch":"*]1}bb","forge":false}, + {"rule":"xb$$","branch":"éx:]-:","forge":false}, + {"rule":"++$!?*$","branch":"bA","forge":false}, + {"rule":"]{","branch":"[xd*x.","forge":false} +]