poe-code 14.0.73 → 14.0.75

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 (47) hide show
  1. package/dist/index.js +2732 -1188
  2. package/dist/index.js.map +4 -4
  3. package/dist/metafile.json +1 -1
  4. package/package.json +1 -1
  5. package/packages/safe-js/dist/chunks/{chunk-I2Z37MKZ.js → chunk-BAJI3K4B.js} +2 -2
  6. package/packages/safe-js/dist/chunks/{chunk-XKCTB5FR.js → chunk-EOTY4PCT.js} +2902 -1425
  7. package/packages/safe-js/dist/chunks/chunk-EOTY4PCT.js.map +7 -0
  8. package/packages/safe-js/dist/cli.js +2 -2
  9. package/packages/safe-js/dist/core.js +1 -1
  10. package/packages/safe-js/dist/graph-depth.js +3 -0
  11. package/packages/safe-js/dist/index.js +2 -2
  12. package/packages/safe-js/dist/interp/cancel.js +3 -0
  13. package/packages/safe-js/dist/interp/globals/misc.js +2 -1
  14. package/packages/safe-js/dist/interp/globals/object.js +3 -0
  15. package/packages/safe-js/dist/interp/globals/regex.js +196 -9
  16. package/packages/safe-js/dist/interp/interpreter.js +14 -7
  17. package/packages/safe-js/dist/interp/iteration.js +9 -1
  18. package/packages/safe-js/dist/interp/methods/regex-split.d.ts +3 -0
  19. package/packages/safe-js/dist/interp/methods/regex-split.js +102 -0
  20. package/packages/safe-js/dist/interp/methods/regex.d.ts +8 -4
  21. package/packages/safe-js/dist/interp/methods/regex.js +141 -24
  22. package/packages/safe-js/dist/interp/methods/regexp-iterator.d.ts +12 -0
  23. package/packages/safe-js/dist/interp/methods/regexp-iterator.js +101 -0
  24. package/packages/safe-js/dist/interp/methods/string.d.ts +2 -0
  25. package/packages/safe-js/dist/interp/methods/string.js +415 -121
  26. package/packages/safe-js/dist/interp/object-model.d.ts +3 -0
  27. package/packages/safe-js/dist/interp/object-model.js +43 -10
  28. package/packages/safe-js/dist/interp/regex/compile-guard.js +26 -4
  29. package/packages/safe-js/dist/interp/regex/engine.d.ts +4 -0
  30. package/packages/safe-js/dist/interp/regex/engine.js +230 -30
  31. package/packages/safe-js/dist/interp/regex/parse.d.ts +42 -4
  32. package/packages/safe-js/dist/interp/regex/parse.js +429 -25
  33. package/packages/safe-js/dist/interp/regexp-iterator.d.ts +16 -0
  34. package/packages/safe-js/dist/interp/regexp-iterator.js +23 -0
  35. package/packages/safe-js/dist/interp/string-coercion.js +11 -7
  36. package/packages/safe-js/dist/interp/values.d.ts +2 -1
  37. package/packages/safe-js/dist/interp/values.js +39 -2
  38. package/packages/safe-js/dist/snapshot/dump-format.js +35 -3
  39. package/packages/safe-js/dist/snapshot/replay-data.d.ts +12 -1
  40. package/packages/safe-js/dist/snapshot/replay-data.js +42 -1
  41. package/packages/safe-js/dist/snapshot/replay-inputs.js +17 -0
  42. package/packages/safe-js/dist/snapshot/restore.js +23 -1
  43. package/packages/safe-js/dist/snapshot/serialize.d.ts +13 -2
  44. package/packages/safe-js/dist/snapshot/serialize.js +30 -11
  45. package/packages/safe-js/dist/snapshot/validation.js +22 -2
  46. package/packages/safe-js/dist/chunks/chunk-XKCTB5FR.js.map +0 -7
  47. /package/packages/safe-js/dist/chunks/{chunk-I2Z37MKZ.js.map → chunk-BAJI3K4B.js.map} +0 -0
@@ -1,13 +1,16 @@
1
1
  import { types } from "node:util";
2
2
  import { invokeBuiltinClosure } from "../builtin-call.js";
3
3
  import { CompileScope } from "../regex/compile-guard.js";
4
- import { normalizeLastIndex } from "../regex/engine.js";
4
+ import { advanceStringIndex, normalizeLastIndex } from "../regex/engine.js";
5
5
  import { sandboxNumber, sandboxString } from "../string-coercion.js";
6
6
  import { retainValues } from "../resources.js";
7
- import { getSandboxDataProperty, getSandboxPropertyDescriptor } from "../object-model.js";
8
- import { createSandboxClosure, createSandboxRegex, deepCopyFromSandbox, isSandboxClosure, isSandboxRegex } from "../values.js";
9
- import { executeRegex, toMatchArray } from "./regex.js";
10
- const SPLIT_STRING_MESSAGE = "String#split only supports string separator values.";
7
+ import { getSandboxDataProperty, getSandboxPropertyDescriptor, hasRegexPropertyOverride } from "../object-model.js";
8
+ import { readPropertyDescriptor } from "../accessors.js";
9
+ import { createSandboxBox } from "../boxed.js";
10
+ import { setSandboxProperty } from "../interpreter.js";
11
+ import { createSandboxClosure, createSandboxRegex, deepCopyFromSandbox, getSandboxRegexPattern, isSandboxClosure, isSandboxRegex, measureSandboxData } from "../values.js";
12
+ import { executeRegex, getRegexMember, regexExec, regexFlagProperties, regexSearch, toMatchArray } from "./regex.js";
13
+ import { restoreSandboxRegExpIterator } from "../regexp-iterator.js";
11
14
  export const stringMethodNames = new Set([
12
15
  "at",
13
16
  "charAt",
@@ -87,9 +90,19 @@ export function validateStringMethodArguments(_methodName, _args) { }
87
90
  export function callStringMethod(value, methodName, args, budget, callClosure = async (closure, closureArgs) => await closure.call(closureArgs), parent, context) {
88
91
  if (value === null || value === undefined)
89
92
  throw new TypeError(`String#${methodName} requires a non-null receiver.`);
90
- const fallback = () => typeof value === "string"
91
- ? callStringMethodBody(value, methodName, args, budget, callClosure, parent, context)
92
- : Promise.resolve(sandboxString(value, budget, context)).then(string => callStringMethodBody(string, methodName, args, budget, callClosure, parent, context));
93
+ const fallback = (coercePattern = false) => {
94
+ const apply = (string) => {
95
+ if (coercePattern && (methodName === "match" || methodName === "matchAll" || methodName === "search"))
96
+ return callStringPattern(string, methodName, args[0], budget, parent, context);
97
+ const useRegex = isSandboxRegex(args[0]) && !coercePattern;
98
+ if (methodName === "replace" || methodName === "replaceAll")
99
+ return callReplaceLikeMethod(string, methodName, args, budget, callClosure, useRegex, context);
100
+ if (methodName === "split")
101
+ return callSplit(string, args, budget, useRegex, parent, context);
102
+ return callStringMethodBody(string, methodName, args, budget, parent, context);
103
+ };
104
+ return typeof value === "string" ? apply(value) : Promise.resolve(sandboxString(value, budget, context)).then(apply);
105
+ };
93
106
  const symbol = methodName === "match" ? Symbol.match
94
107
  : methodName === "search" ? Symbol.search
95
108
  : methodName === "matchAll" ? Symbol.matchAll
@@ -100,18 +113,19 @@ export function callStringMethod(value, methodName, args, budget, callClosure =
100
113
  throw new TypeError(`String#${methodName} does not accept unbranded host RegExp values.`);
101
114
  if (symbol === undefined || pattern === null || pattern === undefined)
102
115
  return fallback();
103
- const applyHook = (hook) => {
104
- if (hook === null || hook === undefined)
105
- return fallback();
106
- if (!isSandboxClosure(hook))
107
- throw new TypeError(`String#${methodName} symbol hook must be callable.`);
108
- return invokeBuiltinClosure(hook, symbol === Symbol.split || symbol === Symbol.replace ? [value, args[1]] : [value], budget, context, pattern);
109
- };
110
116
  const readProperty = (key) => context?.getProperty !== undefined
111
117
  ? context.getProperty(pattern, key)
112
118
  : key === "flags" && isSandboxRegex(pattern) && getSandboxPropertyDescriptor(pattern, key, budget) === undefined
113
- ? pattern.flags : getSandboxDataProperty(pattern, key, budget);
119
+ ? getRegexMember(pattern, key, budget, context) : getSandboxDataProperty(pattern, key, budget);
114
120
  const dispatch = () => {
121
+ const overriddenRegex = isSandboxRegex(pattern) && getSandboxPropertyDescriptor(pattern, symbol, budget) !== undefined;
122
+ const applyHook = (hook) => {
123
+ if (hook === null || hook === undefined)
124
+ return fallback(overriddenRegex);
125
+ if (!isSandboxClosure(hook))
126
+ throw new TypeError(`String#${methodName} symbol hook must be callable.`);
127
+ return invokeBuiltinClosure(hook, symbol === Symbol.split || symbol === Symbol.replace ? [value, args[1]] : [value], budget, context, pattern);
128
+ };
115
129
  const hook = readProperty(symbol);
116
130
  return hook instanceof Promise ? hook.then(applyHook) : applyHook(hook);
117
131
  };
@@ -137,7 +151,7 @@ export function callStringMethod(value, methodName, args, budget, callClosure =
137
151
  }
138
152
  return dispatch();
139
153
  }
140
- function callStringMethodBody(value, methodName, args, budget, callClosure = async (closure, closureArgs) => await closure.call(closureArgs), parent, context) {
154
+ function callStringMethodBody(value, methodName, args, budget, parent, context) {
141
155
  if (methodName === "concat" && args.some(argument => argument !== null && typeof argument === "object")) {
142
156
  return callConcat(value, args, budget, context);
143
157
  }
@@ -178,13 +192,16 @@ function callStringMethodBody(value, methodName, args, budget, callClosure = asy
178
192
  }
179
193
  return budget.allocateString(result + value.slice(spanStart));
180
194
  }
181
- if (methodName === "replace" || methodName === "replaceAll") {
182
- return callReplaceLikeMethod(value, methodName, args, budget, callClosure, context);
183
- }
184
195
  const regex = args[0];
185
- if (isSandboxRegex(regex) && regex.lastIndex !== null && typeof regex.lastIndex === "object" &&
186
- ((methodName === "match" && !regex.flags.includes("g")) ||
187
- methodName === "matchAll")) {
196
+ if (methodName === "search" && isSandboxRegex(regex) &&
197
+ hasRegexPropertyOverride(regex, ["exec"], budget))
198
+ return regexSearch(regex, value, budget, context);
199
+ if (methodName === "match" && isSandboxRegex(regex) &&
200
+ hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget))
201
+ return regexMatch(value, regex, budget, context);
202
+ if (isSandboxRegex(regex) && (methodName === "matchAll" ||
203
+ (methodName === "match" && !regex.flags.includes("g") &&
204
+ regex.lastIndex !== null && typeof regex.lastIndex === "object"))) {
188
205
  return callStringRegexCursor(value, methodName, regex, budget, parent, context);
189
206
  }
190
207
  if ((methodName === "match" || methodName === "matchAll" || methodName === "search") && !isSandboxRegex(args[0])) {
@@ -193,9 +210,6 @@ function callStringMethodBody(value, methodName, args, budget, callClosure = asy
193
210
  const operation = budget.acquireCompileOwner(false, parent?.owner);
194
211
  const compilation = new CompileScope(operation.owner);
195
212
  try {
196
- if (methodName === "split") {
197
- return callSplit(value, args, budget, compilation);
198
- }
199
213
  if (methodName === "match" || methodName === "matchAll" || methodName === "search") {
200
214
  return callMatchLikeMethod(value, methodName, args, compilation);
201
215
  }
@@ -311,20 +325,20 @@ async function callConcat(value, args, budget, context) {
311
325
  release();
312
326
  }
313
327
  }
314
- function callReplaceLikeMethod(value, methodName, args, budget, callClosure, context) {
328
+ function callReplaceLikeMethod(value, methodName, args, budget, callClosure, useRegex, context) {
315
329
  const search = args[0];
316
330
  const replacement = args[1];
317
- const useRegex = isSandboxRegex(search) && getSandboxPropertyDescriptor(search, Symbol.replace, budget) === undefined;
318
- if ((!useRegex && typeof search !== "string") ||
331
+ const regexSearch = useRegex && isSandboxRegex(search);
332
+ if ((!regexSearch && typeof search !== "string") ||
319
333
  (typeof replacement !== "string" && !isSandboxClosure(replacement))) {
320
334
  return (async () => {
321
335
  let normalizedSearch;
322
336
  let normalizedReplacement;
323
337
  const release = retainValues(budget, () => [normalizedSearch, normalizedReplacement]);
324
338
  try {
325
- normalizedSearch = useRegex ? search : await sandboxString(search, budget, context);
339
+ normalizedSearch = regexSearch ? search : await sandboxString(search, budget, context);
326
340
  normalizedReplacement = isSandboxClosure(replacement) ? replacement : await sandboxString(replacement, budget, context);
327
- return await callReplaceLikeMethod(value, methodName, [normalizedSearch, normalizedReplacement], budget, callClosure, context);
341
+ return await callReplaceLikeMethod(value, methodName, [normalizedSearch, normalizedReplacement], budget, callClosure, useRegex, context);
328
342
  }
329
343
  finally {
330
344
  release();
@@ -339,25 +353,27 @@ function callReplaceLikeMethod(value, methodName, args, budget, callClosure, con
339
353
  ? value.replace(search, replacement)
340
354
  : value.replaceAll(search, replacement));
341
355
  }
342
- return replaceWithClosure(value, search, replacement, methodName === "replaceAll", budget, callClosure);
356
+ return replaceWithClosure(value, search, replacement, methodName === "replaceAll", budget, callClosure, context);
343
357
  }
344
358
  async function replaceRegex(value, regex, replacement, budget, callClosure, context) {
359
+ if (getSandboxRegexPattern(regex).groups !== undefined || hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget))
360
+ return regexReplace(value, regex, replacement, budget, callClosure, context);
345
361
  if (regex.flags.includes("g"))
346
362
  regex.lastIndex = 0;
347
363
  const cursor = regex.lastIndex;
364
+ let result = "";
348
365
  const retained = {};
349
- budget.setRetainedValues(retained, () => [value, regex, cursor, replacement]);
366
+ budget.setRetainedValues(retained, () => [value, regex, cursor, replacement, result]);
350
367
  try {
351
368
  const lastIndex = await sandboxNumber(cursor, budget, context);
352
369
  const matches = collectRegexMatches(regex, value, regex.flags.includes("g"), undefined, lastIndex);
353
- let result = "";
354
370
  let copiedThrough = 0;
355
371
  for (const match of matches) {
356
372
  result += value.slice(copiedThrough, match.index);
357
373
  result +=
358
374
  typeof replacement === "string"
359
- ? expandReplacement(replacement, match.text, match.captures, value, match.index)
360
- : String(await callClosure(replacement, [match.text, ...match.captures, match.index, value]));
375
+ ? await expandReplacement(replacement, match.text, match.captures, value, match.index, budget, context)
376
+ : await sandboxString(await callClosure(replacement, [match.text, ...match.captures, match.index, value]), budget, context);
361
377
  copiedThrough = match.index + match.text.length;
362
378
  }
363
379
  result += value.slice(copiedThrough);
@@ -367,62 +383,201 @@ async function replaceRegex(value, regex, replacement, budget, callClosure, cont
367
383
  budget.setRetainedValues(retained, undefined);
368
384
  }
369
385
  }
370
- function expandReplacement(replacement, match, captures, input, matchIndex) {
386
+ function readReplacementProperty(target, key, budget, context) {
387
+ if (context?.getProperty !== undefined)
388
+ return context.getProperty(target, key);
389
+ const descriptor = getSandboxPropertyDescriptor(target, key, budget);
390
+ if (descriptor !== undefined)
391
+ return readPropertyDescriptor(descriptor, target, context);
392
+ return isSandboxRegex(target) ? getRegexMember(target, key, budget, context) : undefined;
393
+ }
394
+ export async function regexReplace(input, regex, replacementInput, budget, callClosure, context) {
395
+ if (regex === null || typeof regex !== "object")
396
+ throw new TypeError("RegExp replace requires an object receiver.");
397
+ let value;
398
+ let replacement;
399
+ const matches = [];
400
+ let current;
401
+ let field;
402
+ let groups;
403
+ let matched;
404
+ let captures = [];
371
405
  let result = "";
372
- for (let index = 0; index < replacement.length; index += 1) {
373
- const character = replacement.charAt(index);
374
- if (character !== "$") {
375
- result += character;
376
- continue;
377
- }
378
- const token = replacement.charAt(index + 1);
379
- if (token === "$") {
380
- result += "$";
381
- }
382
- else if (token === "&") {
383
- result += match;
406
+ let flags;
407
+ const release = retainValues(budget, () => [input, value, regex, replacementInput, replacement, matches, current, field, groups, matched, captures, result, flags]);
408
+ try {
409
+ value = await sandboxString(input, budget, context);
410
+ input = undefined;
411
+ replacement = isSandboxClosure(replacementInput) ? replacementInput : await sandboxString(replacementInput, budget, context);
412
+ replacementInput = undefined;
413
+ if (isSandboxRegex(regex) && getSandboxRegexPattern(regex).groups === undefined && !hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget)) {
414
+ release();
415
+ return await replaceRegex(value, regex, replacement, budget, callClosure, context);
384
416
  }
385
- else if (token === "`") {
386
- result += input.slice(0, matchIndex);
417
+ field = await readReplacementProperty(regex, "flags", budget, context);
418
+ flags = await sandboxString(field, budget, context);
419
+ field = undefined;
420
+ const global = flags.includes("g");
421
+ const fullUnicode = flags.includes("u") || flags.includes("v");
422
+ flags = undefined;
423
+ if (global)
424
+ await setSandboxProperty(regex, "lastIndex", 0, budget, true, context);
425
+ while (true) {
426
+ budget.visitNode();
427
+ current = await regexExec(regex, value, budget, context);
428
+ if (current === null)
429
+ break;
430
+ budget.allocateArrayLength(matches.length + 1);
431
+ matches.push(current);
432
+ if (!global)
433
+ break;
434
+ field = await readReplacementProperty(current, "0", budget, context);
435
+ matched = await sandboxString(field, budget, context);
436
+ field = undefined;
437
+ if (matched.length === 0) {
438
+ field = await readReplacementProperty(regex, "lastIndex", budget, context);
439
+ const index = normalizeLastIndex(await sandboxNumber(field, budget, context));
440
+ const point = fullUnicode ? value.codePointAt(index) : undefined;
441
+ await setSandboxProperty(regex, "lastIndex", index + (point !== undefined && point > 0xffff ? 2 : 1), budget, true, context);
442
+ field = undefined;
443
+ }
444
+ matched = undefined;
445
+ current = undefined;
387
446
  }
388
- else if (token === "'") {
389
- result += input.slice(matchIndex + match.length);
447
+ let copiedThrough = 0;
448
+ for (current of matches) {
449
+ field = await readReplacementProperty(current, "length", budget, context);
450
+ const count = Math.max(normalizeLastIndex(await sandboxNumber(field, budget, context)) - 1, 0);
451
+ field = await readReplacementProperty(current, "0", budget, context);
452
+ matched = await sandboxString(field, budget, context);
453
+ field = await readReplacementProperty(current, "index", budget, context);
454
+ const number = await sandboxNumber(field, budget, context);
455
+ const position = Math.min(Math.max(Number.isNaN(number) ? 0 : Math.trunc(number), 0), value.length);
456
+ field = undefined;
457
+ captures = [];
458
+ budget.allocateArrayLength(count);
459
+ for (let index = 1; index <= count; index++) {
460
+ budget.visitNode();
461
+ field = await readReplacementProperty(current, String(index), budget, context);
462
+ captures.push(field === undefined ? undefined : await sandboxString(field, budget, context));
463
+ field = undefined;
464
+ }
465
+ groups = await readReplacementProperty(current, "groups", budget, context);
466
+ let text;
467
+ if (typeof replacement === "string") {
468
+ if (groups === null)
469
+ throw new TypeError("Replacement groups cannot be null.");
470
+ if (groups !== undefined && typeof groups !== "object") {
471
+ groups = createSandboxBox(groups);
472
+ budget.chargeDataUsage(measureSandboxData([groups]));
473
+ }
474
+ text = await expandReplacement(replacement, matched, captures, value, position, budget, context, groups);
475
+ }
476
+ else {
477
+ const args = [matched, ...captures, position, value];
478
+ if (groups !== undefined)
479
+ args.push(groups);
480
+ budget.allocateArrayLength(args.length);
481
+ field = await callClosure(replacement, args);
482
+ text = await sandboxString(field, budget, context);
483
+ field = undefined;
484
+ }
485
+ if (position >= copiedThrough) {
486
+ result = budget.allocateString(result + value.slice(copiedThrough, position) + text);
487
+ copiedThrough = position + matched.length;
488
+ }
489
+ matched = undefined;
490
+ captures = [];
491
+ groups = undefined;
390
492
  }
391
- else if (token >= "0" && token <= "9") {
392
- let captureIndex = Number(token);
393
- const nextDigit = replacement.charAt(index + 2);
394
- if (nextDigit >= "0" && nextDigit <= "9") {
395
- const twoDigitIndex = Number(token + nextDigit);
396
- if (twoDigitIndex > 0 && twoDigitIndex <= captures.length) {
397
- captureIndex = twoDigitIndex;
398
- index += 1;
493
+ return budget.allocateString(result + value.slice(copiedThrough));
494
+ }
495
+ finally {
496
+ release();
497
+ }
498
+ }
499
+ async function expandReplacement(replacement, match, captures, input, matchIndex, budget, context, groups) {
500
+ let result = "";
501
+ const release = retainValues(budget, () => [replacement, match, captures, input, groups, result]);
502
+ try {
503
+ for (let index = 0; index < replacement.length; index += 1) {
504
+ budget.visitNode();
505
+ budget.allocateString(result);
506
+ const character = replacement.charAt(index);
507
+ if (character !== "$") {
508
+ result += character;
509
+ continue;
510
+ }
511
+ const token = replacement.charAt(index + 1);
512
+ if (token === "$") {
513
+ result += "$";
514
+ }
515
+ else if (token === "&") {
516
+ result += match;
517
+ }
518
+ else if (token === "`") {
519
+ result += input.slice(0, matchIndex);
520
+ }
521
+ else if (token === "'") {
522
+ result += input.slice(matchIndex + match.length);
523
+ }
524
+ else if (token === "<" && groups !== undefined) {
525
+ const end = replacement.indexOf(">", index + 2);
526
+ if (end === -1) {
527
+ result += "$";
528
+ continue;
529
+ }
530
+ const capture = await readReplacementProperty(groups, replacement.slice(index + 2, end), budget, context);
531
+ if (capture !== undefined)
532
+ result += await sandboxString(capture, budget, context);
533
+ index = end;
534
+ continue;
535
+ }
536
+ else if (token >= "0" && token <= "9") {
537
+ let captureIndex = Number(token);
538
+ const nextDigit = replacement.charAt(index + 2);
539
+ if (nextDigit >= "0" && nextDigit <= "9") {
540
+ const twoDigitIndex = Number(token + nextDigit);
541
+ if (twoDigitIndex > 0 && twoDigitIndex <= captures.length) {
542
+ captureIndex = twoDigitIndex;
543
+ index += 1;
544
+ }
545
+ }
546
+ if (captureIndex === 0 || captureIndex > captures.length) {
547
+ result += "$";
548
+ continue;
399
549
  }
550
+ result += captures[captureIndex - 1] ?? "";
400
551
  }
401
- if (captureIndex === 0 || captureIndex > captures.length) {
552
+ else {
402
553
  result += "$";
403
554
  continue;
404
555
  }
405
- result += captures[captureIndex - 1] ?? "";
556
+ index += 1;
406
557
  }
407
- else {
408
- result += "$";
409
- continue;
410
- }
411
- index += 1;
558
+ return budget.allocateString(result);
559
+ }
560
+ finally {
561
+ release();
412
562
  }
413
- return result;
414
563
  }
415
- async function replaceWithClosure(value, searchValue, replacer, replaceAll, budget, callClosure) {
564
+ async function replaceWithClosure(value, searchValue, replacer, replaceAll, budget, callClosure, context) {
416
565
  const offsets = findReplacementOffsets(value, searchValue, replaceAll);
417
566
  let result = "";
418
567
  let copiedThrough = 0;
419
- for (const offset of offsets) {
420
- result += value.slice(copiedThrough, offset);
421
- result += String(await callClosure(replacer, [searchValue, offset, value]));
422
- copiedThrough = offset + searchValue.length;
568
+ const release = retainValues(budget, () => [value, searchValue, replacer, result]);
569
+ try {
570
+ for (const offset of offsets) {
571
+ result += value.slice(copiedThrough, offset);
572
+ result += await sandboxString(await callClosure(replacer, [searchValue, offset, value]), budget, context);
573
+ copiedThrough = offset + searchValue.length;
574
+ }
575
+ result += value.slice(copiedThrough);
576
+ return budget.allocateString(result);
577
+ }
578
+ finally {
579
+ release();
423
580
  }
424
- result += value.slice(copiedThrough);
425
- return budget.allocateString(result);
426
581
  }
427
582
  function findReplacementOffsets(value, searchValue, replaceAll) {
428
583
  const firstOffset = value.indexOf(searchValue);
@@ -443,40 +598,76 @@ function findReplacementOffsets(value, searchValue, replaceAll) {
443
598
  }
444
599
  return offsets;
445
600
  }
446
- function callSplit(value, args, budget, compilation) {
447
- if (args.some(isSandboxClosure))
448
- throw new TypeError("String#split does not support function arguments.");
449
- if (isSandboxRegex(args[0])) {
450
- const regex = args[0];
451
- const splitter = createSandboxRegex(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`, 0, compilation);
452
- const limit = asNumberOrUndefined(args[1]) ?? 2 ** 32 - 1;
453
- const result = [];
454
- let copiedThrough = 0;
455
- let endedWithZeroWidthMatch = false;
456
- for (const match of collectRegexMatches(splitter, value, true)) {
457
- endedWithZeroWidthMatch = match.text.length === 0 && match.index === value.length;
458
- if (match.text.length === 0 &&
459
- (match.index === copiedThrough || match.index === value.length))
460
- continue;
461
- if (result.length >= limit)
462
- break;
463
- result.push(budget.allocateString(value.slice(copiedThrough, match.index)));
464
- for (const capture of match.captures) {
465
- if (result.length >= limit)
601
+ function callSplit(value, args, budget, useRegex, parent, context) {
602
+ const pattern = args[0];
603
+ let separator;
604
+ const release = retainValues(budget, () => [value, ...args, separator]);
605
+ const withLimit = (number) => {
606
+ const limit = number >>> 0;
607
+ const converted = useRegex || pattern === undefined ? pattern : sandboxString(pattern, budget, context);
608
+ const split = (converted) => {
609
+ separator = converted;
610
+ return splitNormalized(value, converted, limit, budget, parent);
611
+ };
612
+ return converted instanceof Promise ? converted.then(split) : split(converted);
613
+ };
614
+ try {
615
+ const limit = args[1] === undefined ? 2 ** 32 - 1 : sandboxNumber(args[1], budget, context);
616
+ const result = limit instanceof Promise ? limit.then(withLimit) : withLimit(limit);
617
+ if (result instanceof Promise)
618
+ return result.finally(release);
619
+ release();
620
+ return result;
621
+ }
622
+ catch (error) {
623
+ release();
624
+ throw error;
625
+ }
626
+ }
627
+ function splitNormalized(value, separator, limit, budget, parent) {
628
+ if (limit === 0)
629
+ return [];
630
+ if (isSandboxRegex(separator)) {
631
+ const regex = separator;
632
+ const operation = budget.acquireCompileOwner(false, parent?.owner);
633
+ const compilation = new CompileScope(operation.owner);
634
+ try {
635
+ const splitter = createSandboxRegex(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`, 0, compilation);
636
+ const result = [];
637
+ let copiedThrough = 0;
638
+ let endedWithZeroWidthMatch = false;
639
+ while (result.length < limit) {
640
+ const match = executeRegex(splitter, value, Number(splitter.lastIndex));
641
+ if (match === null)
466
642
  break;
467
- result.push(capture === undefined ? undefined : budget.allocateString(capture));
643
+ if (match.text.length === 0)
644
+ splitter.lastIndex = advanceStringIndex(value, match.index, splitter.flags.includes("u") || splitter.flags.includes("v"));
645
+ endedWithZeroWidthMatch = match.text.length === 0 && match.index === value.length;
646
+ if (match.text.length === 0 &&
647
+ (match.index === copiedThrough || match.index === value.length))
648
+ continue;
649
+ budget.allocateArrayLength(result.length + 1);
650
+ result.push(budget.allocateString(value.slice(copiedThrough, match.index)));
651
+ for (const capture of match.captures) {
652
+ if (result.length >= limit)
653
+ break;
654
+ budget.allocateArrayLength(result.length + 1);
655
+ result.push(capture === undefined ? undefined : budget.allocateString(capture));
656
+ }
657
+ copiedThrough = match.index + match.text.length;
468
658
  }
469
- copiedThrough = match.index + match.text.length;
659
+ if (result.length < limit && (value.length > 0 || !endedWithZeroWidthMatch)) {
660
+ budget.allocateArrayLength(result.length + 1);
661
+ result.push(budget.allocateString(value.slice(copiedThrough)));
662
+ }
663
+ return result;
664
+ }
665
+ finally {
666
+ compilation.dispose();
667
+ operation.release();
470
668
  }
471
- if (result.length < limit && (value.length > 0 || !endedWithZeroWidthMatch))
472
- result.push(budget.allocateString(value.slice(copiedThrough)));
473
- budget.allocateArrayLength(result.length);
474
- return result;
475
669
  }
476
- if (args[0] !== undefined && typeof args[0] !== "string")
477
- throw new TypeError(SPLIT_STRING_MESSAGE);
478
- const limit = asNumberOrUndefined(args[1]);
479
- const result = splitString(value, args[0], limit).map((part) => budget.allocateString(part));
670
+ const result = splitString(value, separator, limit).map((part) => budget.allocateString(part));
480
671
  budget.allocateArrayLength(result.length);
481
672
  return result;
482
673
  }
@@ -496,20 +687,125 @@ async function callStringPattern(value, methodName, pattern, budget, parent, con
496
687
  operation.release();
497
688
  }
498
689
  }
499
- async function callStringRegexCursor(value, methodName, regex, budget, parent, context) {
500
- const operation = budget.acquireCompileOwner(false, parent?.owner);
501
- const compilation = new CompileScope(operation.owner);
502
- const cursor = regex.lastIndex;
503
- const retained = {};
504
- budget.setRetainedValues(retained, () => [value, regex, cursor]);
690
+ export async function regexMatch(input, regex, budget, context) {
691
+ if (regex === null || typeof regex !== "object")
692
+ throw new TypeError("RegExp match requires an object receiver.");
693
+ let value;
694
+ const matches = [];
695
+ let result;
696
+ let matched;
697
+ let cursor;
698
+ let flagsValue;
699
+ let flags;
700
+ const release = retainValues(budget, () => [input, value, regex, matches, result, matched, cursor, flagsValue, flags]);
701
+ const read = (target, key) => {
702
+ if (context?.getProperty !== undefined)
703
+ return context.getProperty(target, key);
704
+ const descriptor = getSandboxPropertyDescriptor(target, key, budget);
705
+ return descriptor === undefined ? undefined : readPropertyDescriptor(descriptor, target, context);
706
+ };
505
707
  try {
506
- const lastIndex = await sandboxNumber(cursor, budget, context);
507
- return callMatchLikeMethod(value, methodName, [regex], compilation, lastIndex);
708
+ value = await sandboxString(input, budget, context);
709
+ input = undefined;
710
+ if (isSandboxRegex(regex) && !hasRegexPropertyOverride(regex, ["exec", "flags", ...Object.keys(regexFlagProperties)], budget)) {
711
+ release();
712
+ return await callStringMethodBody(value, "match", [regex], budget, context?.compilation, context);
713
+ }
714
+ flagsValue = await read(regex, "flags");
715
+ flags = await sandboxString(flagsValue, budget, context);
716
+ flagsValue = undefined;
717
+ if (!flags.includes("g"))
718
+ return await regexExec(regex, value, budget, context);
719
+ const fullUnicode = flags.includes("u") || flags.includes("v");
720
+ flags = undefined;
721
+ await setSandboxProperty(regex, "lastIndex", 0, budget, true, context);
722
+ while (true) {
723
+ budget.visitNode();
724
+ result = await regexExec(regex, value, budget, context);
725
+ if (result === null)
726
+ return matches.length === 0 ? null : matches;
727
+ const descriptor = context?.getProperty === undefined
728
+ ? getSandboxPropertyDescriptor(result, "0", budget) : undefined;
729
+ matched = await (context?.getProperty !== undefined ? context.getProperty(result, "0")
730
+ : descriptor === undefined ? undefined : readPropertyDescriptor(descriptor, result, context));
731
+ const text = await sandboxString(matched, budget, context);
732
+ matched = undefined;
733
+ budget.allocateArrayLength(matches.length + 1);
734
+ matches.push(text);
735
+ if (text.length === 0) {
736
+ cursor = await read(regex, "lastIndex");
737
+ const index = normalizeLastIndex(await sandboxNumber(cursor, budget, context));
738
+ const point = fullUnicode ? value.codePointAt(index) : undefined;
739
+ await setSandboxProperty(regex, "lastIndex", index + (point !== undefined && point > 0xffff ? 2 : 1), budget, true, context);
740
+ cursor = undefined;
741
+ }
742
+ result = undefined;
743
+ }
508
744
  }
509
745
  finally {
746
+ release();
747
+ }
748
+ }
749
+ function callStringRegexCursor(value, methodName, regex, budget, parent, context) {
750
+ const operation = budget.acquireCompileOwner(false, parent?.owner);
751
+ const compilation = new CompileScope(operation.owner);
752
+ let cursor;
753
+ let flagsValue;
754
+ let flags;
755
+ let matcher;
756
+ const retained = {};
757
+ budget.setRetainedValues(retained, () => [value, regex, cursor, flagsValue, flags, matcher]);
758
+ const release = () => {
510
759
  budget.setRetainedValues(retained, undefined);
511
760
  compilation.dispose();
512
761
  operation.release();
762
+ };
763
+ const readCursor = () => {
764
+ cursor = regex.lastIndex;
765
+ const finish = (lastIndex) => {
766
+ if (matcher !== undefined) {
767
+ matcher.lastIndex = normalizeLastIndex(lastIndex);
768
+ return restoreSandboxRegExpIterator({ matcher, input: value, exhausted: false });
769
+ }
770
+ return callMatchLikeMethod(value, methodName, [regex], compilation, lastIndex);
771
+ };
772
+ const lastIndex = sandboxNumber(cursor, budget, context);
773
+ return lastIndex instanceof Promise ? lastIndex.then(finish) : finish(lastIndex);
774
+ };
775
+ const clone = (text) => {
776
+ flags = text;
777
+ flagsValue = undefined;
778
+ matcher = createSandboxRegex(regex.source, flags, 0, compilation);
779
+ flags = undefined;
780
+ return readCursor();
781
+ };
782
+ const convertFlags = (value) => {
783
+ flagsValue = value;
784
+ const text = sandboxString(value, budget, context);
785
+ return text instanceof Promise ? text.then(clone) : clone(text);
786
+ };
787
+ try {
788
+ let result;
789
+ if (methodName === "matchAll") {
790
+ const descriptor = context?.getProperty === undefined
791
+ ? getSandboxPropertyDescriptor(regex, "flags", budget) : undefined;
792
+ const value = context?.getProperty !== undefined
793
+ ? context.getProperty(regex, "flags")
794
+ : descriptor !== undefined
795
+ ? readPropertyDescriptor(descriptor, regex, context)
796
+ : getRegexMember(regex, "flags", budget, context);
797
+ result = value instanceof Promise ? value.then(convertFlags) : convertFlags(value);
798
+ }
799
+ else
800
+ result = readCursor();
801
+ if (result instanceof Promise)
802
+ return result.finally(release);
803
+ release();
804
+ return result;
805
+ }
806
+ catch (error) {
807
+ release();
808
+ throw error;
513
809
  }
514
810
  }
515
811
  function callMatchLikeMethod(value, methodName, args, compilation, lastIndex) {
@@ -526,18 +822,16 @@ function callMatchLikeMethod(value, methodName, args, compilation, lastIndex) {
526
822
  return match?.index ?? -1;
527
823
  }
528
824
  if (methodName === "match" && !regex.flags.includes("g"))
529
- return toMatchArray(executeRegex(regex, value, lastIndex ?? Number(regex.lastIndex)), value);
825
+ return toMatchArray(executeRegex(regex, value, lastIndex ?? Number(regex.lastIndex)), value, compilation.owner?.budget);
530
826
  if (methodName === "match")
531
827
  regex.lastIndex = 0;
532
828
  const matcher = methodName === "matchAll"
533
829
  ? createSandboxRegex(regex.source, regex.flags, normalizeLastIndex(lastIndex ?? Number(regex.lastIndex)), compilation)
534
830
  : regex;
831
+ if (methodName === "matchAll")
832
+ return restoreSandboxRegExpIterator({ matcher, input: value, exhausted: false });
535
833
  const matches = collectRegexMatches(matcher, value, matcher.flags.includes("g"), compilation.owner?.budget, Number(matcher.lastIndex));
536
- if (methodName === "match" && matches.length === 0)
537
- return null;
538
- return methodName === "match"
539
- ? matches.map((match) => match.text)
540
- : matches.map((match) => toMatchArray(match, value));
834
+ return matches.length === 0 ? null : matches.map((match) => match.text);
541
835
  }
542
836
  function collectRegexMatches(regex, value, all, budget, lastIndex = 0) {
543
837
  const matches = [];
@@ -549,7 +843,7 @@ function collectRegexMatches(regex, value, all, budget, lastIndex = 0) {
549
843
  matches.push(match);
550
844
  lastIndex = match.index + match.text.length;
551
845
  if (all && match.text.length === 0)
552
- regex.lastIndex = ++lastIndex;
846
+ regex.lastIndex = lastIndex = advanceStringIndex(value, lastIndex, regex.flags.includes("u") || regex.flags.includes("v"));
553
847
  } while (all);
554
848
  return matches;
555
849
  }