@untangle-code/search 0.0.0-stage → 0.1.1

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.
Files changed (140) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +90 -0
  3. package/README.md +433 -2
  4. package/THIRD_PARTY_LICENSES.md +29 -0
  5. package/dist/benchmark-integrity.d.ts +12 -0
  6. package/dist/benchmark-integrity.d.ts.map +1 -0
  7. package/dist/benchmark-integrity.js +10 -0
  8. package/dist/benchmark-integrity.js.map +1 -0
  9. package/dist/bm25.d.ts +46 -0
  10. package/dist/bm25.d.ts.map +1 -0
  11. package/dist/bm25.js +105 -0
  12. package/dist/bm25.js.map +1 -0
  13. package/dist/deadline.d.ts +4 -0
  14. package/dist/deadline.d.ts.map +1 -0
  15. package/dist/deadline.js +54 -0
  16. package/dist/deadline.js.map +1 -0
  17. package/dist/dedupe.d.ts +48 -0
  18. package/dist/dedupe.d.ts.map +1 -0
  19. package/dist/dedupe.js +58 -0
  20. package/dist/dedupe.js.map +1 -0
  21. package/dist/diversify.d.ts +45 -0
  22. package/dist/diversify.d.ts.map +1 -0
  23. package/dist/diversify.js +30 -0
  24. package/dist/diversify.js.map +1 -0
  25. package/dist/engine.d.ts +157 -0
  26. package/dist/engine.d.ts.map +1 -0
  27. package/dist/engine.js +1498 -0
  28. package/dist/engine.js.map +1 -0
  29. package/dist/enrichment.d.ts +16 -0
  30. package/dist/enrichment.d.ts.map +1 -0
  31. package/dist/enrichment.js +92 -0
  32. package/dist/enrichment.js.map +1 -0
  33. package/dist/filter.d.ts +86 -0
  34. package/dist/filter.d.ts.map +1 -0
  35. package/dist/filter.js +166 -0
  36. package/dist/filter.js.map +1 -0
  37. package/dist/findability.d.ts +144 -0
  38. package/dist/findability.d.ts.map +1 -0
  39. package/dist/findability.js +393 -0
  40. package/dist/findability.js.map +1 -0
  41. package/dist/format.d.ts +28 -0
  42. package/dist/format.d.ts.map +1 -0
  43. package/dist/format.js +172 -0
  44. package/dist/format.js.map +1 -0
  45. package/dist/fusion.d.ts +47 -0
  46. package/dist/fusion.d.ts.map +1 -0
  47. package/dist/fusion.js +84 -0
  48. package/dist/fusion.js.map +1 -0
  49. package/dist/glob.d.ts +50 -0
  50. package/dist/glob.d.ts.map +1 -0
  51. package/dist/glob.js +144 -0
  52. package/dist/glob.js.map +1 -0
  53. package/dist/grep.d.ts +76 -0
  54. package/dist/grep.d.ts.map +1 -0
  55. package/dist/grep.js +475 -0
  56. package/dist/grep.js.map +1 -0
  57. package/dist/index.d.ts +42 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +63 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/ledger.d.ts +45 -0
  62. package/dist/ledger.d.ts.map +1 -0
  63. package/dist/ledger.js +39 -0
  64. package/dist/ledger.js.map +1 -0
  65. package/dist/payload.d.ts +108 -0
  66. package/dist/payload.d.ts.map +1 -0
  67. package/dist/payload.js +263 -0
  68. package/dist/payload.js.map +1 -0
  69. package/dist/port.d.ts +112 -0
  70. package/dist/port.d.ts.map +1 -0
  71. package/dist/port.js +2 -0
  72. package/dist/port.js.map +1 -0
  73. package/dist/product-complexity.d.ts +89 -0
  74. package/dist/product-complexity.d.ts.map +1 -0
  75. package/dist/product-complexity.js +1367 -0
  76. package/dist/product-complexity.js.map +1 -0
  77. package/dist/regex-safety.d.ts +75 -0
  78. package/dist/regex-safety.d.ts.map +1 -0
  79. package/dist/regex-safety.js +902 -0
  80. package/dist/regex-safety.js.map +1 -0
  81. package/dist/relevance-prerequisites.d.ts +21 -0
  82. package/dist/relevance-prerequisites.d.ts.map +1 -0
  83. package/dist/relevance-prerequisites.js +47 -0
  84. package/dist/relevance-prerequisites.js.map +1 -0
  85. package/dist/relevance.d.ts +107 -0
  86. package/dist/relevance.d.ts.map +1 -0
  87. package/dist/relevance.js +199 -0
  88. package/dist/relevance.js.map +1 -0
  89. package/dist/retrieval.d.ts +46 -0
  90. package/dist/retrieval.d.ts.map +1 -0
  91. package/dist/retrieval.js +182 -0
  92. package/dist/retrieval.js.map +1 -0
  93. package/dist/scoring.d.ts +102 -0
  94. package/dist/scoring.d.ts.map +1 -0
  95. package/dist/scoring.js +175 -0
  96. package/dist/scoring.js.map +1 -0
  97. package/dist/sort.d.ts +21 -0
  98. package/dist/sort.d.ts.map +1 -0
  99. package/dist/sort.js +53 -0
  100. package/dist/sort.js.map +1 -0
  101. package/dist/symbol-match.d.ts +21 -0
  102. package/dist/symbol-match.d.ts.map +1 -0
  103. package/dist/symbol-match.js +72 -0
  104. package/dist/symbol-match.js.map +1 -0
  105. package/dist/tokenize.d.ts +41 -0
  106. package/dist/tokenize.d.ts.map +1 -0
  107. package/dist/tokenize.js +202 -0
  108. package/dist/tokenize.js.map +1 -0
  109. package/dist/types.d.ts +173 -0
  110. package/dist/types.d.ts.map +1 -0
  111. package/dist/types.js +42 -0
  112. package/dist/types.js.map +1 -0
  113. package/package.json +61 -3
  114. package/src/benchmark-integrity.ts +25 -0
  115. package/src/bm25.ts +135 -0
  116. package/src/deadline.ts +59 -0
  117. package/src/dedupe.ts +99 -0
  118. package/src/diversify.ts +71 -0
  119. package/src/engine.ts +1957 -0
  120. package/src/enrichment.ts +115 -0
  121. package/src/filter.ts +267 -0
  122. package/src/findability.ts +580 -0
  123. package/src/format.ts +228 -0
  124. package/src/fusion.ts +121 -0
  125. package/src/glob.ts +151 -0
  126. package/src/grep.ts +618 -0
  127. package/src/index.ts +83 -0
  128. package/src/ledger.ts +72 -0
  129. package/src/payload.ts +382 -0
  130. package/src/port.ts +133 -0
  131. package/src/product-complexity.ts +2168 -0
  132. package/src/regex-safety.ts +1002 -0
  133. package/src/relevance-prerequisites.ts +83 -0
  134. package/src/relevance.ts +328 -0
  135. package/src/retrieval.ts +299 -0
  136. package/src/scoring.ts +321 -0
  137. package/src/sort.ts +86 -0
  138. package/src/symbol-match.ts +87 -0
  139. package/src/tokenize.ts +213 -0
  140. package/src/types.ts +206 -0
@@ -0,0 +1,902 @@
1
+ /**
2
+ * Erkennung von Suchmustern, deren Auswertung exponentiell werden kann.
3
+ *
4
+ * WARUM ES DIESE PRUEFUNG UEBERHAUPT GIBT: Die Auswertung eines regulaeren
5
+ * Ausdrucks laeuft in JavaScript als EIN synchroner Aufruf. Solange er laeuft,
6
+ * ist die Ereignisschleife blockiert — ein Timer feuert nicht, ein
7
+ * `AbortSignal` kann nicht geprueft werden, ein Budget „durchsuchte Chunks"
8
+ * wird nicht erreicht. Gemessen: ein einziger Chunk mit einer 31 Zeichen
9
+ * langen Zeile und dem Muster `(a+)+$` hat den Aufruf ueber 88 Sekunden
10
+ * festgehalten, obwohl `maxScannedChunks` 1 war und das Abbruchsignal nach
11
+ * 200 ms gesetzt wurde. Ein gefaehrliches Muster muss deshalb VOR dem ersten
12
+ * Speicherzugriff abgelehnt werden; nachtraeglich abbrechen laesst es sich
13
+ * nicht.
14
+ *
15
+ * WARUM NICHT „VERSCHACHTELTER QUANTOR" ALS KRITERIUM: Die erste Fassung
16
+ * dieser Pruefung suchte eine Wiederholung ueber einer Gruppe, die selbst eine
17
+ * unbegrenzte Wiederholung enthaelt (`(a+)+`). Das ist ein SYNTAKTISCHER
18
+ * Stellvertreter fuer die eigentliche Eigenschaft und trifft sie in beide
19
+ * Richtungen daneben:
20
+ *
21
+ * - `(a|a)+$` und `(\s|\t)+$` enthalten keinen verschachtelten Quantor und
22
+ * liefen trotzdem nie zurueck (gemessen: nach 30 s per SIGALRM getoetet).
23
+ * `(\s|\t)+$` ist dabei ein alltaegliches Muster — Suche nach
24
+ * Zeilenendleerraum.
25
+ * - `(a+b)+` und `(\w+\.)+` wurden abgelehnt, obwohl ihre Auswertung
26
+ * hoechstens polynomiell ist.
27
+ *
28
+ * Die tatsaechliche Ursache exponentieller Laufzeit ist MEHRDEUTIGKEIT unter
29
+ * einer unbegrenzten Wiederholung: `R+` wird exponentiell, wenn ein Wort auf
30
+ * mehr als eine Art durch `R` bzw. durch `R·R` zerlegt werden kann. Genau das
31
+ * prueft dieses Modul — an einem geparsten Baum, nicht an der Zeichenkette.
32
+ * Fuer jede unbegrenzte Wiederholung `R{n,}` gilt der Rumpf `R` als
33
+ * gefaehrlich, wenn
34
+ *
35
+ * (1) `R` das leere Wort matchen kann (`(a*)*`, `(a?)+`),
36
+ * (2) `R` eine Alternative enthaelt, deren Zweige ein GEMEINSAMES Wort
37
+ * matchen koennen (`(a|a)`, `(\s|\t)`),
38
+ * (3a) `R` am Ende unbegrenzt Zeichen aufsaugen kann, die `R` auch beginnen
39
+ * koennen (`a+`, `\w+\s?`), oder
40
+ * (3b) `R` am Anfang unbegrenzt Zeichen aufsaugen kann, die `R` auch beenden
41
+ * koennen (`\w+a`).
42
+ *
43
+ * `(a+b)+` und `(\w+\.)+` fallen durch keine dieser Regeln: der abschliessende
44
+ * Pflichtteil (`b`, `.`) ist ein Trennzeichen, das die Zerlegung eindeutig
45
+ * macht. Sie werden jetzt ausgefuehrt.
46
+ *
47
+ * BEKANNTE GRENZE, ausdruecklich benannt: Das bleibt eine ENTSCHEIDBARE
48
+ * Naeherung eines unentscheidbaren Problems. Sie ist bewusst auf die sichere
49
+ * Seite gebaut — im Zweifel wird abgelehnt (Rueckwaertsreferenzen unter einer
50
+ * Wiederholung, `\p{…}` als „irgendein Zeichen") — aber sie ist kein Beweis.
51
+ * Ein Muster, das sie passiert, kann im schlimmsten Fall noch immer
52
+ * polynomiell teuer sein (etwa `\w+\d+` mit quadratischem Aufwand); nur die
53
+ * exponentielle Familie ist abgedeckt.
54
+ */
55
+ // ---------------------------------------------------------------------------
56
+ // Zeichenmengen
57
+ // ---------------------------------------------------------------------------
58
+ /** Groesster Unicode-Codepoint. */
59
+ const MAX_CODE_POINT = 0x10ffff;
60
+ const EMPTY_SET = { ranges: [] };
61
+ const UNIVERSE = { ranges: [[0, MAX_CODE_POINT]] };
62
+ function setOf(...ranges) {
63
+ return normalizeSet({ ranges });
64
+ }
65
+ function singleton(codePoint) {
66
+ return { ranges: [[codePoint, codePoint]] };
67
+ }
68
+ function normalizeSet(set) {
69
+ const sorted = [...set.ranges]
70
+ .filter(([low, high]) => low <= high)
71
+ .sort((left, right) => left[0] - right[0] || left[1] - right[1]);
72
+ const merged = [];
73
+ for (const [low, high] of sorted) {
74
+ const last = merged[merged.length - 1];
75
+ if (last !== undefined && low <= last[1] + 1) {
76
+ last[1] = Math.max(last[1], high);
77
+ continue;
78
+ }
79
+ merged.push([low, high]);
80
+ }
81
+ return { ranges: merged };
82
+ }
83
+ function unionSets(left, right) {
84
+ return normalizeSet({ ranges: [...left.ranges, ...right.ranges] });
85
+ }
86
+ function complementSet(set) {
87
+ const normalized = normalizeSet(set);
88
+ const ranges = [];
89
+ let next = 0;
90
+ for (const [low, high] of normalized.ranges) {
91
+ if (low > next) {
92
+ ranges.push([next, low - 1]);
93
+ }
94
+ next = Math.max(next, high + 1);
95
+ }
96
+ if (next <= MAX_CODE_POINT) {
97
+ ranges.push([next, MAX_CODE_POINT]);
98
+ }
99
+ return { ranges };
100
+ }
101
+ function setsIntersect(left, right) {
102
+ let leftIndex = 0;
103
+ let rightIndex = 0;
104
+ while (leftIndex < left.ranges.length && rightIndex < right.ranges.length) {
105
+ const a = left.ranges[leftIndex];
106
+ const b = right.ranges[rightIndex];
107
+ if (Math.max(a[0], b[0]) <= Math.min(a[1], b[1])) {
108
+ return true;
109
+ }
110
+ if (a[1] < b[1]) {
111
+ leftIndex += 1;
112
+ }
113
+ else {
114
+ rightIndex += 1;
115
+ }
116
+ }
117
+ return false;
118
+ }
119
+ function isEmptySet(set) {
120
+ return set.ranges.length === 0;
121
+ }
122
+ /**
123
+ * ASCII-Faltung fuer `i`-Suchen. Nur ASCII: eine unvollstaendige Faltung macht
124
+ * die Menge KLEINER als die Wirklichkeit, und eine zu kleine Menge koennte
125
+ * eine Ueberschneidung uebersehen. Deshalb wird bei `i` zusaetzlich jedes
126
+ * Intervall, das Buchstaben beruehrt, auf beide Schreibweisen erweitert — im
127
+ * Zweifel zu gross, also auf der ablehnenden Seite.
128
+ */
129
+ function foldCase(set) {
130
+ const upperA = 0x41;
131
+ const upperZ = 0x5a;
132
+ const lowerA = 0x61;
133
+ const lowerZ = 0x7a;
134
+ const extra = [];
135
+ for (const [low, high] of set.ranges) {
136
+ const upperLow = Math.max(low, upperA);
137
+ const upperHigh = Math.min(high, upperZ);
138
+ if (upperLow <= upperHigh) {
139
+ extra.push([upperLow + 0x20, upperHigh + 0x20]);
140
+ }
141
+ const lowerLow = Math.max(low, lowerA);
142
+ const lowerHigh = Math.min(high, lowerZ);
143
+ if (lowerLow <= lowerHigh) {
144
+ extra.push([lowerLow - 0x20, lowerHigh - 0x20]);
145
+ }
146
+ }
147
+ return extra.length === 0 ? set : unionSets(set, { ranges: extra });
148
+ }
149
+ const DIGIT_SET = setOf([0x30, 0x39]);
150
+ const WORD_SET = setOf([0x30, 0x39], [0x41, 0x5a], [0x5f, 0x5f], [0x61, 0x7a]);
151
+ const SPACE_SET = setOf([0x09, 0x0d], [0x20, 0x20], [0xa0, 0xa0], [0x1680, 0x1680], [0x2000, 0x200a], [0x2028, 0x2029], [0x202f, 0x202f], [0x205f, 0x205f], [0x3000, 0x3000], [0xfeff, 0xfeff]);
152
+ /** `.` ohne `s`-Flag: alles ausser den Zeilentrennern. */
153
+ const DOT_SET = complementSet(setOf([0x0a, 0x0a], [0x0d, 0x0d], [0x2028, 0x2029]));
154
+ class PatternParseError extends Error {
155
+ }
156
+ /**
157
+ * Rekursiv absteigender Parser fuer die Teilmenge der JavaScript-Syntax, die
158
+ * fuer die Mehrdeutigkeitsfrage zaehlt. Was er nicht versteht, wird zu
159
+ * `opaque` oder `chars(UNIVERSE)` — beides faellt in der Auswertung auf die
160
+ * ablehnende Seite. Ein Muster, das er gar nicht parsen kann, wird NICHT
161
+ * abgelehnt: dann ist es ohnehin syntaktisch fehlerhaft und `new RegExp`
162
+ * meldet den echten Fehler.
163
+ */
164
+ class PatternParser {
165
+ pattern;
166
+ caseInsensitive;
167
+ index = 0;
168
+ constructor(pattern, caseInsensitive) {
169
+ this.pattern = pattern;
170
+ this.caseInsensitive = caseInsensitive;
171
+ }
172
+ parse() {
173
+ const node = this.parseAlternation();
174
+ if (this.index < this.pattern.length) {
175
+ throw new PatternParseError(`unexpected "${this.pattern[this.index] ?? ""}"`);
176
+ }
177
+ return node;
178
+ }
179
+ parseAlternation() {
180
+ const branches = [this.parseConcat()];
181
+ while (this.peek() === "|") {
182
+ this.index += 1;
183
+ branches.push(this.parseConcat());
184
+ }
185
+ return branches.length === 1 ? branches[0] : { kind: "alt", branches };
186
+ }
187
+ parseConcat() {
188
+ const parts = [];
189
+ for (;;) {
190
+ const char = this.peek();
191
+ if (char === undefined || char === "|" || char === ")") {
192
+ break;
193
+ }
194
+ parts.push(this.parseQuantified());
195
+ }
196
+ if (parts.length === 0) {
197
+ return { kind: "zeroWidth" };
198
+ }
199
+ return parts.length === 1 ? parts[0] : { kind: "concat", parts };
200
+ }
201
+ parseQuantified() {
202
+ const start = this.index;
203
+ const atom = this.parseAtom();
204
+ const quantifier = this.readQuantifier();
205
+ if (quantifier === undefined) {
206
+ return atom;
207
+ }
208
+ // Faules `?` bzw. `+` (nur ES2025) aendert die Suchreihenfolge, nicht den
209
+ // schlimmsten Fall — es wird uebergangen.
210
+ if (this.peek() === "?" || this.peek() === "+") {
211
+ this.index += 1;
212
+ }
213
+ return {
214
+ kind: "repeat",
215
+ body: atom,
216
+ min: quantifier.min,
217
+ max: quantifier.max,
218
+ text: this.pattern.slice(start, this.index),
219
+ };
220
+ }
221
+ parseAtom() {
222
+ const start = this.index;
223
+ const char = this.peek();
224
+ if (char === undefined) {
225
+ throw new PatternParseError("unexpected end of pattern");
226
+ }
227
+ if (char === "(") {
228
+ return this.parseGroup(start);
229
+ }
230
+ if (char === "[") {
231
+ return { kind: "chars", set: this.parseCharacterClass() };
232
+ }
233
+ if (char === "\\") {
234
+ return this.parseEscape();
235
+ }
236
+ if (char === "^" || char === "$") {
237
+ this.index += 1;
238
+ return { kind: "zeroWidth" };
239
+ }
240
+ if (char === ".") {
241
+ this.index += 1;
242
+ return { kind: "chars", set: this.applyCase(DOT_SET) };
243
+ }
244
+ if (char === "*" || char === "+" || char === "?") {
245
+ throw new PatternParseError(`quantifier "${char}" without a preceding atom`);
246
+ }
247
+ const codePoint = this.pattern.codePointAt(this.index);
248
+ if (codePoint === undefined) {
249
+ throw new PatternParseError("unexpected end of pattern");
250
+ }
251
+ this.index += String.fromCodePoint(codePoint).length;
252
+ return { kind: "chars", set: this.applyCase(singleton(codePoint)) };
253
+ }
254
+ parseGroup(start) {
255
+ this.index += 1; // "("
256
+ let isLookaround = false;
257
+ if (this.peek() === "?") {
258
+ const next = this.pattern[this.index + 1];
259
+ if (next === ":") {
260
+ this.index += 2;
261
+ }
262
+ else if (next === "=" || next === "!") {
263
+ this.index += 2;
264
+ isLookaround = true;
265
+ }
266
+ else if (next === "<" &&
267
+ (this.pattern[this.index + 2] === "=" || this.pattern[this.index + 2] === "!")) {
268
+ this.index += 3;
269
+ isLookaround = true;
270
+ }
271
+ else if (next === "<") {
272
+ const close = this.pattern.indexOf(">", this.index + 2);
273
+ if (close === -1) {
274
+ throw new PatternParseError("unterminated group name");
275
+ }
276
+ this.index = close + 1;
277
+ }
278
+ else {
279
+ throw new PatternParseError(`unsupported group prefix "(?${next ?? ""}"`);
280
+ }
281
+ }
282
+ const body = this.parseAlternation();
283
+ if (this.peek() !== ")") {
284
+ throw new PatternParseError("unbalanced parenthesis");
285
+ }
286
+ this.index += 1;
287
+ if (isLookaround) {
288
+ // Nullbreite: ein Lookaround verbraucht kein Zeichen und traegt deshalb
289
+ // weder zur Zerlegung noch zur Mehrdeutigkeit von `R·R` bei. Ihn
290
+ // pauschal abzulehnen wuerde alltaegliche Muster wie `(?:(?!foo)\w)+`
291
+ // treffen, die nachweislich eindeutig sind.
292
+ return { kind: "zeroWidth" };
293
+ }
294
+ return { kind: "group", body, text: this.pattern.slice(start, this.index) };
295
+ }
296
+ parseEscape() {
297
+ this.index += 1; // "\\"
298
+ const char = this.peek();
299
+ if (char === undefined) {
300
+ throw new PatternParseError("pattern ends with a backslash");
301
+ }
302
+ this.index += 1;
303
+ switch (char) {
304
+ case "d":
305
+ return { kind: "chars", set: DIGIT_SET };
306
+ case "D":
307
+ return { kind: "chars", set: complementSet(DIGIT_SET) };
308
+ case "w":
309
+ return { kind: "chars", set: this.applyCase(WORD_SET) };
310
+ case "W":
311
+ return { kind: "chars", set: complementSet(WORD_SET) };
312
+ case "s":
313
+ return { kind: "chars", set: SPACE_SET };
314
+ case "S":
315
+ return { kind: "chars", set: complementSet(SPACE_SET) };
316
+ case "b":
317
+ case "B":
318
+ return { kind: "zeroWidth" };
319
+ case "t":
320
+ return { kind: "chars", set: singleton(0x09) };
321
+ case "n":
322
+ return { kind: "chars", set: singleton(0x0a) };
323
+ case "r":
324
+ return { kind: "chars", set: singleton(0x0d) };
325
+ case "f":
326
+ return { kind: "chars", set: singleton(0x0c) };
327
+ case "v":
328
+ return { kind: "chars", set: singleton(0x0b) };
329
+ case "0":
330
+ return { kind: "chars", set: singleton(0x00) };
331
+ case "p":
332
+ case "P": {
333
+ // `\p{…}` ist EIN Zeichen, dessen Menge hier nicht aufgeloest wird:
334
+ // als „irgendein Zeichen" gelesen ueberschneidet es sich mit allem und
335
+ // faellt damit auf die ablehnende Seite.
336
+ this.skipBracedArgument();
337
+ return { kind: "chars", set: UNIVERSE };
338
+ }
339
+ case "k": {
340
+ this.skipAngledArgument();
341
+ return { kind: "opaque", text: "\\k<…>" };
342
+ }
343
+ case "u":
344
+ case "x": {
345
+ const codePoint = this.readHexEscape(char);
346
+ return { kind: "chars", set: this.applyCase(singleton(codePoint)) };
347
+ }
348
+ case "c": {
349
+ this.index += 1;
350
+ return { kind: "chars", set: UNIVERSE };
351
+ }
352
+ default: {
353
+ if (char >= "1" && char <= "9") {
354
+ while (/[0-9]/u.test(this.peek() ?? "")) {
355
+ this.index += 1;
356
+ }
357
+ return { kind: "opaque", text: `\\${char}` };
358
+ }
359
+ return { kind: "chars", set: this.applyCase(singleton(char.codePointAt(0))) };
360
+ }
361
+ }
362
+ }
363
+ readHexEscape(kind) {
364
+ if (kind === "u" && this.peek() === "{") {
365
+ const close = this.pattern.indexOf("}", this.index);
366
+ if (close === -1) {
367
+ throw new PatternParseError("unterminated \\u{…}");
368
+ }
369
+ const value = Number.parseInt(this.pattern.slice(this.index + 1, close), 16);
370
+ this.index = close + 1;
371
+ return Number.isNaN(value) ? 0 : value;
372
+ }
373
+ const width = kind === "u" ? 4 : 2;
374
+ const text = this.pattern.slice(this.index, this.index + width);
375
+ if (text.length < width || !/^[0-9a-fA-F]+$/u.test(text)) {
376
+ throw new PatternParseError(`malformed \\${kind} escape`);
377
+ }
378
+ this.index += width;
379
+ return Number.parseInt(text, 16);
380
+ }
381
+ skipBracedArgument() {
382
+ if (this.peek() !== "{") {
383
+ throw new PatternParseError("\\p must be followed by {…}");
384
+ }
385
+ const close = this.pattern.indexOf("}", this.index);
386
+ if (close === -1) {
387
+ throw new PatternParseError("unterminated \\p{…}");
388
+ }
389
+ this.index = close + 1;
390
+ }
391
+ skipAngledArgument() {
392
+ if (this.peek() !== "<") {
393
+ throw new PatternParseError("\\k must be followed by <…>");
394
+ }
395
+ const close = this.pattern.indexOf(">", this.index);
396
+ if (close === -1) {
397
+ throw new PatternParseError("unterminated \\k<…>");
398
+ }
399
+ this.index = close + 1;
400
+ }
401
+ parseCharacterClass() {
402
+ this.index += 1; // "["
403
+ let negated = false;
404
+ if (this.peek() === "^") {
405
+ negated = true;
406
+ this.index += 1;
407
+ }
408
+ let set = EMPTY_SET;
409
+ let pendingLow;
410
+ let closed = false;
411
+ while (this.index < this.pattern.length) {
412
+ if (this.peek() === "]") {
413
+ this.index += 1;
414
+ closed = true;
415
+ break;
416
+ }
417
+ const item = this.readClassItem();
418
+ if (item.set !== undefined) {
419
+ // Eine Klassen-Kurzform (`\d`) kann kein Bereichsende sein.
420
+ if (pendingLow !== undefined) {
421
+ set = unionSets(set, singleton(pendingLow));
422
+ pendingLow = undefined;
423
+ }
424
+ set = unionSets(set, item.set);
425
+ continue;
426
+ }
427
+ const codePoint = item.codePoint;
428
+ if (pendingLow !== undefined) {
429
+ set = unionSets(set, setOf([pendingLow, codePoint]));
430
+ pendingLow = undefined;
431
+ continue;
432
+ }
433
+ if (this.peek() === "-" && this.pattern[this.index + 1] !== "]") {
434
+ this.index += 1;
435
+ pendingLow = codePoint;
436
+ continue;
437
+ }
438
+ set = unionSets(set, singleton(codePoint));
439
+ }
440
+ if (!closed) {
441
+ throw new PatternParseError("unterminated character class");
442
+ }
443
+ if (pendingLow !== undefined) {
444
+ set = unionSets(set, singleton(pendingLow));
445
+ }
446
+ const resolved = negated ? complementSet(set) : set;
447
+ return this.applyCase(resolved);
448
+ }
449
+ readClassItem() {
450
+ if (this.peek() !== "\\") {
451
+ const codePoint = this.pattern.codePointAt(this.index);
452
+ this.index += String.fromCodePoint(codePoint).length;
453
+ return { codePoint };
454
+ }
455
+ const node = this.parseEscape();
456
+ if (node.kind === "chars") {
457
+ if (node.set.ranges.length === 1) {
458
+ const [low, high] = node.set.ranges[0];
459
+ if (low === high) {
460
+ return { codePoint: low };
461
+ }
462
+ }
463
+ return { set: node.set };
464
+ }
465
+ // `\b` heisst in einer Klasse Backspace; alles Uebrige (Rueckwaerts-
466
+ // referenzen gibt es hier nicht) wird als „irgendein Zeichen" gelesen.
467
+ return { set: UNIVERSE };
468
+ }
469
+ readQuantifier() {
470
+ const char = this.peek();
471
+ if (char === "*") {
472
+ this.index += 1;
473
+ return { min: 0, max: Number.POSITIVE_INFINITY };
474
+ }
475
+ if (char === "+") {
476
+ this.index += 1;
477
+ return { min: 1, max: Number.POSITIVE_INFINITY };
478
+ }
479
+ if (char === "?") {
480
+ this.index += 1;
481
+ return { min: 0, max: 1 };
482
+ }
483
+ if (char !== "{") {
484
+ return undefined;
485
+ }
486
+ const close = this.pattern.indexOf("}", this.index + 1);
487
+ if (close === -1) {
488
+ return undefined; // `{` als Literal
489
+ }
490
+ const body = this.pattern.slice(this.index + 1, close);
491
+ const match = /^(\d+)(,(\d*)?)?$/u.exec(body);
492
+ if (match === null) {
493
+ return undefined; // `{` als Literal
494
+ }
495
+ const min = Number(match[1]);
496
+ const hasComma = match[2] !== undefined;
497
+ const maxText = match[3];
498
+ const max = hasComma && (maxText === undefined || maxText === "")
499
+ ? Number.POSITIVE_INFINITY
500
+ : hasComma
501
+ ? Number(maxText)
502
+ : min;
503
+ this.index = close + 1;
504
+ return { min, max };
505
+ }
506
+ applyCase(set) {
507
+ return this.caseInsensitive ? foldCase(set) : set;
508
+ }
509
+ peek() {
510
+ return this.pattern[this.index];
511
+ }
512
+ }
513
+ // ---------------------------------------------------------------------------
514
+ // Auswertung
515
+ // ---------------------------------------------------------------------------
516
+ /** Kann der Knoten das leere Wort matchen? */
517
+ function isNullable(node) {
518
+ switch (node.kind) {
519
+ case "chars":
520
+ return false;
521
+ case "zeroWidth":
522
+ return true;
523
+ case "opaque":
524
+ return true; // eine Rueckwaertsreferenz kann leer sein
525
+ case "concat":
526
+ return node.parts.every(isNullable);
527
+ case "alt":
528
+ return node.branches.some(isNullable);
529
+ case "repeat":
530
+ return node.min === 0 || isNullable(node.body);
531
+ case "group":
532
+ return isNullable(node.body);
533
+ }
534
+ }
535
+ /** Alle Zeichen, die der Knoten ueberhaupt verbrauchen kann (Ueberschaetzung). */
536
+ function consumableSet(node) {
537
+ switch (node.kind) {
538
+ case "chars":
539
+ return node.set;
540
+ case "zeroWidth":
541
+ return EMPTY_SET;
542
+ case "opaque":
543
+ return UNIVERSE;
544
+ case "concat":
545
+ return node.parts.reduce((acc, part) => unionSets(acc, consumableSet(part)), EMPTY_SET);
546
+ case "alt":
547
+ return node.branches.reduce((acc, branch) => unionSets(acc, consumableSet(branch)), EMPTY_SET);
548
+ case "repeat":
549
+ return node.max === 0 ? EMPTY_SET : consumableSet(node.body);
550
+ case "group":
551
+ return consumableSet(node.body);
552
+ }
553
+ }
554
+ /** Zeichen, mit denen ein Treffer des Knotens beginnen kann. */
555
+ function firstSet(node) {
556
+ return edgeSet(node, "first");
557
+ }
558
+ /** Zeichen, mit denen ein Treffer des Knotens enden kann. */
559
+ function lastSet(node) {
560
+ return edgeSet(node, "last");
561
+ }
562
+ function edgeSet(node, side) {
563
+ switch (node.kind) {
564
+ case "chars":
565
+ return node.set;
566
+ case "zeroWidth":
567
+ return EMPTY_SET;
568
+ case "opaque":
569
+ return UNIVERSE;
570
+ case "alt":
571
+ return node.branches.reduce((acc, branch) => unionSets(acc, edgeSet(branch, side)), EMPTY_SET);
572
+ case "group":
573
+ return edgeSet(node.body, side);
574
+ case "repeat":
575
+ return node.max === 0 ? EMPTY_SET : edgeSet(node.body, side);
576
+ case "concat": {
577
+ const order = side === "first" ? node.parts : [...node.parts].reverse();
578
+ let acc = EMPTY_SET;
579
+ for (const part of order) {
580
+ acc = unionSets(acc, edgeSet(part, side));
581
+ if (!isNullable(part)) {
582
+ break;
583
+ }
584
+ }
585
+ return acc;
586
+ }
587
+ }
588
+ }
589
+ /**
590
+ * Zeichen, die der Knoten an seinem Rand UNBEGRENZT aufsaugen kann.
591
+ *
592
+ * Das ist die Menge, die eine Zerlegung von `R·R` mehrdeutig macht: kann `R`
593
+ * am Ende beliebig viele Zeichen schlucken, die `R` auch beginnen koennen,
594
+ * dann gibt es zu einem Wort mehr als eine Aufteilung — und genau daraus
595
+ * entsteht das exponentielle Zurueckziehen.
596
+ */
597
+ function unboundedEdgeSet(node, side) {
598
+ switch (node.kind) {
599
+ case "chars":
600
+ case "zeroWidth":
601
+ case "opaque":
602
+ return EMPTY_SET;
603
+ case "alt":
604
+ return node.branches.reduce((acc, branch) => unionSets(acc, unboundedEdgeSet(branch, side)), EMPTY_SET);
605
+ case "group":
606
+ return unboundedEdgeSet(node.body, side);
607
+ case "repeat":
608
+ return node.max === Number.POSITIVE_INFINITY
609
+ ? unionSets(consumableSet(node.body), unboundedEdgeSet(node.body, side))
610
+ : unboundedEdgeSet(node.body, side);
611
+ case "concat": {
612
+ const order = side === "first" ? node.parts : [...node.parts].reverse();
613
+ let acc = EMPTY_SET;
614
+ for (const part of order) {
615
+ acc = unionSets(acc, unboundedEdgeSet(part, side));
616
+ if (!isNullable(part)) {
617
+ break;
618
+ }
619
+ }
620
+ return acc;
621
+ }
622
+ }
623
+ }
624
+ /** Feste Laenge eines Knotens in Zeichen, oder `undefined` bei Variabilitaet. */
625
+ function fixedLength(node) {
626
+ switch (node.kind) {
627
+ case "chars":
628
+ return 1;
629
+ case "zeroWidth":
630
+ return 0;
631
+ case "opaque":
632
+ return undefined;
633
+ case "concat": {
634
+ let total = 0;
635
+ for (const part of node.parts) {
636
+ const length = fixedLength(part);
637
+ if (length === undefined) {
638
+ return undefined;
639
+ }
640
+ total += length;
641
+ }
642
+ return total;
643
+ }
644
+ case "alt": {
645
+ let common;
646
+ for (const branch of node.branches) {
647
+ const length = fixedLength(branch);
648
+ if (length === undefined || (common !== undefined && common !== length)) {
649
+ return undefined;
650
+ }
651
+ common = length;
652
+ }
653
+ return common;
654
+ }
655
+ case "repeat":
656
+ if (node.min !== node.max) {
657
+ return undefined;
658
+ }
659
+ {
660
+ const length = fixedLength(node.body);
661
+ return length === undefined ? undefined : length * node.min;
662
+ }
663
+ case "group":
664
+ return fixedLength(node.body);
665
+ }
666
+ }
667
+ /**
668
+ * Die Zeichenmenge an Position `offset` eines Knotens FESTER Laenge.
669
+ * Nur fuer solche Knoten definiert; sonst `UNIVERSE` (ablehnende Seite).
670
+ */
671
+ function setAtPosition(node, offset) {
672
+ switch (node.kind) {
673
+ case "chars":
674
+ return offset === 0 ? node.set : EMPTY_SET;
675
+ case "zeroWidth":
676
+ return EMPTY_SET;
677
+ case "opaque":
678
+ return UNIVERSE;
679
+ case "group":
680
+ return setAtPosition(node.body, offset);
681
+ case "alt":
682
+ return node.branches.reduce((acc, branch) => unionSets(acc, setAtPosition(branch, offset)), EMPTY_SET);
683
+ case "concat": {
684
+ let consumed = 0;
685
+ for (const part of node.parts) {
686
+ const length = fixedLength(part);
687
+ if (length === undefined) {
688
+ return UNIVERSE;
689
+ }
690
+ if (offset < consumed + length) {
691
+ return setAtPosition(part, offset - consumed);
692
+ }
693
+ consumed += length;
694
+ }
695
+ return EMPTY_SET;
696
+ }
697
+ case "repeat": {
698
+ const bodyLength = fixedLength(node.body);
699
+ if (bodyLength === undefined || bodyLength === 0) {
700
+ return UNIVERSE;
701
+ }
702
+ return setAtPosition(node.body, offset % bodyLength);
703
+ }
704
+ }
705
+ }
706
+ /**
707
+ * Koennen zwei Alternativzweige unter einer Wiederholung eine mehrdeutige
708
+ * Zerlegung erzeugen?
709
+ *
710
+ * GLEICHE FESTE LAENGE ist exakt entscheidbar: ein gemeinsames Wort gibt es
711
+ * genau dann, wenn sich die Zeichenmengen an JEDER Position ueberschneiden.
712
+ * `(a|b)` und `(ab|ac)` fallen damit durch — zu Recht.
713
+ *
714
+ * VERSCHIEDENE FESTE LAENGE ist der Fall, an dem eine reine „gemeinsames
715
+ * Wort"-Pruefung zu kurz greift: `(x|xx)+` hat keine zwei Zweige mit
716
+ * gemeinsamem Wort und ist trotzdem exponentiell, weil der kuerzere Zweig ein
717
+ * PRAEFIX des laengeren ist und `n` Zeichen sich damit auf Fibonacci-viele
718
+ * Arten zerlegen lassen. Geprueft wird deshalb das Praefix — das ist der
719
+ * Ansatz von Sardinas/Patterson in seiner ersten Runde. Der Preis ist eine
720
+ * bewusste Ueber-Sperrung: `(a|ab)+` wird abgelehnt, obwohl es eindeutig ist.
721
+ *
722
+ * VARIABLE LAENGE laesst sich hier nicht mehr entscheiden; dann entscheiden
723
+ * die Anfangszeichen, wieder auf der ablehnenden Seite.
724
+ */
725
+ function branchesCanShareWord(left, right) {
726
+ if (isNullable(left) && isNullable(right)) {
727
+ return true;
728
+ }
729
+ const leftLength = fixedLength(left);
730
+ const rightLength = fixedLength(right);
731
+ if (leftLength !== undefined && rightLength !== undefined) {
732
+ const shorter = leftLength <= rightLength ? left : right;
733
+ const longer = leftLength <= rightLength ? right : left;
734
+ const overlap = Math.min(leftLength, rightLength);
735
+ for (let offset = 0; offset < overlap; offset += 1) {
736
+ if (!setsIntersect(setAtPosition(shorter, offset), setAtPosition(longer, offset))) {
737
+ return false;
738
+ }
739
+ }
740
+ return true;
741
+ }
742
+ return setsIntersect(firstSet(left), firstSet(right));
743
+ }
744
+ /** Sammelt alle Alternativknoten unterhalb (und einschliesslich) `node`. */
745
+ function collectAlternations(node, into) {
746
+ switch (node.kind) {
747
+ case "alt":
748
+ into.push(node);
749
+ node.branches.forEach((branch) => collectAlternations(branch, into));
750
+ return;
751
+ case "concat":
752
+ node.parts.forEach((part) => collectAlternations(part, into));
753
+ return;
754
+ case "group":
755
+ case "repeat":
756
+ collectAlternations(node.body, into);
757
+ return;
758
+ default:
759
+ return;
760
+ }
761
+ }
762
+ /** Enthaelt der Teilbaum eine Rueckwaertsreferenz? */
763
+ function containsBackreference(node) {
764
+ switch (node.kind) {
765
+ case "opaque":
766
+ return true;
767
+ case "concat":
768
+ return node.parts.some(containsBackreference);
769
+ case "alt":
770
+ return node.branches.some(containsBackreference);
771
+ case "group":
772
+ case "repeat":
773
+ return containsBackreference(node.body);
774
+ default:
775
+ return false;
776
+ }
777
+ }
778
+ function describe(node) {
779
+ if (node.kind === "group" || node.kind === "repeat") {
780
+ return node.text;
781
+ }
782
+ return "the repeated subexpression";
783
+ }
784
+ /**
785
+ * Begruendung, warum der Rumpf einer unbegrenzten Wiederholung mehrdeutig ist
786
+ * — oder `undefined`, wenn er es nachweislich nicht ist.
787
+ */
788
+ function ambiguityReason(body, repeatText) {
789
+ if (isNullable(body)) {
790
+ return `the repeated subexpression in "${repeatText}" can match the empty string, so a word has unboundedly many decompositions`;
791
+ }
792
+ if (containsBackreference(body)) {
793
+ return `the repeated subexpression in "${repeatText}" contains a backreference, whose matched length is unknown; ambiguity cannot be ruled out`;
794
+ }
795
+ const alternations = [];
796
+ collectAlternations(body, alternations);
797
+ for (const alternation of alternations) {
798
+ if (alternation.kind !== "alt") {
799
+ continue;
800
+ }
801
+ for (let left = 0; left < alternation.branches.length; left += 1) {
802
+ for (let right = left + 1; right < alternation.branches.length; right += 1) {
803
+ const a = alternation.branches[left];
804
+ const b = alternation.branches[right];
805
+ if (branchesCanShareWord(a, b)) {
806
+ return `two alternatives inside "${repeatText}" can match the same text, so the repetition can split a word in more than one way`;
807
+ }
808
+ }
809
+ }
810
+ }
811
+ const tail = unboundedEdgeSet(body, "last");
812
+ if (!isEmptySet(tail) && setsIntersect(tail, firstSet(body))) {
813
+ return `"${repeatText}" can absorb characters at the end of one repetition that could equally start the next one, so a word has more than one decomposition`;
814
+ }
815
+ const head = unboundedEdgeSet(body, "first");
816
+ if (!isEmptySet(head) && setsIntersect(head, lastSet(body))) {
817
+ return `"${repeatText}" can absorb characters at the start of one repetition that could equally end the previous one, so a word has more than one decomposition`;
818
+ }
819
+ return undefined;
820
+ }
821
+ /** Durchsucht den Baum nach einer gefaehrlichen Wiederholung. */
822
+ function findAmbiguousRepetition(node) {
823
+ switch (node.kind) {
824
+ case "repeat": {
825
+ // `max > 1` genau wie zuvor: schon `(a+){2}` verdoppelt den Aufwand je
826
+ // Schachtelungsebene, und `{2,}` ist ohnehin unbegrenzt.
827
+ if (node.max > 1) {
828
+ const reason = ambiguityReason(node.body, describe(node));
829
+ if (reason !== undefined) {
830
+ return reason;
831
+ }
832
+ }
833
+ return findAmbiguousRepetition(node.body);
834
+ }
835
+ case "group":
836
+ return findAmbiguousRepetition(node.body);
837
+ case "concat": {
838
+ for (const part of node.parts) {
839
+ const reason = findAmbiguousRepetition(part);
840
+ if (reason !== undefined) {
841
+ return reason;
842
+ }
843
+ }
844
+ return undefined;
845
+ }
846
+ case "alt": {
847
+ for (const branch of node.branches) {
848
+ const reason = findAmbiguousRepetition(branch);
849
+ if (reason !== undefined) {
850
+ return reason;
851
+ }
852
+ }
853
+ return undefined;
854
+ }
855
+ default:
856
+ return undefined;
857
+ }
858
+ }
859
+ /**
860
+ * Beschreibt, warum ein Muster als unsicher gilt — oder `undefined`, wenn die
861
+ * Pruefung keine exponentielle Mehrdeutigkeit findet.
862
+ *
863
+ * Ein Muster, das der Parser nicht versteht, gilt hier als sicher: es ist dann
864
+ * syntaktisch fehlerhaft, und `new RegExp` meldet den zutreffenden Fehler,
865
+ * statt dass diese Pruefung eine falsche Begruendung erfindet (E4).
866
+ */
867
+ export function findUnsafeGrepPattern(pattern, options = {}) {
868
+ const caseInsensitive = options.caseInsensitive === true;
869
+ let tree;
870
+ try {
871
+ tree = new PatternParser(pattern, caseInsensitive).parse();
872
+ }
873
+ catch {
874
+ // WICHTIG: „mein Parser hat es nicht verstanden" heisst NICHT „das Muster
875
+ // ist harmlos". Ohne diese Unterscheidung waere jede Luecke im Parser eine
876
+ // stille Luecke in der Sicherheitspruefung — das Muster liefe ungeprueft.
877
+ // Also wird gegenueber der Sprache selbst geprueft: haelt `RegExp` das
878
+ // Muster fuer gueltig, ist es GUELTIG UND UNANALYSIERBAR und wird
879
+ // abgelehnt. Haelt `RegExp` es fuer fehlerhaft, ist der Syntaxfehler die
880
+ // zutreffende Meldung, und diese Pruefung erfindet keine zweite (E4).
881
+ try {
882
+ new RegExp(pattern, caseInsensitive ? "ui" : "u");
883
+ }
884
+ catch {
885
+ return undefined;
886
+ }
887
+ return `the pattern uses a construct this safety check cannot analyse, so exponential backtracking cannot be ruled out`;
888
+ }
889
+ return findAmbiguousRepetition(tree);
890
+ }
891
+ /**
892
+ * Wirft, wenn das Muster exponentielles Zurueckziehen ausloesen kann.
893
+ *
894
+ * Siehe den Modulkopf fuer das Kriterium und seine ausdrueckliche Grenze.
895
+ */
896
+ export function assertSafeGrepPattern(pattern, options = {}) {
897
+ const unsafe = findUnsafeGrepPattern(pattern, options);
898
+ if (unsafe !== undefined) {
899
+ throw new Error(`grep pattern is rejected as unsafe: ${unsafe}. Such a repetition can take exponential time to evaluate, and that evaluation cannot be interrupted — not by maxScannedChunks and not by an abort signal. Rewrite the repeated part so that each repetition has exactly one possible decomposition (for example by ending it with a character the repetition itself cannot match).`);
900
+ }
901
+ }
902
+ //# sourceMappingURL=regex-safety.js.map