@wrongstack/primitives 0.317.2 → 0.319.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +939 -40
- package/dist/regex-ambiguity.d.ts +58 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,536 @@
|
|
|
1
|
+
// src/regex-ambiguity.ts
|
|
2
|
+
var MAX_CP = 1114111;
|
|
3
|
+
function complementOf(set) {
|
|
4
|
+
const out = [];
|
|
5
|
+
let next = 0;
|
|
6
|
+
for (const [lo, hi] of set) {
|
|
7
|
+
if (lo > next) out.push([next, lo - 1]);
|
|
8
|
+
next = hi + 1;
|
|
9
|
+
}
|
|
10
|
+
if (next <= MAX_CP) out.push([next, MAX_CP]);
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
function intersect(a, b) {
|
|
14
|
+
const out = [];
|
|
15
|
+
let i = 0;
|
|
16
|
+
let j = 0;
|
|
17
|
+
while (i < a.length && j < b.length) {
|
|
18
|
+
const lo = Math.max(a[i][0], b[j][0]);
|
|
19
|
+
const hi = Math.min(a[i][1], b[j][1]);
|
|
20
|
+
if (lo <= hi) out.push([lo, hi]);
|
|
21
|
+
if (a[i][1] < b[j][1]) i++;
|
|
22
|
+
else j++;
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function anyMember(set) {
|
|
27
|
+
return set[0][0];
|
|
28
|
+
}
|
|
29
|
+
var WORD = [
|
|
30
|
+
[48, 57],
|
|
31
|
+
[65, 90],
|
|
32
|
+
[95, 95],
|
|
33
|
+
[97, 122]
|
|
34
|
+
];
|
|
35
|
+
var DIGIT = [[48, 57]];
|
|
36
|
+
var SPACE = [
|
|
37
|
+
[9, 13],
|
|
38
|
+
[32, 32]
|
|
39
|
+
];
|
|
40
|
+
var NAMED_SETS = {
|
|
41
|
+
w: WORD,
|
|
42
|
+
W: complementOf(WORD),
|
|
43
|
+
d: DIGIT,
|
|
44
|
+
D: complementOf(DIGIT),
|
|
45
|
+
s: SPACE,
|
|
46
|
+
S: complementOf(SPACE)
|
|
47
|
+
};
|
|
48
|
+
var DOT = [
|
|
49
|
+
[0, 9],
|
|
50
|
+
[11, MAX_CP]
|
|
51
|
+
];
|
|
52
|
+
var CHECKER_BUDGET = 6e4;
|
|
53
|
+
var Budget = class {
|
|
54
|
+
left = CHECKER_BUDGET;
|
|
55
|
+
spend(n = 1) {
|
|
56
|
+
this.left -= n;
|
|
57
|
+
return this.left >= 0;
|
|
58
|
+
}
|
|
59
|
+
get exhausted() {
|
|
60
|
+
return this.left < 0;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var MAX_COPIES = 64;
|
|
64
|
+
var Cursor = class {
|
|
65
|
+
constructor(s) {
|
|
66
|
+
this.s = s;
|
|
67
|
+
}
|
|
68
|
+
s;
|
|
69
|
+
i = 0;
|
|
70
|
+
peek() {
|
|
71
|
+
return this.s[this.i];
|
|
72
|
+
}
|
|
73
|
+
eat(ch) {
|
|
74
|
+
if (this.s[this.i] === ch) {
|
|
75
|
+
this.i++;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
function parseAlt(c, budget) {
|
|
82
|
+
const first = parseSeq(c, budget);
|
|
83
|
+
if (first === null) return null;
|
|
84
|
+
const parts = [first];
|
|
85
|
+
while (c.eat("|")) {
|
|
86
|
+
const p = parseSeq(c, budget);
|
|
87
|
+
if (p === null) return null;
|
|
88
|
+
parts.push(p);
|
|
89
|
+
}
|
|
90
|
+
return parts.length === 1 ? parts[0] : { k: "alt", parts };
|
|
91
|
+
}
|
|
92
|
+
function parseSeq(c, budget) {
|
|
93
|
+
const parts = [];
|
|
94
|
+
for (; ; ) {
|
|
95
|
+
const ch = c.peek();
|
|
96
|
+
if (ch === void 0 || ch === "|" || ch === ")") break;
|
|
97
|
+
if (!budget.spend()) return null;
|
|
98
|
+
let atom;
|
|
99
|
+
if (ch === "(") {
|
|
100
|
+
c.i++;
|
|
101
|
+
if (c.s[c.i] === "?") {
|
|
102
|
+
const n = c.s[c.i + 1];
|
|
103
|
+
if (n === ":") {
|
|
104
|
+
c.i += 2;
|
|
105
|
+
} else if (n === "<" && /[A-Za-z_$]/.test(c.s[c.i + 2] ?? "")) {
|
|
106
|
+
const close = c.s.indexOf(">", c.i + 2);
|
|
107
|
+
if (close === -1) return null;
|
|
108
|
+
c.i = close + 1;
|
|
109
|
+
} else {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const node = parseAlt(c, budget);
|
|
114
|
+
if (node === null || !c.eat(")")) return null;
|
|
115
|
+
atom = node;
|
|
116
|
+
} else if (ch === "[") {
|
|
117
|
+
const set = parseClass(c);
|
|
118
|
+
if (set === null) return null;
|
|
119
|
+
atom = { k: "cls", set };
|
|
120
|
+
} else if (ch === "\\") {
|
|
121
|
+
const t = parseEscape(c);
|
|
122
|
+
if (t === null) return null;
|
|
123
|
+
atom = { k: "cls", set: t };
|
|
124
|
+
} else if (ch === ".") {
|
|
125
|
+
c.i++;
|
|
126
|
+
atom = { k: "cls", set: DOT };
|
|
127
|
+
} else if (ch === "^" || ch === "$" || ch === "{" || ch === "*" || ch === "+" || ch === "?") {
|
|
128
|
+
return null;
|
|
129
|
+
} else {
|
|
130
|
+
const cp = ch.codePointAt(0);
|
|
131
|
+
c.i++;
|
|
132
|
+
atom = { k: "cls", set: [[cp, cp]] };
|
|
133
|
+
}
|
|
134
|
+
const q = c.peek();
|
|
135
|
+
if (q === "*") {
|
|
136
|
+
c.i++;
|
|
137
|
+
atom = { k: "rep", node: atom, min: 0, max: Number.POSITIVE_INFINITY };
|
|
138
|
+
} else if (q === "+") {
|
|
139
|
+
c.i++;
|
|
140
|
+
atom = { k: "rep", node: atom, min: 1, max: Number.POSITIVE_INFINITY };
|
|
141
|
+
} else if (q === "?") {
|
|
142
|
+
c.i++;
|
|
143
|
+
atom = { k: "rep", node: atom, min: 0, max: 1 };
|
|
144
|
+
} else if (q === "{") {
|
|
145
|
+
const close = c.s.indexOf("}", c.i);
|
|
146
|
+
if (close === -1) return null;
|
|
147
|
+
const m = /^(\d+)(,(\d*)?)?$/.exec(c.s.slice(c.i + 1, close));
|
|
148
|
+
if (!m) return null;
|
|
149
|
+
const min = Number.parseInt(m[1], 10);
|
|
150
|
+
const max = m[2] === void 0 ? min : m[3] === "" ? Number.POSITIVE_INFINITY : Number.parseInt(m[3], 10);
|
|
151
|
+
if (max < min || min > MAX_COPIES || max > MAX_COPIES) return null;
|
|
152
|
+
c.i = close + 1;
|
|
153
|
+
atom = { k: "rep", node: atom, min, max };
|
|
154
|
+
}
|
|
155
|
+
parts.push(atom);
|
|
156
|
+
}
|
|
157
|
+
return parts.length === 0 ? { k: "seq", parts: [] } : parts.length === 1 ? parts[0] : { k: "seq", parts };
|
|
158
|
+
}
|
|
159
|
+
function parseClass(c) {
|
|
160
|
+
const s = c.s;
|
|
161
|
+
let i = c.i + 1;
|
|
162
|
+
let negated = false;
|
|
163
|
+
if (s[i] === "^") {
|
|
164
|
+
negated = true;
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
const ranges = [];
|
|
168
|
+
let first = true;
|
|
169
|
+
while (i < s.length && (s[i] !== "]" || first)) {
|
|
170
|
+
first = false;
|
|
171
|
+
let lo;
|
|
172
|
+
if (s[i] === "\\") {
|
|
173
|
+
const t = escapeAt(s, i);
|
|
174
|
+
if (t === null) return null;
|
|
175
|
+
if (typeof t !== "number") {
|
|
176
|
+
ranges.push(...NAMED_SETS[t]);
|
|
177
|
+
i += 2;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
lo = t;
|
|
181
|
+
i += escapeWidth(s, i);
|
|
182
|
+
} else {
|
|
183
|
+
lo = s[i].codePointAt(0);
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
if (s[i] === "-" && s[i + 1] !== "]" && s[i + 1] !== void 0) {
|
|
187
|
+
i++;
|
|
188
|
+
let hi;
|
|
189
|
+
if (s[i] === "\\") {
|
|
190
|
+
const t = escapeAt(s, i);
|
|
191
|
+
if (t === null || typeof t !== "number") return null;
|
|
192
|
+
hi = t;
|
|
193
|
+
i += escapeWidth(s, i);
|
|
194
|
+
} else {
|
|
195
|
+
hi = s[i].codePointAt(0);
|
|
196
|
+
i++;
|
|
197
|
+
}
|
|
198
|
+
if (hi < lo) return null;
|
|
199
|
+
ranges.push([lo, hi]);
|
|
200
|
+
} else {
|
|
201
|
+
ranges.push([lo, lo]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (s[i] !== "]") return null;
|
|
205
|
+
c.i = i + 1;
|
|
206
|
+
if (ranges.length === 0) return null;
|
|
207
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
208
|
+
const merged = [ranges[0]];
|
|
209
|
+
for (let k = 1; k < ranges.length; k++) {
|
|
210
|
+
const last = merged[merged.length - 1];
|
|
211
|
+
if (ranges[k][0] <= last[1] + 1) last[1] = Math.max(last[1], ranges[k][1]);
|
|
212
|
+
else merged.push(ranges[k]);
|
|
213
|
+
}
|
|
214
|
+
return negated ? complementOf(merged) : merged;
|
|
215
|
+
}
|
|
216
|
+
function escapeWidth(s, i) {
|
|
217
|
+
const ch = s[i + 1];
|
|
218
|
+
if (ch === "x") return 4;
|
|
219
|
+
if (ch === "u") return s[i + 2] === "{" ? s.indexOf("}", i + 3) - i + 1 : 6;
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
function escapeAt(s, i) {
|
|
223
|
+
const ch = s[i + 1];
|
|
224
|
+
if (ch === void 0) return null;
|
|
225
|
+
if (NAMED_SETS[ch] !== void 0) return ch;
|
|
226
|
+
const simple = { n: 10, r: 13, t: 9, f: 12, v: 11, 0: 0 };
|
|
227
|
+
if (simple[ch] !== void 0) return simple[ch];
|
|
228
|
+
if (ch === "x") {
|
|
229
|
+
const cp = Number.parseInt(s.slice(i + 2, i + 4), 16);
|
|
230
|
+
return Number.isFinite(cp) ? cp : null;
|
|
231
|
+
}
|
|
232
|
+
if (ch === "u") {
|
|
233
|
+
if (s[i + 2] === "{") {
|
|
234
|
+
const close = s.indexOf("}", i + 3);
|
|
235
|
+
if (close === -1) return null;
|
|
236
|
+
const cp2 = Number.parseInt(s.slice(i + 3, close), 16);
|
|
237
|
+
return Number.isFinite(cp2) && cp2 <= MAX_CP ? cp2 : null;
|
|
238
|
+
}
|
|
239
|
+
const cp = Number.parseInt(s.slice(i + 2, i + 6), 16);
|
|
240
|
+
return Number.isFinite(cp) ? cp : null;
|
|
241
|
+
}
|
|
242
|
+
if (/[A-Za-z0-9]/.test(ch)) return null;
|
|
243
|
+
return ch.codePointAt(0);
|
|
244
|
+
}
|
|
245
|
+
function parseEscape(c) {
|
|
246
|
+
const t = escapeAt(c.s, c.i);
|
|
247
|
+
if (t === null) return null;
|
|
248
|
+
const w = escapeWidth(c.s, c.i);
|
|
249
|
+
c.i += w;
|
|
250
|
+
return typeof t === "number" ? [[t, t]] : NAMED_SETS[t];
|
|
251
|
+
}
|
|
252
|
+
var MAX_NFA_STATES = 600;
|
|
253
|
+
var Nfa = class {
|
|
254
|
+
edges = [];
|
|
255
|
+
start = -1;
|
|
256
|
+
accept = -1;
|
|
257
|
+
newState() {
|
|
258
|
+
this.edges.push([]);
|
|
259
|
+
return this.edges.length - 1;
|
|
260
|
+
}
|
|
261
|
+
add(from, to, set) {
|
|
262
|
+
this.edges[from].push({ to, set });
|
|
263
|
+
}
|
|
264
|
+
chain(f1, f2) {
|
|
265
|
+
this.add(f1.accept, f2.start, null);
|
|
266
|
+
return { start: f1.start, accept: f2.accept };
|
|
267
|
+
}
|
|
268
|
+
opt(f) {
|
|
269
|
+
const s = this.newState();
|
|
270
|
+
const a = this.newState();
|
|
271
|
+
this.add(s, f.start, null);
|
|
272
|
+
this.add(f.accept, a, null);
|
|
273
|
+
this.add(s, a, null);
|
|
274
|
+
return { start: s, accept: a };
|
|
275
|
+
}
|
|
276
|
+
star(node, budget) {
|
|
277
|
+
const inner = this.build(node, budget);
|
|
278
|
+
if (inner === null) return null;
|
|
279
|
+
const s = this.newState();
|
|
280
|
+
const a = this.newState();
|
|
281
|
+
this.add(s, inner.start, null);
|
|
282
|
+
this.add(s, a, null);
|
|
283
|
+
this.add(inner.accept, inner.start, null);
|
|
284
|
+
this.add(inner.accept, a, null);
|
|
285
|
+
return { start: s, accept: a };
|
|
286
|
+
}
|
|
287
|
+
build(node, budget) {
|
|
288
|
+
if (this.edges.length > MAX_NFA_STATES || !budget.spend()) return null;
|
|
289
|
+
if (node.k === "cls") {
|
|
290
|
+
const s = this.newState();
|
|
291
|
+
const a = this.newState();
|
|
292
|
+
this.add(s, a, node.set);
|
|
293
|
+
return { start: s, accept: a };
|
|
294
|
+
}
|
|
295
|
+
if (node.k === "seq") {
|
|
296
|
+
if (node.parts.length === 0) {
|
|
297
|
+
const s = this.newState();
|
|
298
|
+
return { start: s, accept: s };
|
|
299
|
+
}
|
|
300
|
+
let frag2 = null;
|
|
301
|
+
for (const p of node.parts) {
|
|
302
|
+
const f = this.build(p, budget);
|
|
303
|
+
if (f === null) return null;
|
|
304
|
+
frag2 = frag2 === null ? f : this.chain(frag2, f);
|
|
305
|
+
}
|
|
306
|
+
return frag2;
|
|
307
|
+
}
|
|
308
|
+
if (node.k === "alt") {
|
|
309
|
+
const s = this.newState();
|
|
310
|
+
const a = this.newState();
|
|
311
|
+
for (const p of node.parts) {
|
|
312
|
+
const f = this.build(p, budget);
|
|
313
|
+
if (f === null) return null;
|
|
314
|
+
this.add(s, f.start, null);
|
|
315
|
+
this.add(f.accept, a, null);
|
|
316
|
+
}
|
|
317
|
+
return { start: s, accept: a };
|
|
318
|
+
}
|
|
319
|
+
if (node.max === Number.POSITIVE_INFINITY) {
|
|
320
|
+
if (node.min === 0) return this.star(node.node, budget);
|
|
321
|
+
let frag2 = null;
|
|
322
|
+
for (let i = 0; i < Math.min(node.min, MAX_COPIES); i++) {
|
|
323
|
+
const copy = this.build(node.node, budget);
|
|
324
|
+
if (copy === null) return null;
|
|
325
|
+
frag2 = frag2 === null ? copy : this.chain(frag2, copy);
|
|
326
|
+
}
|
|
327
|
+
const tail = this.star(node.node, budget);
|
|
328
|
+
if (tail === null || frag2 === null) return null;
|
|
329
|
+
return this.chain(frag2, tail);
|
|
330
|
+
}
|
|
331
|
+
let frag = null;
|
|
332
|
+
for (let i = 0; i < node.max; i++) {
|
|
333
|
+
let copy = this.build(node.node, budget);
|
|
334
|
+
if (copy === null) return null;
|
|
335
|
+
if (i >= node.min) copy = this.opt(copy);
|
|
336
|
+
frag = frag === null ? copy : this.chain(frag, copy);
|
|
337
|
+
}
|
|
338
|
+
if (frag === null) {
|
|
339
|
+
const s = this.newState();
|
|
340
|
+
return { start: s, accept: s };
|
|
341
|
+
}
|
|
342
|
+
return frag;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
function epsilonClosure(nfa, from) {
|
|
346
|
+
const out = new Set(from);
|
|
347
|
+
const stack = [...from];
|
|
348
|
+
while (stack.length > 0) {
|
|
349
|
+
const p = stack.pop();
|
|
350
|
+
for (const e of nfa.edges[p]) {
|
|
351
|
+
if (e.set === null && !out.has(e.to)) {
|
|
352
|
+
out.add(e.to);
|
|
353
|
+
stack.push(e.to);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
function parseAmbiguity(nfa, startSources, acceptReachable, budget) {
|
|
360
|
+
const n = nfa.edges.length;
|
|
361
|
+
const key = (p, q) => p * n + q;
|
|
362
|
+
const seen = /* @__PURE__ */ new Map();
|
|
363
|
+
const completions = /* @__PURE__ */ new Map();
|
|
364
|
+
let head = 0;
|
|
365
|
+
const queue = [];
|
|
366
|
+
for (const p of startSources) {
|
|
367
|
+
for (const q of startSources) {
|
|
368
|
+
const k = key(p, q);
|
|
369
|
+
if (!seen.has(k)) {
|
|
370
|
+
seen.set(k, null);
|
|
371
|
+
queue.push({ p, q, diverged: p !== q });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
while (head < queue.length) {
|
|
376
|
+
if (!budget.spend(4)) return { ambiguous: false };
|
|
377
|
+
const { p, q, diverged } = queue[head++];
|
|
378
|
+
const k = key(p, q);
|
|
379
|
+
const nextSources = (target) => [...epsilonClosure(nfa, [target])].filter((s) => nfa.edges[s].some((e) => e.set !== null));
|
|
380
|
+
for (const e1 of nfa.edges[p]) {
|
|
381
|
+
if (e1.set === null) continue;
|
|
382
|
+
for (const e2 of nfa.edges[q]) {
|
|
383
|
+
if (e2.set === null) continue;
|
|
384
|
+
const both = intersect(e1.set, e2.set);
|
|
385
|
+
if (both.length === 0) continue;
|
|
386
|
+
if (diverged && acceptReachable.has(e1.to) && acceptReachable.has(e2.to) && !completions.has(k)) {
|
|
387
|
+
completions.set(k, String.fromCodePoint(anyMember(both)));
|
|
388
|
+
}
|
|
389
|
+
const from1 = nextSources(e1.to);
|
|
390
|
+
const from2 = nextSources(e2.to);
|
|
391
|
+
const stepChar = String.fromCodePoint(anyMember(both));
|
|
392
|
+
for (const p2 of from1) {
|
|
393
|
+
for (const q2 of from2) {
|
|
394
|
+
const k2 = key(p2, q2);
|
|
395
|
+
if (!seen.has(k2)) {
|
|
396
|
+
const childDiverged = diverged || p2 !== q2;
|
|
397
|
+
seen.set(k2, [k, stepChar, childDiverged]);
|
|
398
|
+
queue.push({ p: p2, q: q2, diverged: childDiverged });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (completions.size === 0) return { ambiguous: false };
|
|
406
|
+
const [doneKey, doneChar] = completions.entries().next().value;
|
|
407
|
+
let witness = doneChar;
|
|
408
|
+
let cur = seen.get(doneKey);
|
|
409
|
+
while (cur !== null && cur !== void 0) {
|
|
410
|
+
witness = cur[1] + witness;
|
|
411
|
+
cur = seen.get(cur[0]);
|
|
412
|
+
}
|
|
413
|
+
return { ambiguous: true, witness };
|
|
414
|
+
}
|
|
415
|
+
var WORD_MAX = 6;
|
|
416
|
+
var MAX_WORDS = 120;
|
|
417
|
+
function decompositionAmbiguity(nfa, startClosure, acceptClosureOf, budget) {
|
|
418
|
+
if (startClosure.has(nfa.accept)) {
|
|
419
|
+
return { ambiguous: true, witness: "\u03B5 (empty iterations insertable)" };
|
|
420
|
+
}
|
|
421
|
+
const labels = [];
|
|
422
|
+
for (const edges of nfa.edges) {
|
|
423
|
+
for (const e of edges) {
|
|
424
|
+
if (e.set !== null && !labels.some((l) => l === e.set)) labels.push(e.set);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const repsSet = /* @__PURE__ */ new Set();
|
|
428
|
+
for (let i = 0; i < labels.length; i++) {
|
|
429
|
+
repsSet.add(anyMember(labels[i]));
|
|
430
|
+
for (let j = i + 1; j < labels.length; j++) {
|
|
431
|
+
const ov = intersect(labels[i], labels[j]);
|
|
432
|
+
if (ov.length > 0) repsSet.add(anyMember(ov));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const reps = [...repsSet].slice(0, 8);
|
|
436
|
+
if (reps.length === 0) return { ambiguous: false };
|
|
437
|
+
const step = (config, ch) => {
|
|
438
|
+
const next = /* @__PURE__ */ new Set();
|
|
439
|
+
for (const p of config) {
|
|
440
|
+
for (const e of nfa.edges[p]) {
|
|
441
|
+
if (e.set?.some(([lo, hi]) => ch >= lo && ch <= hi)) {
|
|
442
|
+
for (const s of epsilonClosure(nfa, [e.to])) next.add(s);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return next;
|
|
447
|
+
};
|
|
448
|
+
const words = [];
|
|
449
|
+
let level = [
|
|
450
|
+
{ config: new Set(startClosure), s: "" }
|
|
451
|
+
];
|
|
452
|
+
for (let len = 1; len <= WORD_MAX && level.length > 0 && words.length < MAX_WORDS; len++) {
|
|
453
|
+
if (!budget.spend(level.length * reps.length)) return { ambiguous: false };
|
|
454
|
+
const nextLevel = [];
|
|
455
|
+
for (const { config, s } of level) {
|
|
456
|
+
for (const rep of reps) {
|
|
457
|
+
const next = step(config, rep);
|
|
458
|
+
if (next.size === 0) continue;
|
|
459
|
+
const ns = s + String.fromCodePoint(rep);
|
|
460
|
+
if ([...next].some((p) => acceptClosureOf.has(p))) words.push(ns);
|
|
461
|
+
nextLevel.push({ config: next, s: ns });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
level = nextLevel;
|
|
465
|
+
}
|
|
466
|
+
if (words.length === 0) return { ambiguous: false };
|
|
467
|
+
const wordSet = new Set(words);
|
|
468
|
+
const residualsOf = (sources) => {
|
|
469
|
+
const out = /* @__PURE__ */ new Set();
|
|
470
|
+
for (const u of sources) {
|
|
471
|
+
for (const x of words) {
|
|
472
|
+
if (x.length > u.length && x.startsWith(u)) out.add(x.slice(u.length));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return out;
|
|
476
|
+
};
|
|
477
|
+
const leftDiv = (src) => {
|
|
478
|
+
const out = /* @__PURE__ */ new Set();
|
|
479
|
+
for (const l of words) {
|
|
480
|
+
for (const t of src) {
|
|
481
|
+
if (t.length > l.length && t.startsWith(l)) out.add(t.slice(l.length));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
};
|
|
486
|
+
let frontier = residualsOf(words);
|
|
487
|
+
const visited = /* @__PURE__ */ new Set();
|
|
488
|
+
while (frontier.size > 0) {
|
|
489
|
+
if (!budget.spend(words.length)) return { ambiguous: false };
|
|
490
|
+
const frozen = [...frontier].sort().join("");
|
|
491
|
+
if (visited.has(frozen)) break;
|
|
492
|
+
visited.add(frozen);
|
|
493
|
+
for (const v of frontier) {
|
|
494
|
+
if (v === "" || wordSet.has(v)) {
|
|
495
|
+
return { ambiguous: true, witness: `Sardinas\u2013Patterson residual '${v || "\u03B5"}'` };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const next = residualsOf(frontier);
|
|
499
|
+
for (const v of leftDiv(frontier)) next.add(v);
|
|
500
|
+
frontier = next;
|
|
501
|
+
}
|
|
502
|
+
return { ambiguous: false };
|
|
503
|
+
}
|
|
504
|
+
function detectQuantifiedAmbiguity(content) {
|
|
505
|
+
const budget = new Budget();
|
|
506
|
+
const cursor = new Cursor(content);
|
|
507
|
+
const ast = parseAlt(cursor, budget);
|
|
508
|
+
if (ast === null || cursor.i !== content.length) return { verdict: "unparsable" };
|
|
509
|
+
const nfa = new Nfa();
|
|
510
|
+
const frag = nfa.build(ast, budget);
|
|
511
|
+
if (frag === null) return { verdict: budget.exhausted ? "budget" : "unparsable" };
|
|
512
|
+
nfa.start = frag.start;
|
|
513
|
+
nfa.accept = frag.accept;
|
|
514
|
+
const startClosure = epsilonClosure(nfa, [nfa.start]);
|
|
515
|
+
const charSources = /* @__PURE__ */ new Set();
|
|
516
|
+
for (let p = 0; p < nfa.edges.length; p++) {
|
|
517
|
+
if (nfa.edges[p].some((e) => e.set !== null)) charSources.add(p);
|
|
518
|
+
}
|
|
519
|
+
const acceptClosureOf = /* @__PURE__ */ new Set();
|
|
520
|
+
for (let p = 0; p < nfa.edges.length; p++) {
|
|
521
|
+
if (epsilonClosure(nfa, [p]).has(nfa.accept)) acceptClosureOf.add(p);
|
|
522
|
+
}
|
|
523
|
+
const startSources = /* @__PURE__ */ new Set();
|
|
524
|
+
for (const p of startClosure) {
|
|
525
|
+
if (charSources.has(p)) startSources.add(p);
|
|
526
|
+
}
|
|
527
|
+
const s1 = parseAmbiguity(nfa, startSources, acceptClosureOf, budget);
|
|
528
|
+
if (s1.ambiguous) return { verdict: "ambiguous", witness: s1.witness };
|
|
529
|
+
const s2 = decompositionAmbiguity(nfa, startClosure, acceptClosureOf, budget);
|
|
530
|
+
if (s2.ambiguous) return { verdict: "ambiguous", witness: s2.witness };
|
|
531
|
+
return { verdict: budget.exhausted ? "budget" : "unambiguous" };
|
|
532
|
+
}
|
|
533
|
+
|
|
1
534
|
// src/regex-guard.ts
|
|
2
535
|
var MAX_PATTERN_LEN = 256;
|
|
3
536
|
var DANGEROUS_PATTERNS = [
|
|
@@ -11,6 +544,408 @@ var DANGEROUS_PATTERNS = [
|
|
|
11
544
|
// Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
|
|
12
545
|
/\(\?<?[!=][^)]*[+*][^)]*\)/
|
|
13
546
|
];
|
|
547
|
+
var GROUP_PREFIX_RE = /^\?(?::|[=!]|<[=!]|<[$_\p{ID_Start}][$_\p{ID_Continue}\u200C\u200D]*>)/u;
|
|
548
|
+
function stripGroupPrefix(inner) {
|
|
549
|
+
return inner.replace(GROUP_PREFIX_RE, "");
|
|
550
|
+
}
|
|
551
|
+
function splitTopLevelBranches(inner) {
|
|
552
|
+
const branches = [];
|
|
553
|
+
let current = "";
|
|
554
|
+
let d = 0;
|
|
555
|
+
let cls = false;
|
|
556
|
+
for (let k = 0; k < inner.length; k++) {
|
|
557
|
+
const ch = inner[k];
|
|
558
|
+
if (ch === "\\") {
|
|
559
|
+
current += ch + (inner[k + 1] ?? "");
|
|
560
|
+
k++;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (cls) {
|
|
564
|
+
if (ch === "]") cls = false;
|
|
565
|
+
current += ch;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (ch === "[") {
|
|
569
|
+
cls = true;
|
|
570
|
+
current += ch;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (ch === "(") d++;
|
|
574
|
+
if (ch === ")") d--;
|
|
575
|
+
if (ch === "|" && d === 0) {
|
|
576
|
+
branches.push(current);
|
|
577
|
+
current = "";
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
current += ch;
|
|
581
|
+
}
|
|
582
|
+
branches.push(current);
|
|
583
|
+
return branches;
|
|
584
|
+
}
|
|
585
|
+
function branchesOverlap(branches) {
|
|
586
|
+
const norm = branches.map(unwrapGroupBranch);
|
|
587
|
+
for (const list of [branches, norm]) {
|
|
588
|
+
for (let a = 0; a < list.length; a++) {
|
|
589
|
+
for (let b = a + 1; b < list.length; b++) {
|
|
590
|
+
const x = list[a];
|
|
591
|
+
const y = list[b];
|
|
592
|
+
if (x === "" || y === "") return true;
|
|
593
|
+
if (matchesEmptyToken(x) || matchesEmptyToken(y)) return true;
|
|
594
|
+
if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
|
|
595
|
+
const sx = singleTokenCharSet(x);
|
|
596
|
+
const sy = singleTokenCharSet(y);
|
|
597
|
+
if (sx && sy && charSetsIntersect(sx, sy)) return true;
|
|
598
|
+
const fx = fixedTokenSets(x);
|
|
599
|
+
const fy = fixedTokenSets(y);
|
|
600
|
+
if (fx && fy && fx.length === fy.length && fx.every((s, idx) => charSetsIntersect(s, fy[idx]))) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
var MAX_CP2 = 1114111;
|
|
609
|
+
var WORD_SET = [
|
|
610
|
+
[48, 57],
|
|
611
|
+
[65, 90],
|
|
612
|
+
[95, 95],
|
|
613
|
+
[97, 122]
|
|
614
|
+
];
|
|
615
|
+
var DIGIT_SET = [[48, 57]];
|
|
616
|
+
var SPACE_SET = [
|
|
617
|
+
[9, 13],
|
|
618
|
+
[32, 32],
|
|
619
|
+
[160, 160],
|
|
620
|
+
[5760, 5760],
|
|
621
|
+
[8192, 8202],
|
|
622
|
+
[8232, 8233],
|
|
623
|
+
[8239, 8239],
|
|
624
|
+
[8287, 8287],
|
|
625
|
+
[12288, 12288],
|
|
626
|
+
[65279, 65279]
|
|
627
|
+
];
|
|
628
|
+
var DOT_SET = [
|
|
629
|
+
[0, 9],
|
|
630
|
+
[11, MAX_CP2]
|
|
631
|
+
];
|
|
632
|
+
function complementOf2(set) {
|
|
633
|
+
const out = [];
|
|
634
|
+
let next = 0;
|
|
635
|
+
for (const [lo, hi] of set) {
|
|
636
|
+
if (lo > next) out.push([next, lo - 1]);
|
|
637
|
+
next = hi + 1;
|
|
638
|
+
}
|
|
639
|
+
if (next <= MAX_CP2) out.push([next, MAX_CP2]);
|
|
640
|
+
return out;
|
|
641
|
+
}
|
|
642
|
+
function mergeRanges(ranges) {
|
|
643
|
+
if (ranges.length === 0) return [];
|
|
644
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
645
|
+
const out = [ranges[0]];
|
|
646
|
+
for (let i = 1; i < ranges.length; i++) {
|
|
647
|
+
const last = out[out.length - 1];
|
|
648
|
+
const r = ranges[i];
|
|
649
|
+
if (r[0] <= last[1] + 1) {
|
|
650
|
+
last[1] = Math.max(last[1], r[1]);
|
|
651
|
+
} else {
|
|
652
|
+
out.push(r);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return out;
|
|
656
|
+
}
|
|
657
|
+
function charSetsIntersect(a, b) {
|
|
658
|
+
let i = 0;
|
|
659
|
+
let j = 0;
|
|
660
|
+
while (i < a.length && j < b.length) {
|
|
661
|
+
const [alo, ahi] = a[i];
|
|
662
|
+
const [blo, bhi] = b[j];
|
|
663
|
+
if (alo <= bhi && blo <= ahi) return true;
|
|
664
|
+
if (ahi < bhi) i++;
|
|
665
|
+
else j++;
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
var NAMED_CLASS_SETS = {
|
|
670
|
+
w: WORD_SET,
|
|
671
|
+
W: complementOf2(WORD_SET),
|
|
672
|
+
d: DIGIT_SET,
|
|
673
|
+
D: complementOf2(DIGIT_SET),
|
|
674
|
+
s: SPACE_SET,
|
|
675
|
+
S: complementOf2(SPACE_SET)
|
|
676
|
+
};
|
|
677
|
+
var SIMPLE_ESCAPES = {
|
|
678
|
+
n: 10,
|
|
679
|
+
r: 13,
|
|
680
|
+
t: 9,
|
|
681
|
+
f: 12,
|
|
682
|
+
v: 11,
|
|
683
|
+
0: 0,
|
|
684
|
+
b: 8
|
|
685
|
+
// backspace inside a class; outside, a lone `\b` is caught by matchesEmptyToken first
|
|
686
|
+
};
|
|
687
|
+
function hexAt(s, start, len) {
|
|
688
|
+
if (start + len > s.length) return null;
|
|
689
|
+
const cp = Number.parseInt(s.slice(start, start + len), 16);
|
|
690
|
+
return Number.isFinite(cp) ? cp : null;
|
|
691
|
+
}
|
|
692
|
+
function parseEscape2(s, i) {
|
|
693
|
+
const ch = s[i + 1];
|
|
694
|
+
if (ch === void 0) return null;
|
|
695
|
+
if (NAMED_CLASS_SETS[ch] !== void 0) {
|
|
696
|
+
return { kind: "named", cp: -1, name: ch, next: i + 2 };
|
|
697
|
+
}
|
|
698
|
+
const simple = SIMPLE_ESCAPES[ch];
|
|
699
|
+
if (simple !== void 0) {
|
|
700
|
+
return { kind: "literal", cp: simple, name: "", next: i + 2 };
|
|
701
|
+
}
|
|
702
|
+
if (ch === "x") {
|
|
703
|
+
const cp = hexAt(s, i + 2, 2);
|
|
704
|
+
if (cp === null) return null;
|
|
705
|
+
return { kind: "literal", cp, name: "", next: i + 4 };
|
|
706
|
+
}
|
|
707
|
+
if (ch === "u") {
|
|
708
|
+
if (s[i + 2] === "{") {
|
|
709
|
+
const close = s.indexOf("}", i + 3);
|
|
710
|
+
if (close === -1 || close - (i + 3) > 6) return null;
|
|
711
|
+
const cp2 = hexAt(s, i + 3, close - (i + 3));
|
|
712
|
+
if (cp2 === null || cp2 > MAX_CP2) return null;
|
|
713
|
+
return { kind: "literal", cp: cp2, name: "", next: close + 1 };
|
|
714
|
+
}
|
|
715
|
+
const cp = hexAt(s, i + 2, 4);
|
|
716
|
+
if (cp === null) return null;
|
|
717
|
+
return { kind: "literal", cp, name: "", next: i + 6 };
|
|
718
|
+
}
|
|
719
|
+
if (/[A-Za-z]/.test(ch)) return null;
|
|
720
|
+
return { kind: "literal", cp: ch.codePointAt(0) ?? -1, name: "", next: i + 2 };
|
|
721
|
+
}
|
|
722
|
+
function groupCloseIndex(s, open) {
|
|
723
|
+
let depth = 0;
|
|
724
|
+
let inClass = false;
|
|
725
|
+
for (let j = open; j < s.length; j++) {
|
|
726
|
+
const c = s[j];
|
|
727
|
+
if (c === "\\") {
|
|
728
|
+
j++;
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (inClass) {
|
|
732
|
+
if (c === "]") inClass = false;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (c === "[") {
|
|
736
|
+
inClass = true;
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
if (c === "(") depth++;
|
|
740
|
+
else if (c === ")") {
|
|
741
|
+
depth--;
|
|
742
|
+
if (depth === 0) return j;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return -1;
|
|
746
|
+
}
|
|
747
|
+
function unwrapGroupBranch(branch) {
|
|
748
|
+
let s = branch;
|
|
749
|
+
for (; ; ) {
|
|
750
|
+
if (!s.startsWith("(") || !s.endsWith(")")) return s;
|
|
751
|
+
const close = groupCloseIndex(s, 0);
|
|
752
|
+
if (close !== s.length - 1) return s;
|
|
753
|
+
let inner = s.slice(1, -1);
|
|
754
|
+
if (inner.startsWith("?")) {
|
|
755
|
+
if (/^(?:=|!|<=|<!)/.test(inner.slice(1))) return s;
|
|
756
|
+
const prefix = GROUP_PREFIX_RE.exec(inner);
|
|
757
|
+
if (!prefix) return s;
|
|
758
|
+
inner = inner.slice(prefix[0].length);
|
|
759
|
+
}
|
|
760
|
+
s = inner;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function matchesEmptyToken(branch) {
|
|
764
|
+
return branch === "^" || branch === "$" || branch === "\\b" || branch === "\\B" || /^\(\?(?:=|!|<=|<!)[\s\S]*\)$/.test(branch);
|
|
765
|
+
}
|
|
766
|
+
function parseCharClass(s) {
|
|
767
|
+
if (!s.startsWith("[") || !s.endsWith("]") || s.length < 3) return null;
|
|
768
|
+
let i = 1;
|
|
769
|
+
let negated = false;
|
|
770
|
+
if (s[i] === "^") {
|
|
771
|
+
negated = true;
|
|
772
|
+
i++;
|
|
773
|
+
}
|
|
774
|
+
const ranges = [];
|
|
775
|
+
let first = true;
|
|
776
|
+
while (i < s.length - 1) {
|
|
777
|
+
const ch = s[i];
|
|
778
|
+
let lo;
|
|
779
|
+
let width;
|
|
780
|
+
if (ch === "\\") {
|
|
781
|
+
const tok = parseEscape2(s, i);
|
|
782
|
+
if (!tok) return null;
|
|
783
|
+
if (tok.kind === "named") {
|
|
784
|
+
ranges.push(...NAMED_CLASS_SETS[tok.name]);
|
|
785
|
+
i = tok.next;
|
|
786
|
+
first = false;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
lo = tok.cp;
|
|
790
|
+
width = tok.next - i;
|
|
791
|
+
} else {
|
|
792
|
+
lo = ch.codePointAt(0) ?? -1;
|
|
793
|
+
width = 1;
|
|
794
|
+
}
|
|
795
|
+
const nextCh = s[i + width];
|
|
796
|
+
if (nextCh === "-" && i + width + 1 < s.length - 1) {
|
|
797
|
+
const hiStart = i + width + 1;
|
|
798
|
+
let hi;
|
|
799
|
+
if (s[hiStart] === "\\") {
|
|
800
|
+
const tok = parseEscape2(s, hiStart);
|
|
801
|
+
if (tok?.kind !== "literal") return null;
|
|
802
|
+
hi = tok.cp;
|
|
803
|
+
i = tok.next;
|
|
804
|
+
} else {
|
|
805
|
+
hi = s[hiStart].codePointAt(0) ?? -1;
|
|
806
|
+
i = hiStart + 1;
|
|
807
|
+
}
|
|
808
|
+
if (hi < lo) return null;
|
|
809
|
+
ranges.push([lo, hi]);
|
|
810
|
+
} else {
|
|
811
|
+
ranges.push([lo, lo]);
|
|
812
|
+
i += width;
|
|
813
|
+
}
|
|
814
|
+
first = false;
|
|
815
|
+
void first;
|
|
816
|
+
}
|
|
817
|
+
if (ranges.length === 0) return null;
|
|
818
|
+
const merged = mergeRanges(ranges);
|
|
819
|
+
return negated ? complementOf2(merged) : merged;
|
|
820
|
+
}
|
|
821
|
+
function singleTokenCharSet(branch) {
|
|
822
|
+
if (branch === ".") return DOT_SET;
|
|
823
|
+
if (branch.startsWith("\\")) {
|
|
824
|
+
const tok = parseEscape2(branch, 0);
|
|
825
|
+
if (!tok || tok.next !== branch.length) return null;
|
|
826
|
+
if (tok.kind === "named") return NAMED_CLASS_SETS[tok.name] ?? null;
|
|
827
|
+
return [[tok.cp, tok.cp]];
|
|
828
|
+
}
|
|
829
|
+
if (branch.length === 1) {
|
|
830
|
+
const cp = branch.codePointAt(0);
|
|
831
|
+
return cp === void 0 ? null : [[cp, cp]];
|
|
832
|
+
}
|
|
833
|
+
if (branch.startsWith("[")) return parseCharClass(branch);
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
function fixedTokenSets(branch) {
|
|
837
|
+
const sets = [];
|
|
838
|
+
let i = 0;
|
|
839
|
+
while (i < branch.length) {
|
|
840
|
+
const ch = branch[i];
|
|
841
|
+
let set;
|
|
842
|
+
let next;
|
|
843
|
+
if (ch === ".") {
|
|
844
|
+
set = DOT_SET;
|
|
845
|
+
next = i + 1;
|
|
846
|
+
} else if (ch === "\\") {
|
|
847
|
+
const tok = parseEscape2(branch, i);
|
|
848
|
+
if (!tok) return null;
|
|
849
|
+
set = tok.kind === "named" ? NAMED_CLASS_SETS[tok.name] ?? null : [[tok.cp, tok.cp]];
|
|
850
|
+
next = tok.next;
|
|
851
|
+
} else if (ch === "[") {
|
|
852
|
+
let end = i + 1;
|
|
853
|
+
if (branch[end] === "^") end++;
|
|
854
|
+
if (branch[end] === "]") end++;
|
|
855
|
+
while (end < branch.length && branch[end] !== "]") {
|
|
856
|
+
if (branch[end] === "\\") end++;
|
|
857
|
+
end++;
|
|
858
|
+
}
|
|
859
|
+
if (end >= branch.length) return null;
|
|
860
|
+
set = parseCharClass(branch.slice(i, end + 1));
|
|
861
|
+
next = end + 1;
|
|
862
|
+
} else if ("^$()*+?{|".includes(ch)) {
|
|
863
|
+
return null;
|
|
864
|
+
} else {
|
|
865
|
+
const cp = ch.codePointAt(0);
|
|
866
|
+
if (cp === void 0) return null;
|
|
867
|
+
set = [[cp, cp]];
|
|
868
|
+
next = i + 1;
|
|
869
|
+
}
|
|
870
|
+
if (set === null) return null;
|
|
871
|
+
const q = branch[next];
|
|
872
|
+
if (q === void 0) {
|
|
873
|
+
sets.push(set);
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
if (q === "*" || q === "+" || q === "?") return null;
|
|
877
|
+
if (q === "{") {
|
|
878
|
+
const close = branch.indexOf("}", next);
|
|
879
|
+
if (close === -1) return null;
|
|
880
|
+
const body = branch.slice(next + 1, close);
|
|
881
|
+
if (!/^\d+$/.test(body)) return null;
|
|
882
|
+
const n = Number.parseInt(body, 10);
|
|
883
|
+
if (n < 1 || n > 16 || sets.length + n > 64) return null;
|
|
884
|
+
for (let k = 0; k < n; k++) sets.push(set);
|
|
885
|
+
i = close + 1;
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
sets.push(set);
|
|
889
|
+
i = next;
|
|
890
|
+
}
|
|
891
|
+
return sets.length > 0 && sets.length <= 64 ? sets : null;
|
|
892
|
+
}
|
|
893
|
+
function directChildGroupContents(s) {
|
|
894
|
+
const children = [];
|
|
895
|
+
let i = 0;
|
|
896
|
+
while (i < s.length) {
|
|
897
|
+
const ch = s[i];
|
|
898
|
+
if (ch === "\\") {
|
|
899
|
+
i += 2;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (ch === "[") {
|
|
903
|
+
for (i++; i < s.length && s[i] !== "]"; i++) {
|
|
904
|
+
if (s[i] === "\\") i++;
|
|
905
|
+
}
|
|
906
|
+
i++;
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (ch !== "(") {
|
|
910
|
+
i++;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
let depth = 0;
|
|
914
|
+
let j = i;
|
|
915
|
+
for (; j < s.length; j++) {
|
|
916
|
+
const c = s[j];
|
|
917
|
+
if (c === "\\") {
|
|
918
|
+
j++;
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (c === "[") {
|
|
922
|
+
for (j++; j < s.length && s[j] !== "]"; j++) {
|
|
923
|
+
if (s[j] === "\\") j++;
|
|
924
|
+
}
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (c === "(") depth++;
|
|
928
|
+
else if (c === ")") {
|
|
929
|
+
depth--;
|
|
930
|
+
if (depth === 0) break;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (j >= s.length) break;
|
|
934
|
+
children.push(s.slice(i + 1, j));
|
|
935
|
+
i = j + 1;
|
|
936
|
+
}
|
|
937
|
+
return children;
|
|
938
|
+
}
|
|
939
|
+
function hasAmbiguousBranches(content) {
|
|
940
|
+
const branches = splitTopLevelBranches(content);
|
|
941
|
+
if (branches.length >= 2 && branchesOverlap(branches)) return true;
|
|
942
|
+
for (const branch of branches) {
|
|
943
|
+
for (const child of directChildGroupContents(branch)) {
|
|
944
|
+
if (hasAmbiguousBranches(stripGroupPrefix(child))) return true;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
14
949
|
function hasAmbiguousQuantifiedAlternation(pattern) {
|
|
15
950
|
let outerInClass = false;
|
|
16
951
|
for (let i = 0; i < pattern.length; i++) {
|
|
@@ -54,47 +989,11 @@ function hasAmbiguousQuantifiedAlternation(pattern) {
|
|
|
54
989
|
if (j >= pattern.length) return false;
|
|
55
990
|
const next = pattern[j + 1];
|
|
56
991
|
if (next !== "+" && next !== "*" && next !== "{") continue;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const branches = [];
|
|
60
|
-
let current = "";
|
|
61
|
-
let d = 0;
|
|
62
|
-
let cls = false;
|
|
63
|
-
for (let k = 0; k < inner.length; k++) {
|
|
64
|
-
const ch2 = inner[k];
|
|
65
|
-
if (ch2 === "\\") {
|
|
66
|
-
current += ch2 + (inner[k + 1] ?? "");
|
|
67
|
-
k++;
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (cls) {
|
|
71
|
-
if (ch2 === "]") cls = false;
|
|
72
|
-
current += ch2;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if (ch2 === "[") {
|
|
76
|
-
cls = true;
|
|
77
|
-
current += ch2;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (ch2 === "(") d++;
|
|
81
|
-
if (ch2 === ")") d--;
|
|
82
|
-
if (ch2 === "|" && d === 0) {
|
|
83
|
-
branches.push(current);
|
|
84
|
-
current = "";
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
current += ch2;
|
|
992
|
+
if (hasAmbiguousBranches(stripGroupPrefix(pattern.slice(i + 1, j)))) {
|
|
993
|
+
return true;
|
|
88
994
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
for (let a = 0; a < branches.length; a++) {
|
|
92
|
-
for (let b = a + 1; b < branches.length; b++) {
|
|
93
|
-
const x = branches[a];
|
|
94
|
-
const y = branches[b];
|
|
95
|
-
if (x === "" || y === "") return true;
|
|
96
|
-
if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
|
|
97
|
-
}
|
|
995
|
+
if (detectQuantifiedAmbiguity(pattern.slice(i + 1, j)).verdict === "ambiguous") {
|
|
996
|
+
return true;
|
|
98
997
|
}
|
|
99
998
|
}
|
|
100
999
|
return false;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step-budgeted regex ambiguity matcher — ADR-004.
|
|
3
|
+
*
|
|
4
|
+
* The final semantic layer of the ReDoS guard (packages/primitives/src/
|
|
5
|
+
* regex-guard.ts, wired additively after the static layers from rounds
|
|
6
|
+
* 11-14). It answers, for the CONTENT of a quantified group X:
|
|
7
|
+
*
|
|
8
|
+
* does `(?:X)+` admit a string with two or more distinct parses?
|
|
9
|
+
*
|
|
10
|
+
* Two mechanisms, both bounded by a checker-side step budget:
|
|
11
|
+
*
|
|
12
|
+
* 1. PARSE AMBIGUITY (within one iteration) — build a Thompson NFA of X,
|
|
13
|
+
* ε-eliminate onto CHAR-SOURCE states (states that own an outgoing
|
|
14
|
+
* consuming edge; ε-closure is folded into transition targets), then run
|
|
15
|
+
* the squared-product construction: product nodes are (p, q) source
|
|
16
|
+
* pairs; steps consume one character synchronously through both tracks;
|
|
17
|
+
* ambiguity = a divergent pair (p ≠ q — a different branch choice)
|
|
18
|
+
* from which both tracks can still complete to acceptance. Keying the
|
|
19
|
+
* product on SOURCE states is what removes the ε-timing false positives
|
|
20
|
+
* that falsified the naive asynchronous-ε product in the ADR-004 spike:
|
|
21
|
+
* the same parse paused at different ε-points never appears.
|
|
22
|
+
*
|
|
23
|
+
* 2. DECOMPOSITION AMBIGUITY (across iterations) — the Sardinas–Patterson
|
|
24
|
+
* code question: `X+` is unambiguous iff L(X) is a code (uniquely
|
|
25
|
+
* decodable). This is the only mechanism that can see `(a+)+`-style
|
|
26
|
+
* self-decomposition, where the ambiguity lives BETWEEN iterations and
|
|
27
|
+
* both iterations consume through the same edges. L(X) is approximated
|
|
28
|
+
* by a finite word set W (all L-words up to WORD_MAX over a
|
|
29
|
+
* representative alphabet) and the SP residual recurrence runs on W —
|
|
30
|
+
* sound in the flagging direction: a non-code W proves L(X) not a code.
|
|
31
|
+
* Beyond the word bound or the budget the stage UNDER-REJECTS.
|
|
32
|
+
*
|
|
33
|
+
* Verdict doctrine (guard-wide): rejection requires a proof — an 'ambiguous'
|
|
34
|
+
* verdict always carries a witness string with two decompositions. Budget
|
|
35
|
+
* exhaustion ('budget') and out-of-subset content ('unparsable') both
|
|
36
|
+
* ALLOW the pattern: the layer can only under-reject, never over-reject
|
|
37
|
+
* relative to its subset.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately self-contained (its own CharSet/parser copies) so the guard
|
|
40
|
+
* file keeps its committed shape; the duplication is documented isolation,
|
|
41
|
+
* not drift — the parity test in tools/ pins the guard entry points, and
|
|
42
|
+
* this module has its own property test against a brute-force oracle.
|
|
43
|
+
*
|
|
44
|
+
* @module regex-ambiguity
|
|
45
|
+
* @see docs/adr/adr-004-step-budgeted-regex-ambiguity-matcher.md
|
|
46
|
+
*/
|
|
47
|
+
export interface AmbiguityResult {
|
|
48
|
+
readonly verdict: 'ambiguous' | 'unambiguous' | 'unparsable' | 'budget';
|
|
49
|
+
/** Present iff verdict === 'ambiguous': a string with ≥2 decompositions. */
|
|
50
|
+
readonly witness?: string | undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Does `(?:content)+` admit a string with two or more distinct parses?
|
|
54
|
+
* 'ambiguous' is a proof (witness included); 'budget' and 'unparsable'
|
|
55
|
+
* both mean "allow" — the layer can only under-reject, never over-reject.
|
|
56
|
+
*/
|
|
57
|
+
export declare function detectQuantifiedAmbiguity(content: string): AmbiguityResult;
|
|
58
|
+
//# sourceMappingURL=regex-ambiguity.d.ts.map
|