@poe-platform/safe-bash 0.1.73 → 0.1.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 (48) hide show
  1. package/dist/safe-bash/browser.js +435 -176
  2. package/dist/safe-bash/browser.js.map +4 -4
  3. package/dist/safe-bash/commands/structured/limits.js +3 -1
  4. package/dist/safe-bash/commands/structured/limits.js.map +1 -1
  5. package/dist/safe-bash/commands/structured/split.d.ts.map +1 -1
  6. package/dist/safe-bash/commands/structured/split.js +7 -2
  7. package/dist/safe-bash/commands/structured/split.js.map +1 -1
  8. package/dist/safe-bash/commands/structured/values.d.ts.map +1 -1
  9. package/dist/safe-bash/commands/structured/values.js +4 -2
  10. package/dist/safe-bash/commands/structured/values.js.map +1 -1
  11. package/dist/safe-bash/commands/text.d.ts.map +1 -1
  12. package/dist/safe-bash/commands/text.js +194 -52
  13. package/dist/safe-bash/commands/text.js.map +1 -1
  14. package/dist/safe-bash/shell/arithmetic-parameters.d.ts +2 -0
  15. package/dist/safe-bash/shell/arithmetic-parameters.d.ts.map +1 -1
  16. package/dist/safe-bash/shell/arithmetic-parameters.js +1 -1
  17. package/dist/safe-bash/shell/arithmetic-parameters.js.map +1 -1
  18. package/dist/safe-bash/shell/arithmetic.d.ts +4 -3
  19. package/dist/safe-bash/shell/arithmetic.d.ts.map +1 -1
  20. package/dist/safe-bash/shell/arithmetic.js +19 -7
  21. package/dist/safe-bash/shell/arithmetic.js.map +1 -1
  22. package/dist/safe-bash/shell/arrays/syntax.d.ts +6 -5
  23. package/dist/safe-bash/shell/arrays/syntax.d.ts.map +1 -1
  24. package/dist/safe-bash/shell/arrays/syntax.js +22 -12
  25. package/dist/safe-bash/shell/arrays/syntax.js.map +1 -1
  26. package/dist/safe-bash/shell/index.d.ts +1 -1
  27. package/dist/safe-bash/shell/index.d.ts.map +1 -1
  28. package/dist/safe-bash/shell/parse-budget.d.ts +10 -0
  29. package/dist/safe-bash/shell/parse-budget.d.ts.map +1 -0
  30. package/dist/safe-bash/shell/parse-budget.js +29 -0
  31. package/dist/safe-bash/shell/parse-budget.js.map +1 -0
  32. package/dist/safe-bash/shell/parser.d.ts +6 -4
  33. package/dist/safe-bash/shell/parser.d.ts.map +1 -1
  34. package/dist/safe-bash/shell/parser.js +80 -29
  35. package/dist/safe-bash/shell/parser.js.map +1 -1
  36. package/dist/safe-bash/shell/runtime.d.ts +3 -0
  37. package/dist/safe-bash/shell/runtime.d.ts.map +1 -1
  38. package/dist/safe-bash/shell/runtime.js +136 -78
  39. package/dist/safe-bash/shell/runtime.js.map +1 -1
  40. package/dist/safe-bash/shell/shell.js +2 -2
  41. package/dist/safe-bash/shell/shell.js.map +1 -1
  42. package/dist/safe-bash/shell/types.d.ts +4 -0
  43. package/dist/safe-bash/shell/types.d.ts.map +1 -1
  44. package/dist/safe-bash/shell/types.js.map +1 -1
  45. package/dist/safe-bash/shell/worker-limits.d.ts.map +1 -1
  46. package/dist/safe-bash/shell/worker-limits.js +1 -0
  47. package/dist/safe-bash/shell/worker-limits.js.map +1 -1
  48. package/package.json +2 -2
@@ -4673,6 +4673,103 @@ async function sortRecords(records, compare, work) {
4673
4673
  }
4674
4674
  return source;
4675
4675
  }
4676
+ async function cutRanges(list, work) {
4677
+ const ranges = [];
4678
+ let tokenStart = 0;
4679
+ let dash = -1;
4680
+ let start = 0;
4681
+ let end = 0;
4682
+ let invalid = false;
4683
+ for (let index = 0; index <= list.length; index++) {
4684
+ const checkpoint = work.charge();
4685
+ if (checkpoint) await checkpoint;
4686
+ const character = list[index];
4687
+ if (character === "," || character === " " || index === list.length) {
4688
+ if (index === tokenStart) {
4689
+ if (index === 0 || index === list.length) throw new UsageError("invalid range ''");
4690
+ tokenStart = index + 1;
4691
+ continue;
4692
+ }
4693
+ if (invalid || dash === tokenStart && dash === index - 1) throw new UsageError(`invalid range '${list.slice(tokenStart, index)}'`);
4694
+ if (dash === tokenStart) start = 1;
4695
+ if (dash < 0) end = start;
4696
+ const openEnd = dash >= 0 && dash === index - 1;
4697
+ if (openEnd) end = Infinity;
4698
+ if (!Number.isSafeInteger(start) || start < 1) throw new UsageError(`invalid number '${list.slice(tokenStart, dash < 0 ? index : dash)}'`);
4699
+ if (!openEnd && (!Number.isSafeInteger(end) || end < 1)) throw new UsageError(`invalid number '${list.slice(dash < 0 ? tokenStart : dash + 1, index)}'`);
4700
+ if (end < start) throw new UsageError(`decreasing range '${list.slice(tokenStart, index)}'`);
4701
+ ranges.push({ start, end });
4702
+ tokenStart = index + 1;
4703
+ dash = -1;
4704
+ start = 0;
4705
+ end = 0;
4706
+ invalid = false;
4707
+ } else if (character === "-" && dash < 0) {
4708
+ dash = index;
4709
+ } else {
4710
+ const digit2 = list.charCodeAt(index) - 48;
4711
+ if (digit2 < 0 || digit2 > 9) invalid = true;
4712
+ else if (dash < 0) start = start * 10 + digit2;
4713
+ else end = end * 10 + digit2;
4714
+ }
4715
+ }
4716
+ const ordered = await sortRecords(ranges, async (left, right) => left.start - right.start, work);
4717
+ const normalized = [];
4718
+ for (const range of ordered) {
4719
+ const checkpoint = work.charge();
4720
+ if (checkpoint) await checkpoint;
4721
+ const previous = normalized.at(-1);
4722
+ if (previous && range.start <= previous.end + 1) previous.end = Math.max(previous.end, range.end);
4723
+ else normalized.push(range);
4724
+ }
4725
+ return normalized;
4726
+ }
4727
+ var CutOutput = class {
4728
+ constructor(context, work) {
4729
+ this.context = context;
4730
+ this.work = work;
4731
+ }
4732
+ context;
4733
+ work;
4734
+ #buffer = new Uint8Array(64 * 1024);
4735
+ #used = 0;
4736
+ async write(bytes2) {
4737
+ for (let offset = 0; offset < bytes2.length; ) {
4738
+ const length = Math.min(4096, bytes2.length - offset, this.#buffer.length - this.#used);
4739
+ const checkpoint = this.work.charge(length);
4740
+ if (checkpoint) await checkpoint;
4741
+ this.#buffer.set(bytes2.subarray(offset, offset + length), this.#used);
4742
+ this.#used += length;
4743
+ offset += length;
4744
+ if (this.#used === this.#buffer.length) await this.flush();
4745
+ }
4746
+ }
4747
+ async text(text) {
4748
+ for (let offset = 0; offset < text.length; ) {
4749
+ let end = Math.min(offset + 4096, text.length);
4750
+ const last = text.charCodeAt(end - 1);
4751
+ if (end < text.length && last >= 55296 && last <= 56319) end--;
4752
+ await this.write(encoder2.encode(text.slice(offset, end)));
4753
+ offset = end;
4754
+ }
4755
+ }
4756
+ async flush() {
4757
+ if (!this.#used) return;
4758
+ const bytes2 = this.#buffer.slice(0, this.#used);
4759
+ this.#used = 0;
4760
+ await output(this.context, bytes2);
4761
+ }
4762
+ };
4763
+ async function cutFieldBoundary(record4, separator, start, work) {
4764
+ for (let offset = start; offset < record4.length; offset += 4096) {
4765
+ const window = record4.subarray(offset, Math.min(record4.length, offset + 4096 + separator.length - 1));
4766
+ const found = window.indexOf(separator);
4767
+ const checkpoint = work.charge(found < 0 ? Math.min(4096, window.length) : found + separator.length);
4768
+ if (checkpoint) await checkpoint;
4769
+ if (found >= 0) return offset + found;
4770
+ }
4771
+ return -1;
4772
+ }
4676
4773
  async function compareSortBytes(left, right, work) {
4677
4774
  const length = Math.min(left.length, right.length);
4678
4775
  for (let offset = 0; offset < length; offset += 1024) {
@@ -5032,85 +5129,104 @@ function textCommands() {
5032
5129
  if (modes.length !== 1) throw new UsageError("exactly one byte, character, or field list is required");
5033
5130
  const mode = modes[0];
5034
5131
  if (mode !== "f" && (parsed.flags.has("d") || parsed.flags.has("s"))) throw new UsageError("delimiter options require field mode");
5035
- const ranges = value(parsed, mode).split(/[ ,]+/u).map((part) => {
5036
- const match = /^(?:([0-9]+)(?:-([0-9]*))?|-([0-9]+))$/u.exec(part);
5037
- if (!match) throw new UsageError(`invalid range '${part}'`);
5038
- const start = match[3] === void 0 ? integer(match[1], 1) : 1;
5039
- const end = match[3] !== void 0 ? integer(match[3], 1) : match[2] === void 0 ? start : match[2] === "" ? Infinity : integer(match[2], 1);
5040
- if (end < start) throw new UsageError(`decreasing range '${part}'`);
5041
- return { start, end };
5042
- });
5043
- const selected = (position) => ranges.some((range) => position >= range.start && position <= range.end) !== parsed.flags.has("C");
5132
+ const work = new SortWork(context.signal);
5133
+ const ranges = await cutRanges(value(parsed, mode), work);
5134
+ const complement = parsed.flags.has("C");
5044
5135
  const delimiter = value(parsed, "d") ?? " ";
5045
- if ([...delimiter].length !== 1) throw new UsageError("delimiter must be a single character");
5136
+ if (delimiter.length !== (delimiter.codePointAt(0) > 65535 ? 2 : 1)) throw new UsageError("delimiter must be a single character");
5046
5137
  const outputDelimiter = value(parsed, "o");
5047
5138
  const recordDelimiter = parsed.flags.has("z") ? 0 : 10;
5139
+ const separator = import_buffer.Buffer.from(delimiter);
5140
+ const writer = new CutOutput(context, work);
5048
5141
  let exitCode = 0;
5049
5142
  for (const name2 of parsed.operands.length ? parsed.operands : ["-"]) {
5050
5143
  try {
5051
5144
  for await (const line of lines(input(context, name2), recordDelimiter)) {
5052
5145
  context.signal.throwIfAborted();
5053
- let bytes2;
5146
+ let cursor = 0;
5147
+ const selected = (position) => {
5148
+ while (cursor < ranges.length && position > ranges[cursor].end) cursor++;
5149
+ return (cursor < ranges.length && position >= ranges[cursor].start) !== complement;
5150
+ };
5054
5151
  if (mode === "f") {
5055
5152
  const record4 = import_buffer.Buffer.from(line.bytes.buffer, line.bytes.byteOffset, line.bytes.byteLength);
5056
- const separator = import_buffer.Buffer.from(delimiter);
5057
- let boundary = record4.indexOf(separator);
5153
+ let boundary = await cutFieldBoundary(record4, separator, 0, work);
5058
5154
  if (boundary < 0) {
5059
5155
  if (parsed.flags.has("s")) continue;
5060
- bytes2 = line.bytes;
5156
+ await writer.write(line.bytes);
5061
5157
  } else {
5062
- const pieces = [];
5063
- const joiner = encoder2.encode(outputDelimiter ?? delimiter);
5064
5158
  let field = 1;
5065
5159
  let start = 0;
5066
5160
  let emitted = false;
5067
5161
  while (true) {
5162
+ const checkpoint = work.charge();
5163
+ if (checkpoint) await checkpoint;
5068
5164
  if (selected(field++)) {
5069
- if (emitted) pieces.push(joiner);
5070
- pieces.push(record4.subarray(start, boundary < 0 ? record4.length : boundary));
5165
+ if (emitted) await writer.text(outputDelimiter ?? delimiter);
5166
+ await writer.write(record4.subarray(start, boundary < 0 ? record4.length : boundary));
5071
5167
  emitted = true;
5072
5168
  }
5073
5169
  if (boundary < 0) break;
5074
5170
  start = boundary + separator.length;
5075
- boundary = record4.indexOf(separator, start);
5171
+ boundary = await cutFieldBoundary(record4, separator, start, work);
5076
5172
  }
5077
- bytes2 = concatenate(pieces);
5078
5173
  }
5079
5174
  } else if (mode === "b") {
5080
- const chunks = [];
5081
- let start = -1;
5082
5175
  let emitted = false;
5083
- for (let index = 0; index <= line.bytes.length; index++) {
5084
- const included = index < line.bytes.length && selected(index + 1);
5085
- if (included && start < 0) start = index;
5086
- if (!included && start >= 0) {
5087
- if (emitted && outputDelimiter !== void 0) chunks.push(encoder2.encode(outputDelimiter));
5088
- chunks.push(line.bytes.subarray(start, index));
5089
- emitted = true;
5090
- start = -1;
5176
+ let previousIncluded = false;
5177
+ for (let offset = 0; offset < line.bytes.length; offset += 4096) {
5178
+ const end = Math.min(line.bytes.length, offset + 4096);
5179
+ const checkpoint = work.charge(end - offset);
5180
+ if (checkpoint) await checkpoint;
5181
+ let start = -1;
5182
+ for (let index = offset; index < end; index++) {
5183
+ const included = selected(index + 1);
5184
+ if (included && start < 0) {
5185
+ if (!previousIncluded && emitted && outputDelimiter !== void 0) await writer.text(outputDelimiter);
5186
+ start = index;
5187
+ emitted = true;
5188
+ }
5189
+ if (!included && start >= 0) {
5190
+ await writer.write(line.bytes.subarray(start, index));
5191
+ start = -1;
5192
+ }
5193
+ previousIncluded = included;
5091
5194
  }
5195
+ if (start >= 0) await writer.write(line.bytes.subarray(start, end));
5092
5196
  }
5093
- bytes2 = concatenate(chunks);
5094
5197
  } else {
5095
- const text = new TextDecoder().decode(line.bytes);
5096
- const pieces = [];
5198
+ const decoder3 = new TextDecoder();
5097
5199
  let index = 0;
5098
- let offset = 0;
5099
- let start = -1;
5100
- for (const character of text) {
5101
- const included = selected(++index);
5102
- if (included && start < 0) start = offset;
5103
- if (!included && start >= 0) {
5104
- pieces.push(text.slice(start, offset));
5105
- start = -1;
5200
+ let emitted = false;
5201
+ let previousIncluded = false;
5202
+ for (let offset = 0; offset < line.bytes.length; offset += 4096) {
5203
+ const end = Math.min(line.bytes.length, offset + 4096);
5204
+ const checkpoint = work.charge(end - offset);
5205
+ if (checkpoint) await checkpoint;
5206
+ const text = decoder3.decode(line.bytes.subarray(offset, end), { stream: end < line.bytes.length });
5207
+ let start = -1;
5208
+ let position = 0;
5209
+ for (const character of text) {
5210
+ const checkpoint2 = work.charge();
5211
+ if (checkpoint2) await checkpoint2;
5212
+ const included = selected(++index);
5213
+ if (included && start < 0) {
5214
+ if (!previousIncluded && emitted && outputDelimiter !== void 0) await writer.text(outputDelimiter);
5215
+ start = position;
5216
+ emitted = true;
5217
+ }
5218
+ if (!included && start >= 0) {
5219
+ await writer.text(text.slice(start, position));
5220
+ start = -1;
5221
+ }
5222
+ position += character.length;
5223
+ previousIncluded = included;
5106
5224
  }
5107
- offset += character.length;
5225
+ if (start >= 0) await writer.text(text.slice(start));
5108
5226
  }
5109
- if (start >= 0) pieces.push(text.slice(start));
5110
- bytes2 = encoder2.encode(pieces.join(outputDelimiter ?? ""));
5111
5227
  }
5112
- await output(context, bytes2);
5113
- await output(context, Uint8Array.of(recordDelimiter));
5228
+ await writer.write(Uint8Array.of(recordDelimiter));
5229
+ await writer.flush();
5114
5230
  }
5115
5231
  } catch (error) {
5116
5232
  await diagnostic(context, error);
@@ -5169,6 +5285,33 @@ var ShellLimitError = class extends Error {
5169
5285
  limit;
5170
5286
  };
5171
5287
 
5288
+ // packages/safe-bash/src/shell/parse-budget.ts
5289
+ init_platform();
5290
+ var defaultMaxParseUnits = 262144;
5291
+ var ParseBudget = class {
5292
+ constructor(maximum = defaultMaxParseUnits, signal, onLimit) {
5293
+ this.signal = signal;
5294
+ this.onLimit = onLimit;
5295
+ if (!Number.isSafeInteger(maximum) || maximum < 0) throw new RangeError("maxParseUnits must be a nonnegative safe integer");
5296
+ this.#remaining = maximum;
5297
+ }
5298
+ signal;
5299
+ onLimit;
5300
+ #remaining;
5301
+ #failure;
5302
+ admit(units = 1) {
5303
+ this.signal?.throwIfAborted();
5304
+ if (this.#failure) throw this.#failure;
5305
+ if (!Number.isSafeInteger(units) || units < 0) throw new RangeError("Parse admission must be a nonnegative safe integer");
5306
+ if (units > this.#remaining) {
5307
+ const error = this.#failure = new ShellLimitError("maxParseUnits");
5308
+ this.onLimit?.(error);
5309
+ throw error;
5310
+ }
5311
+ this.#remaining -= units;
5312
+ }
5313
+ };
5314
+
5172
5315
  // packages/safe-bash/src/shell/arithmetic.ts
5173
5316
  init_platform();
5174
5317
  var ArithmeticFailure = class extends Error {
@@ -5178,11 +5321,13 @@ var ArithmeticFailure = class extends Error {
5178
5321
  }
5179
5322
  offset;
5180
5323
  };
5181
- function prepareArithmetic(source) {
5324
+ function prepareArithmetic(source, budget = new ParseBudget()) {
5325
+ budget.admit();
5182
5326
  try {
5183
- return { source, tree: parseArithmetic(source) };
5327
+ return { source, tree: parseArithmetic(source, 0, budget) };
5184
5328
  } catch (error) {
5185
5329
  if (!(error instanceof ShellSyntaxError) || /nesting/u.test(error.reason)) throw error;
5330
+ budget.admit();
5186
5331
  return { source, error };
5187
5332
  }
5188
5333
  }
@@ -5237,7 +5382,7 @@ function integer2(text) {
5237
5382
  }
5238
5383
  return value2;
5239
5384
  }
5240
- function parseArithmetic(source, offset = 0) {
5385
+ function parseArithmetic(source, offset = 0, budget = new ParseBudget()) {
5241
5386
  const tokens2 = [];
5242
5387
  let position = 0;
5243
5388
  while (position < source.length) {
@@ -5247,6 +5392,7 @@ function parseArithmetic(source, offset = 0) {
5247
5392
  }
5248
5393
  const value2 = /^(?:\d+#[\da-zA-Z@_]+|0[xX][\da-fA-F]+|\d+|[a-zA-Z_][a-zA-Z_0-9]*|<<=|>>=|\*\*|\+\+|--|&&|\|\||<<|>>|[+*/%&^|!<>=-]=|[()+*/%~!<>=&^|?:,\-])/u.exec(source.slice(position))?.[0];
5249
5394
  if (!value2) throw new ShellSyntaxError("Unsupported arithmetic token", offset + position);
5395
+ budget.admit();
5250
5396
  tokens2.push({ value: value2, offset: offset + position });
5251
5397
  position += value2.length;
5252
5398
  }
@@ -5263,6 +5409,7 @@ function parseArithmetic(source, offset = 0) {
5263
5409
  const token = current();
5264
5410
  cursor++;
5265
5411
  if (["+", "-", "!", "~", "++", "--"].includes(token)) {
5412
+ budget.admit();
5266
5413
  const operand = expression(15);
5267
5414
  if (["++", "--"].includes(token) && operand.kind !== "name") error("Arithmetic assignment requires a variable");
5268
5415
  left = { kind: "unary", operator: token, operand, postfix: false };
@@ -5270,8 +5417,11 @@ function parseArithmetic(source, offset = 0) {
5270
5417
  left = expression();
5271
5418
  if (current() !== ")") error("Unclosed arithmetic parenthesis");
5272
5419
  cursor++;
5273
- } else if (/^[a-zA-Z_]/u.test(token)) left = { kind: "name", name: token };
5274
- else {
5420
+ } else if (/^[a-zA-Z_]/u.test(token)) {
5421
+ budget.admit();
5422
+ left = { kind: "name", name: token };
5423
+ } else {
5424
+ budget.admit();
5275
5425
  try {
5276
5426
  left = { kind: "literal", value: BigInt.asIntN(64, integer2(token)) };
5277
5427
  } catch {
@@ -5284,12 +5434,14 @@ function parseArithmetic(source, offset = 0) {
5284
5434
  if (operator === "++" || operator === "--") {
5285
5435
  if (left.kind !== "name") error("Arithmetic assignment requires a variable");
5286
5436
  cursor++;
5437
+ budget.admit();
5287
5438
  left = { kind: "unary", operator, operand: left, postfix: true, start };
5288
5439
  continue;
5289
5440
  }
5290
5441
  const priority = Object.hasOwn(precedence, operator) ? precedence[operator] : 0;
5291
5442
  if (priority < minimum || priority === 0) break;
5292
5443
  cursor++;
5444
+ budget.admit();
5293
5445
  if (operator === "?") {
5294
5446
  const yes = expression();
5295
5447
  if (current() !== ":") error("Expected arithmetic colon");
@@ -5305,7 +5457,10 @@ function parseArithmetic(source, offset = 0) {
5305
5457
  depth--;
5306
5458
  return left;
5307
5459
  };
5308
- if (!tokens2.length) return { kind: "literal", value: 0n };
5460
+ if (!tokens2.length) {
5461
+ budget.admit();
5462
+ return { kind: "literal", value: 0n };
5463
+ }
5309
5464
  const tree = expression();
5310
5465
  if (cursor < tokens2.length) error("Unexpected arithmetic token");
5311
5466
  return tree;
@@ -5322,7 +5477,7 @@ function arithmeticEnd(source, start) {
5322
5477
  }
5323
5478
  throw new ShellSyntaxError("Unterminated arithmetic expression", start);
5324
5479
  }
5325
- function evaluateArithmetic(program, variables) {
5480
+ function evaluateArithmetic(program, variables, budget = new ParseBudget()) {
5326
5481
  const visiting = /* @__PURE__ */ new Set();
5327
5482
  let steps = 0;
5328
5483
  const binary = (operator, left, right, offset) => {
@@ -5390,7 +5545,7 @@ function evaluateArithmetic(program, variables) {
5390
5545
  else if (node.kind === "name") {
5391
5546
  if (visiting.has(node.name) || visiting.size >= 64) throw new Error("Arithmetic variable recursion");
5392
5547
  visiting.add(node.name);
5393
- pending.push({ kind: "variable", name: node.name }, { kind: "evaluate", node: parseArithmetic(variables[node.name] ?? "0") });
5548
+ pending.push({ kind: "variable", name: node.name }, { kind: "evaluate", node: parseArithmetic(variables[node.name] ?? "0", 0, budget) });
5394
5549
  } else if (node.kind === "conditional") {
5395
5550
  pending.push({ kind: "conditional", node }, { kind: "evaluate", node: node.condition });
5396
5551
  } else if (node.kind === "unary") {
@@ -5456,21 +5611,23 @@ function setQuoteMarker(part, synthetic) {
5456
5611
  function isQuoteMarker(part) {
5457
5612
  return quoteMarkers.has(part);
5458
5613
  }
5459
- function literalIndex(source, offset) {
5614
+ function literalIndex(source, offset, budget = new ParseBudget()) {
5460
5615
  let decimal = source;
5461
5616
  if (source[0] === "'" || source[0] === '"') {
5462
5617
  if (source.length < 2 || source.at(-1) !== source[0]) throw new ShellSyntaxError("Unsupported indexed-array subscript", offset);
5463
5618
  decimal = source.slice(1, -1);
5464
5619
  }
5465
5620
  if (!/^(?:0|[1-9][0-9]*)$/u.test(decimal)) throw new ShellSyntaxError("Unsupported indexed-array subscript", offset);
5621
+ budget.admit();
5466
5622
  return { decimal };
5467
5623
  }
5468
5624
  function numericIndex(index) {
5469
5625
  if (index.decimal.length > 10 || index.decimal.length === 10 && index.decimal > "2147483647") return void 0;
5470
5626
  return Number(index.decimal);
5471
5627
  }
5472
- function arraySelector(source, offset) {
5473
- return source === "@" || source === "*" ? { kind: "members", separator: source } : { kind: "element", index: literalIndex(source, offset) };
5628
+ function arraySelector(source, offset, budget = new ParseBudget()) {
5629
+ budget.admit();
5630
+ return source === "@" || source === "*" ? { kind: "members", separator: source } : { kind: "element", index: literalIndex(source, offset, budget) };
5474
5631
  }
5475
5632
  function setArraySelector(part, selector) {
5476
5633
  selectors.set(part, selector);
@@ -5490,7 +5647,8 @@ function setArrayAssignment(word, assignment) {
5490
5647
  function getArrayAssignment(word) {
5491
5648
  return assignments.get(word);
5492
5649
  }
5493
- function removePrefix(word, length) {
5650
+ function removePrefix(word, length, budget) {
5651
+ budget.admit();
5494
5652
  const parts = [];
5495
5653
  for (const part of word.parts) {
5496
5654
  if (length === 0) parts.push(part);
@@ -5498,6 +5656,7 @@ function removePrefix(word, length) {
5498
5656
  if (part.kind !== "text") throw new ShellSyntaxError("Unsupported indexed-array subscript", word.offset);
5499
5657
  if (part.value.length <= length) length -= part.value.length;
5500
5658
  else {
5659
+ budget.admit();
5501
5660
  parts.push({ ...part, value: part.value.slice(length) });
5502
5661
  length = 0;
5503
5662
  }
@@ -5506,7 +5665,7 @@ function removePrefix(word, length) {
5506
5665
  if (length !== 0) throw new ShellSyntaxError("Invalid indexed-array assignment", word.offset);
5507
5666
  return { offset: word.offset, parts };
5508
5667
  }
5509
- function elementAssignment(word) {
5668
+ function elementAssignment(word, budget = new ParseBudget()) {
5510
5669
  const source = word.spelling;
5511
5670
  const first = word.parts[0];
5512
5671
  if (source === void 0 || first?.kind !== "text" || first.quoted) return void 0;
@@ -5517,19 +5676,23 @@ function elementAssignment(word) {
5517
5676
  if (source.includes("=")) throw new ShellSyntaxError("Invalid indexed-array assignment", word.offset);
5518
5677
  return void 0;
5519
5678
  }
5520
- const index = literalIndex(source.slice(name2.length + 1, end), word.offset + name2.length + 1);
5679
+ budget.admit();
5680
+ const index = literalIndex(source.slice(name2.length + 1, end), word.offset + name2.length + 1, budget);
5521
5681
  const append = source[end + 1] === "+";
5522
- const value2 = removePrefix(word, name2.length + index.decimal.length + (append ? 4 : 3));
5682
+ const value2 = removePrefix(word, name2.length + index.decimal.length + (append ? 4 : 3), budget);
5523
5683
  return { kind: "element", name: name2, index, append, value: value2 };
5524
5684
  }
5525
- function compoundHead(word) {
5685
+ function compoundHead(word, budget = new ParseBudget()) {
5526
5686
  if (word.parts.length !== 1) return void 0;
5527
5687
  const first = word.parts[0];
5528
5688
  if (first?.kind !== "text" || first.quoted) return void 0;
5529
5689
  const match = /^([a-zA-Z_][a-zA-Z_0-9]*)(\+?)=$/u.exec(first.value);
5530
- return match ? { name: match[1], append: match[2] === "+" } : void 0;
5690
+ if (!match) return void 0;
5691
+ budget.admit();
5692
+ return { name: match[1], append: match[2] === "+" };
5531
5693
  }
5532
- function compoundEntry(word) {
5694
+ function compoundEntry(word, budget = new ParseBudget()) {
5695
+ budget.admit();
5533
5696
  const source = word.spelling;
5534
5697
  const first = word.parts[0];
5535
5698
  if (source === void 0 || source[0] !== "[" || first?.kind !== "text" || first.quoted) return { value: word };
@@ -5538,8 +5701,8 @@ function compoundEntry(word) {
5538
5701
  if (source.includes("=")) throw new ShellSyntaxError("Invalid indexed-array entry", word.offset);
5539
5702
  return { value: word };
5540
5703
  }
5541
- const index = literalIndex(source.slice(1, end), word.offset + 1);
5542
- return { index, value: removePrefix(word, index.decimal.length + 3) };
5704
+ const index = literalIndex(source.slice(1, end), word.offset + 1, budget);
5705
+ return { index, value: removePrefix(word, index.decimal.length + 3, budget) };
5543
5706
  }
5544
5707
  function scalarAssignmentName(word) {
5545
5708
  const first = word.parts[0];
@@ -5859,7 +6022,8 @@ var HereDocumentSyntaxError = class extends Error {
5859
6022
  }
5860
6023
  diagnostic;
5861
6024
  };
5862
- function printedSimpleLines(lists, separators) {
6025
+ function printedSimpleLines(lists, separators, budget) {
6026
+ budget.admit();
5863
6027
  const printedLines = /* @__PURE__ */ new Map();
5864
6028
  let line = 1;
5865
6029
  for (let index = 0; index < lists.length; index++) {
@@ -5877,7 +6041,8 @@ function printedSimpleLines(lists, separators) {
5877
6041
  var IncompleteShellInput = class extends Error {
5878
6042
  };
5879
6043
  var Lexer = class {
5880
- constructor(source, depth, warnings = [], lineOffset = 0, byteLocale2 = false, documentLine, partial = false) {
6044
+ constructor(budget, source, depth, warnings = [], lineOffset = 0, byteLocale2 = false, documentLine, partial = false) {
6045
+ this.budget = budget;
5881
6046
  this.source = source;
5882
6047
  this.depth = depth;
5883
6048
  this.warnings = warnings;
@@ -5888,6 +6053,7 @@ var Lexer = class {
5888
6053
  if (depth > 64) throw new ShellSyntaxError("Syntax nesting exceeds 64", 0);
5889
6054
  for (let offset = source.indexOf("\n"); offset !== -1; offset = source.indexOf("\n", offset + 1)) this.newlineOffsets.push(offset);
5890
6055
  }
6056
+ budget;
5891
6057
  source;
5892
6058
  depth;
5893
6059
  warnings;
@@ -5917,6 +6083,7 @@ var Lexer = class {
5917
6083
  throw new ShellSyntaxError(message2, this.position, 2, void 0, unclosedQuote);
5918
6084
  }
5919
6085
  next() {
6086
+ this.budget.admit();
5920
6087
  while (this.position < this.source.length) {
5921
6088
  const current = this.source[this.position];
5922
6089
  if (current === " " || current === " " || this.conditional && current === "\n") {
@@ -5958,6 +6125,7 @@ var Lexer = class {
5958
6125
  if (this.position <= offset) this.error("Tokenizer made no progress");
5959
6126
  let document;
5960
6127
  if (delimiterOperator) {
6128
+ this.budget.admit();
5961
6129
  document = {
5962
6130
  delimiter: word.parts.map((part) => part.kind === "text" ? part.value : "").join(""),
5963
6131
  quoted: word.parts.some((part) => part.quoted),
@@ -5969,6 +6137,7 @@ var Lexer = class {
5969
6137
  };
5970
6138
  this.documents.push(document);
5971
6139
  }
6140
+ this.budget.admit();
5972
6141
  return { kind: "word", value: word.plain ?? "", offset, end: this.position, word: { ...word, spelling: this.source.slice(offset, this.position) }, ...document ? { document } : {} };
5973
6142
  }
5974
6143
  readDocuments() {
@@ -6003,10 +6172,12 @@ var Lexer = class {
6003
6172
  const current = this.source[this.position];
6004
6173
  if (current === "$" || current === "`") {
6005
6174
  if (text) {
6175
+ this.budget.admit(2);
6006
6176
  yield { offset: 0, parts: [{ kind: "text", value: text, quoted: true }] };
6007
6177
  text = "";
6008
6178
  }
6009
6179
  const offset = this.position;
6180
+ this.budget.admit();
6010
6181
  const parts = [];
6011
6182
  try {
6012
6183
  this.expansion(parts, true);
@@ -6030,11 +6201,15 @@ var Lexer = class {
6030
6201
  this.position++;
6031
6202
  }
6032
6203
  if (text.length >= 1024) {
6204
+ this.budget.admit(2);
6033
6205
  yield { offset: 0, parts: [{ kind: "text", value: text, quoted: true }] };
6034
6206
  text = "";
6035
6207
  }
6036
6208
  }
6037
- if (text) yield { offset: 0, parts: [{ kind: "text", value: text, quoted: true }] };
6209
+ if (text) {
6210
+ this.budget.admit(2);
6211
+ yield { offset: 0, parts: [{ kind: "text", value: text, quoted: true }] };
6212
+ }
6038
6213
  }
6039
6214
  documentSubstitutionError(source, error, backtick = false) {
6040
6215
  const line = this.documentLine + source.slice(0, error.offset).split("\n").length;
@@ -6081,6 +6256,7 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6081
6256
  this.error("Unterminated delimiter expansion syntax");
6082
6257
  }
6083
6258
  word(terminator, enclosingQuoted = false, literal = false, arithmetic = false) {
6259
+ this.budget.admit();
6084
6260
  const offset = this.position;
6085
6261
  const reduction = this.printedNewlineReduction;
6086
6262
  const unprinted = this.unprintedWords;
@@ -6105,6 +6281,7 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6105
6281
  previous.value += value2;
6106
6282
  if (!synthetic) setQuoteMarker(previous, false);
6107
6283
  } else {
6284
+ this.budget.admit();
6108
6285
  const part = { kind: "text", value: projection, quoted, ...typeof value2 === "string" ? {} : { byteValue: value2 } };
6109
6286
  setQuoteMarker(part, synthetic);
6110
6287
  parts.push(part);
@@ -6212,6 +6389,7 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6212
6389
  return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw);
6213
6390
  } catch (error) {
6214
6391
  if (!(error instanceof TypeError) || !("code" in error) || error.code !== "ERR_ENCODING_INVALID_ENCODED_DATA") throw error;
6392
+ this.budget.admit(2);
6215
6393
  return shellValueFromBytes(raw);
6216
6394
  }
6217
6395
  }
@@ -6254,6 +6432,7 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6254
6432
  this.error("Unterminated ANSI-C quote", { quote: "'", line: quoteLine });
6255
6433
  }
6256
6434
  expansion(parts, quoted) {
6435
+ this.budget.admit();
6257
6436
  const line = this.documentLine ?? this.lineAt(this.position);
6258
6437
  if (this.source[this.position] === "`") {
6259
6438
  this.position++;
@@ -6265,9 +6444,10 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6265
6444
  if (this.source[this.position] !== "`") this.error("Unterminated command substitution");
6266
6445
  this.position++;
6267
6446
  try {
6268
- parts.push({ kind: "substitution", script: parseSource(source, this.depth + 1, this.warnings, line - 1, this.byteLocale), line, quoted });
6447
+ parts.push({ kind: "substitution", script: parseSource(source, this.depth + 1, this.warnings, line - 1, this.byteLocale, this.budget), line, quoted });
6269
6448
  } catch (error) {
6270
6449
  if (this.documentLine === void 0 || !(error instanceof ShellSyntaxError) || /nesting|exceeds/u.test(error.reason)) throw error;
6450
+ this.budget.admit();
6271
6451
  parts.push({ kind: "failed-substitution", diagnostic: this.documentSubstitutionError(source, error, true).diagnostic, quoted });
6272
6452
  }
6273
6453
  return;
@@ -6278,14 +6458,15 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6278
6458
  const start = this.position + 2;
6279
6459
  const end = arithmeticEnd(this.source, start);
6280
6460
  const source = this.source.slice(start, end);
6281
- parts.push({ kind: "arithmetic", expression: prepareArithmetic(source), source, line, quoted });
6461
+ parts.push({ kind: "arithmetic", expression: prepareArithmetic(source, this.budget), source, line, quoted });
6282
6462
  this.position = end + 2;
6283
6463
  } else if (this.source[this.position] === "(") {
6284
6464
  const start = this.position + 1;
6285
6465
  let nested;
6286
6466
  let script;
6287
6467
  try {
6288
- nested = new Parser(this.source.slice(start), this.depth + 1, this.warnings, this.lineAt(start) - 1, void 0, this.byteLocale);
6468
+ this.budget.admit();
6469
+ nested = new Parser(this.budget, this.source.slice(start), this.depth + 1, this.warnings, this.lineAt(start) - 1, void 0, this.byteLocale);
6289
6470
  script = nested.script(/* @__PURE__ */ new Set([")"]));
6290
6471
  } catch (error) {
6291
6472
  if (this.documentLine !== void 0 && error instanceof ShellSyntaxError && !/nesting|exceeds/u.test(error.reason)) throw this.documentSubstitutionError(this.source.slice(start), error);
@@ -6315,7 +6496,7 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6315
6496
  const start = ++this.position;
6316
6497
  const end = this.source.indexOf("]", start);
6317
6498
  if (end < 0) this.error("Unterminated indexed-array subscript");
6318
- selector = arraySelector(this.source.slice(start, end), start);
6499
+ selector = arraySelector(this.source.slice(start, end), start, this.budget);
6319
6500
  this.position = end + 1;
6320
6501
  if (this.source[this.position] !== "}") this.error("Unsupported indexed-array operator");
6321
6502
  }
@@ -6340,6 +6521,7 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6340
6521
  this.position++;
6341
6522
  substringLength = this.word("}", true, false, true);
6342
6523
  }
6524
+ this.budget.admit();
6343
6525
  substring = { offset, ...substringLength ? { length: substringLength } : {}, source: this.source.slice(parameterStart, this.position + 1) };
6344
6526
  }
6345
6527
  if (this.source[this.position] !== "}") this.error("Unterminated or unsupported parameter expansion");
@@ -6357,17 +6539,20 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6357
6539
  }
6358
6540
  };
6359
6541
  var Parser = class {
6542
+ constructor(budget, source, depth, warnings = [], lineOffset = 0, position, byteLocale2 = false, partial = false) {
6543
+ this.budget = budget;
6544
+ if (position === void 0 && depth === 0 && source.includes("\0")) throw new ShellSyntaxError("NUL bytes are not valid shell source", source.indexOf("\0"));
6545
+ budget.admit();
6546
+ this.lexer = new Lexer(budget, source, depth, warnings, lineOffset, byteLocale2, void 0, partial);
6547
+ this.lexer.position = position ?? 0;
6548
+ this.current = this.lexer.next();
6549
+ }
6550
+ budget;
6360
6551
  lexer;
6361
6552
  current;
6362
6553
  lookahead;
6363
6554
  nesting = 0;
6364
6555
  openCommands = [];
6365
- constructor(source, depth, warnings = [], lineOffset = 0, position, byteLocale2 = false, partial = false) {
6366
- if (position === void 0 && depth === 0 && source.includes("\0")) throw new ShellSyntaxError("NUL bytes are not valid shell source", source.indexOf("\0"));
6367
- this.lexer = new Lexer(source, depth, warnings, lineOffset, byteLocale2, void 0, partial);
6368
- this.lexer.position = position ?? 0;
6369
- this.current = this.lexer.next();
6370
- }
6371
6556
  error(message2) {
6372
6557
  const command2 = this.current.kind === "end" ? this.openCommands.findLast((command3) => ["{", "if", "while", "until", "for", "case"].includes(command3.name)) : void 0;
6373
6558
  throw new ShellSyntaxError(message2, this.current.offset, 2, command2);
@@ -6395,11 +6580,13 @@ var Parser = class {
6395
6580
  while (this.is("\n")) this.advance();
6396
6581
  }
6397
6582
  script(stops = /* @__PURE__ */ new Set(), inputUnit = false) {
6583
+ this.budget.admit();
6398
6584
  const lists = [];
6399
6585
  const separators = [];
6400
6586
  this.newlines();
6401
6587
  const line = this.lexer.lineAt(this.current.offset);
6402
6588
  while (this.current.kind !== "end" && !stops.has(this.current.value)) {
6589
+ this.budget.admit();
6403
6590
  const pipelines = [this.pipeline()];
6404
6591
  const operators = [];
6405
6592
  while (this.is("&&") || this.is("||")) {
@@ -6416,21 +6603,25 @@ var Parser = class {
6416
6603
  this.newlines();
6417
6604
  } else if (!this.isEnd() && !this.is(")") && !(stops.has(this.current.value) && [";;", ";&", ";;&"].includes(this.current.value))) this.error("Expected command separator");
6418
6605
  }
6419
- const printed = printedSimpleLines(lists, separators);
6606
+ const printed = printedSimpleLines(lists, separators, this.budget);
6420
6607
  return { lists, line, ...printed };
6421
6608
  }
6422
6609
  pipeline() {
6610
+ this.budget.admit();
6423
6611
  const negate = this.is("!");
6424
6612
  if (negate) this.advance();
6425
6613
  const commands = [this.command()];
6426
6614
  while (this.is("|") || this.is("|&")) {
6427
6615
  const operator = this.advance();
6428
- if (operator.value === "|&") commands.at(-1).redirects.push({
6429
- descriptor: 2,
6430
- operator: ">&",
6431
- implicitPipeline: true,
6432
- target: { offset: operator.offset, plain: "1", spelling: "1", parts: [{ kind: "text", value: "1", quoted: false }] }
6433
- });
6616
+ if (operator.value === "|&") {
6617
+ this.budget.admit(3);
6618
+ commands.at(-1).redirects.push({
6619
+ descriptor: 2,
6620
+ operator: ">&",
6621
+ implicitPipeline: true,
6622
+ target: { offset: operator.offset, plain: "1", spelling: "1", parts: [{ kind: "text", value: "1", quoted: false }] }
6623
+ });
6624
+ }
6434
6625
  this.newlines();
6435
6626
  commands.push(this.command());
6436
6627
  }
@@ -6438,10 +6629,12 @@ var Parser = class {
6438
6629
  }
6439
6630
  command() {
6440
6631
  if (++this.nesting + this.lexer.depth > 64) this.error("Syntax nesting exceeds 64");
6632
+ this.budget.admit();
6441
6633
  const line = this.lexer.lineAt(this.current.offset);
6442
6634
  this.openCommands.push({ name: this.current.value, line });
6443
6635
  try {
6444
6636
  const command2 = this.commandInner();
6637
+ this.budget.admit();
6445
6638
  return { ...command2, line: command2.line ?? line };
6446
6639
  } finally {
6447
6640
  this.nesting--;
@@ -6449,6 +6642,7 @@ var Parser = class {
6449
6642
  }
6450
6643
  }
6451
6644
  commandInner() {
6645
+ this.budget.admit();
6452
6646
  let command2;
6453
6647
  if (this.is("[[")) {
6454
6648
  const start = this.current.end;
@@ -6470,7 +6664,9 @@ var Parser = class {
6470
6664
  admit2();
6471
6665
  this.advance();
6472
6666
  const expression2 = primary(depth + 1);
6473
- return expression2.kind === "not" && !grouped.has(expression2) ? expression2.operand : { kind: "not", operand: expression2 };
6667
+ if (expression2.kind === "not" && !grouped.has(expression2)) return expression2.operand;
6668
+ this.budget.admit();
6669
+ return { kind: "not", operand: expression2 };
6474
6670
  }
6475
6671
  if (this.is("(")) {
6476
6672
  this.advance();
@@ -6480,6 +6676,7 @@ var Parser = class {
6480
6676
  return expression2;
6481
6677
  }
6482
6678
  admit2();
6679
+ this.budget.admit();
6483
6680
  const first = operand();
6484
6681
  if (conditionalBinary.has(this.current.value)) {
6485
6682
  const operator = this.current.value;
@@ -6495,6 +6692,7 @@ var Parser = class {
6495
6692
  let expression2 = primary(depth);
6496
6693
  while (this.is("&&")) {
6497
6694
  admit2();
6695
+ this.budget.admit();
6498
6696
  this.advance();
6499
6697
  expression2 = { kind: "and", left: expression2, right: primary(depth) };
6500
6698
  }
@@ -6504,6 +6702,7 @@ var Parser = class {
6504
6702
  let expression2 = conjunction(depth);
6505
6703
  while (this.is("||")) {
6506
6704
  admit2();
6705
+ this.budget.admit();
6507
6706
  this.advance();
6508
6707
  expression2 = { kind: "or", left: expression2, right: conjunction(depth) };
6509
6708
  }
@@ -6519,7 +6718,7 @@ var Parser = class {
6519
6718
  const start = this.current.offset + 2;
6520
6719
  const end = arithmeticEnd(this.lexer.source, start);
6521
6720
  const source = this.lexer.source.slice(start, end);
6522
- command2 = { kind: "arithmetic", expression: prepareArithmetic(source), source, redirects: [] };
6721
+ command2 = { kind: "arithmetic", expression: prepareArithmetic(source, this.budget), source, redirects: [] };
6523
6722
  this.lexer.position = end + 2;
6524
6723
  this.lookahead = void 0;
6525
6724
  this.current = this.lexer.next();
@@ -6537,6 +6736,7 @@ var Parser = class {
6537
6736
  const condition = this.nonemptyScript(/* @__PURE__ */ new Set(["then"]));
6538
6737
  this.expect("then");
6539
6738
  const body = this.nonemptyScript(/* @__PURE__ */ new Set(["elif", "else", "fi"]));
6739
+ this.budget.admit();
6540
6740
  branches.push({ condition, body });
6541
6741
  if (!this.is("elif")) break;
6542
6742
  this.advance();
@@ -6569,6 +6769,7 @@ var Parser = class {
6569
6769
  const body = this.script(/* @__PURE__ */ new Set([";;", ";&", ";;&", "esac"]));
6570
6770
  if (![";;", ";&", ";;&", "esac"].includes(this.current.value)) this.error("Expected case terminator");
6571
6771
  const terminator = this.current.value;
6772
+ this.budget.admit();
6572
6773
  clauses.push({ patterns: patterns2, body, terminator });
6573
6774
  if (terminator === "esac") break;
6574
6775
  this.advance();
@@ -6612,7 +6813,7 @@ var Parser = class {
6612
6813
  this.newlines();
6613
6814
  if (!this.is("{")) this.error("Expected brace function body");
6614
6815
  command2 = { kind: "function", name: name2, body: this.command(), redirects: [] };
6615
- } else if (this.current.kind === "word" && !compoundHead(this.current.word) && this.peek().value === "(") {
6816
+ } else if (this.current.kind === "word" && !compoundHead(this.current.word, this.budget) && this.peek().value === "(") {
6616
6817
  const name2 = this.advance().value;
6617
6818
  if (!/^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(name2)) this.error("Invalid function name");
6618
6819
  this.expect("(");
@@ -6634,21 +6835,22 @@ var Parser = class {
6634
6835
  } else if (this.current.kind === "word") {
6635
6836
  line ??= wordLine;
6636
6837
  let word = this.advance().word;
6637
- const head = compoundHead(word);
6838
+ const head = compoundHead(word, this.budget);
6638
6839
  if (head && this.is("(")) {
6639
6840
  this.advance();
6640
6841
  const entries = [];
6641
6842
  this.newlines();
6642
6843
  while (this.current.kind === "word") {
6643
- entries.push(compoundEntry(this.advance().word));
6844
+ entries.push(compoundEntry(this.advance().word, this.budget));
6644
6845
  this.newlines();
6645
6846
  }
6646
6847
  if (!this.is(")")) this.error("Unsupported indexed-array compound assignment");
6647
6848
  const end = this.advance().end;
6849
+ this.budget.admit(2);
6648
6850
  word = { offset: word.offset, parts: word.parts, spelling: this.lexer.source.slice(word.offset, end) };
6649
6851
  setArrayAssignment(word, { kind: "compound", ...head, entries });
6650
6852
  } else {
6651
- const assignment = words.every((previous) => getArrayAssignment(previous) || scalarAssignmentName(previous)) ? elementAssignment(word) : void 0;
6853
+ const assignment = words.every((previous) => getArrayAssignment(previous) || scalarAssignmentName(previous)) ? elementAssignment(word, this.budget) : void 0;
6652
6854
  if (assignment) setArrayAssignment(word, assignment);
6653
6855
  }
6654
6856
  words.push(word);
@@ -6674,6 +6876,7 @@ var Parser = class {
6674
6876
  if (/^(?:>|>>|<|<<|<<-|<<<|>&|<&|>\|)$/u.test(next.value) && this.current.end === next.offset) descriptor2 = Number(this.advance().value);
6675
6877
  }
6676
6878
  if (!/^(?:>|>>|<|<<|<<-|<<<|>&|<&|>\||&>)$/u.test(this.current.value) || this.current.kind !== "operator") return void 0;
6879
+ this.budget.admit();
6677
6880
  const operator = this.advance().value;
6678
6881
  descriptor2 ??= operator.startsWith("<") ? 0 : 1;
6679
6882
  if (!Number.isSafeInteger(descriptor2) || descriptor2 > 255) this.error("File descriptor must be between 0 and 255");
@@ -6682,29 +6885,40 @@ var Parser = class {
6682
6885
  return { descriptor: descriptor2, operator, target: target.word, line: this.lexer.lineAt(Math.max(target.offset, target.end - 1)), ...(operator === "<&" || operator === ">&") && this.lexer.source[target.end - 1] === "-" ? { move: true } : {}, ...target.document ? { document: target.document } : {} };
6683
6886
  }
6684
6887
  };
6685
- function parseShell(source, depth = 0) {
6888
+ function parseShell(source, depth = 0, options3 = {}) {
6889
+ const budget = new ParseBudget(options3.maxParseUnits);
6686
6890
  const warnings = [];
6687
- const script = parseSource(source, depth, warnings);
6891
+ const script = parseSource(source, depth, warnings, 0, false, budget);
6892
+ budget.admit();
6688
6893
  return { ...script, ...warnings.length ? { warnings } : {} };
6689
6894
  }
6690
- function* hereDocumentWords(document, line, byteLocale2, warnings) {
6691
- if (document.quoted) yield { offset: document.offset, parts: [{ kind: "text", value: document.body, quoted: true }] };
6692
- else yield* new Lexer(document.body, document.depth, warnings, line - 1, byteLocale2, line).documentWords();
6895
+ function* hereDocumentWords(document, line, byteLocale2, warnings, budget = new ParseBudget()) {
6896
+ if (document.quoted) {
6897
+ budget.admit(2);
6898
+ yield { offset: document.offset, parts: [{ kind: "text", value: document.body, quoted: true }] };
6899
+ } else {
6900
+ budget.admit();
6901
+ yield* new Lexer(budget, document.body, document.depth, warnings, line - 1, byteLocale2, line).documentWords();
6902
+ }
6693
6903
  }
6694
- function parseShellUnit(source, position = 0, byteLocale2 = false) {
6904
+ function parseShellUnit(source, position = 0, byteLocale2 = false, budget = new ParseBudget()) {
6695
6905
  const warnings = [];
6696
- const parser = new Parser(source, 0, warnings, 0, position, byteLocale2);
6906
+ budget.admit();
6907
+ const parser = new Parser(budget, source, 0, warnings, 0, position, byteLocale2);
6697
6908
  const script = parser.script(/* @__PURE__ */ new Set(), true);
6698
6909
  const next = parser.current.end;
6699
6910
  const nul = source.indexOf("\0", position);
6700
6911
  if (nul >= 0 && nul < next) throw new ShellSyntaxError("NUL bytes are not valid shell source", nul);
6912
+ budget.admit(2);
6701
6913
  return { script: { ...script, ...warnings.length ? { warnings } : {} }, next };
6702
6914
  }
6703
- function parseShellInputUnit(source, byteLocale2 = false) {
6915
+ function parseShellInputUnit(source, byteLocale2 = false, budget = new ParseBudget()) {
6704
6916
  const warnings = [];
6705
6917
  try {
6706
- const parser = new Parser(source, 0, warnings, 0, 0, byteLocale2, true);
6918
+ budget.admit();
6919
+ const parser = new Parser(budget, source, 0, warnings, 0, 0, byteLocale2, true);
6707
6920
  const script = parser.script(/* @__PURE__ */ new Set(), true);
6921
+ budget.admit(2);
6708
6922
  return { script: { ...script, ...warnings.length ? { warnings } : {} }, next: parser.current.end };
6709
6923
  } catch (error) {
6710
6924
  if (error instanceof IncompleteShellInput) return void 0;
@@ -6712,8 +6926,9 @@ function parseShellInputUnit(source, byteLocale2 = false) {
6712
6926
  throw error;
6713
6927
  }
6714
6928
  }
6715
- function parseSource(source, depth, warnings, lineOffset = 0, byteLocale2 = false) {
6716
- const parser = new Parser(source, depth, warnings, lineOffset, void 0, byteLocale2);
6929
+ function parseSource(source, depth, warnings, lineOffset, byteLocale2, budget) {
6930
+ budget.admit();
6931
+ const parser = new Parser(budget, source, depth, warnings, lineOffset, void 0, byteLocale2);
6717
6932
  const script = parser.script();
6718
6933
  if (parser.current.kind !== "end") parser.error("Unexpected token");
6719
6934
  return script;
@@ -7118,7 +7333,7 @@ function evaluatePositionalArithmetic(program, options3, evaluate) {
7118
7333
  reserve(bytes2);
7119
7334
  const expanded = chunks.join("");
7120
7335
  options3.checkpoint();
7121
- return evaluate(prepareArithmetic(expanded));
7336
+ return evaluate(prepareArithmetic(expanded, options3.parseBudget));
7122
7337
  } finally {
7123
7338
  for (let position = admissions.length - 1; position >= 0; position--) admissions[position].release();
7124
7339
  header?.release();
@@ -10080,6 +10295,7 @@ async function matchEre(program, subject, ledger, signal) {
10080
10295
 
10081
10296
  // packages/safe-bash/src/shell/runtime.ts
10082
10297
  var defaultLimits = {
10298
+ maxParseUnits: defaultMaxParseUnits,
10083
10299
  maxInputBytes: 32 * 1024 * 1024,
10084
10300
  maxOutputBytes: 16 * 1024 * 1024,
10085
10301
  maxCommands: 1e4,
@@ -10170,11 +10386,13 @@ var Budget = class {
10170
10386
  constructor(limits, signal) {
10171
10387
  this.limits = limits;
10172
10388
  this.signal = signal ? AbortSignal.any([signal, this.controller.signal]) : this.controller.signal;
10389
+ this.parsing = new ParseBudget(limits.maxParseUnits, this.signal, (error) => this.controller.abort(error));
10173
10390
  this.values = new ValueArena(limits.maxExpansionBytes, limits.maxExpansionFields, () => this.signal.throwIfAborted(), (limit) => this.fail(limit));
10174
10391
  this.#wallClockDeadline = Date.now() + limits.maxWallClockMs;
10175
10392
  this.#armWallClock();
10176
10393
  }
10177
10394
  limits;
10395
+ parsing;
10178
10396
  values;
10179
10397
  commands = 0;
10180
10398
  iterations = 0;
@@ -11314,7 +11532,7 @@ var Runtime = class _Runtime {
11314
11532
  if (shellValueByteLength(value2) > this.budget.limits.maxExpansionBytes) this.budget.fail("maxExpansionBytes");
11315
11533
  if (name2 === "OPTIND" && state.getopts?.integer && origin !== "arithmetic") {
11316
11534
  try {
11317
- value2 = String(evaluateArithmetic(prepareArithmetic(shellValueText(value2) || "0"), this.arithmeticVariables(state)));
11535
+ value2 = String(evaluateArithmetic(prepareArithmetic(shellValueText(value2) || "0", this.budget.parsing), this.arithmeticVariables(state), this.budget.parsing));
11318
11536
  } catch (error) {
11319
11537
  this.rethrowArithmeticControl(error);
11320
11538
  throw new ExpansionFailure(message(error));
@@ -12020,6 +12238,7 @@ var Runtime = class _Runtime {
12020
12238
  if (command2.kind === "arithmetic") {
12021
12239
  try {
12022
12240
  return Number(evaluatePositionalArithmetic(command2.expression, {
12241
+ parseBudget: this.budget.parsing,
12023
12242
  positional: state.positional,
12024
12243
  arg0: state.arg0 ?? "virtual-bash",
12025
12244
  owner: arrayStore(state)?.owner,
@@ -12027,7 +12246,7 @@ var Runtime = class _Runtime {
12027
12246
  checkpoint: () => this.signal.throwIfAborted(),
12028
12247
  requireParameter: (name2, value2) => this.requireParameter(value2, name2, state, io),
12029
12248
  limit: () => this.budget.fail("maxExpansionBytes")
12030
- }, (prepared) => evaluateArithmetic(prepared, this.arithmeticVariables(state, io.diagnosticLine))) === 0n);
12249
+ }, (prepared) => evaluateArithmetic(prepared, this.arithmeticVariables(state, io.diagnosticLine), this.budget.parsing)) === 0n);
12031
12250
  } catch (error) {
12032
12251
  this.rethrowArithmeticControl(error);
12033
12252
  throw new Error(`((: ${message(error)}`);
@@ -12181,7 +12400,7 @@ var Runtime = class _Runtime {
12181
12400
  let words = 0;
12182
12401
  const warnings = [];
12183
12402
  try {
12184
- for (const word of hereDocumentWords(document, line, byteLocale(state.variables), warnings)) {
12403
+ for (const word of hereDocumentWords(document, line, byteLocale(state.variables), warnings, this.budget.parsing)) {
12185
12404
  this.signal.throwIfAborted();
12186
12405
  for (const warning of warnings.splice(0)) await writeText(io.stderr, `shell: warning: ${warning}
12187
12406
  `);
@@ -13073,7 +13292,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
13073
13292
  try {
13074
13293
  do {
13075
13294
  this.signal.throwIfAborted();
13076
- const unit = parseShellUnit(source, position, byteLocale(state.variables));
13295
+ const unit = parseShellUnit(source, position, byteLocale(state.variables), this.budget.parsing);
13077
13296
  for (const warning of unit.script.warnings ?? []) await writeText(io.stderr, `${io.scriptName}: warning: ${warning}
13078
13297
  `);
13079
13298
  if (unit.script.lists.length) {
@@ -13110,7 +13329,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
13110
13329
  if (bytes2) source += this.sourceText(bytes2, io.scriptName ?? "shell");
13111
13330
  const unitIO = { ...io, diagnosticOffset: offset };
13112
13331
  try {
13113
- const unit = eof ? parseShellUnit(source, 0, byteLocale(state.variables)) : parseShellInputUnit(source, byteLocale(state.variables));
13332
+ const unit = eof ? parseShellUnit(source, 0, byteLocale(state.variables), this.budget.parsing) : parseShellInputUnit(source, byteLocale(state.variables), this.budget.parsing);
13114
13333
  if (unit) {
13115
13334
  for (const warning of unit.script.warnings ?? []) await writeText(io.stderr, `${io.scriptName}: warning: ${warning}
13116
13335
  `);
@@ -13374,7 +13593,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
13374
13593
  let position = 0;
13375
13594
  do {
13376
13595
  this.signal.throwIfAborted();
13377
- const unit = parseShellUnit(source, position, byteLocale(context.env));
13596
+ const unit = parseShellUnit(source, position, byteLocale(context.env), this.budget.parsing);
13378
13597
  units.push(unit.script);
13379
13598
  position = unit.next;
13380
13599
  } while (position < source.length);
@@ -13407,7 +13626,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
13407
13626
  try {
13408
13627
  do {
13409
13628
  this.signal.throwIfAborted();
13410
- const unit = parseShellUnit(source, position, byteLocale(state.variables));
13629
+ const unit = parseShellUnit(source, position, byteLocale(state.variables), this.budget.parsing);
13411
13630
  for (const warning of unit.script.warnings ?? []) await writeText(io.stderr, `${io.scriptName ?? "shell"}: warning: ${warning}
13412
13631
  `);
13413
13632
  if (unit.script.lists.length) {
@@ -13721,7 +13940,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
13721
13940
  for (let index = offset; index < args.length; index++) {
13722
13941
  this.signal.throwIfAborted();
13723
13942
  try {
13724
- value2 = evaluateArithmetic(prepareArithmetic(args[index]), variables);
13943
+ value2 = evaluateArithmetic(prepareArithmetic(args[index], this.budget.parsing), variables, this.budget.parsing);
13725
13944
  } catch (error) {
13726
13945
  this.rethrowArithmeticControl(error);
13727
13946
  throw new Error(`let: ${message(error)}`);
@@ -14386,8 +14605,10 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14386
14605
  else {
14387
14606
  let index;
14388
14607
  try {
14389
- index = numericIndex(literalIndex(selector, 0));
14390
- } catch {
14608
+ index = numericIndex(literalIndex(selector, 0, this.budget.parsing));
14609
+ } catch (error) {
14610
+ this.signal.throwIfAborted();
14611
+ if (error instanceof ShellLimitError) throw error;
14391
14612
  await this.diagnostic(context, "indexed array: unsupported subscript");
14392
14613
  status = 2;
14393
14614
  continue;
@@ -14591,6 +14812,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14591
14812
  if (part.kind === "arithmetic") {
14592
14813
  try {
14593
14814
  return String(evaluatePositionalArithmetic(part.expression, {
14815
+ parseBudget: this.budget.parsing,
14594
14816
  positional: state.positional,
14595
14817
  arg0: state.arg0 ?? "virtual-bash",
14596
14818
  owner: arrayStore(state)?.owner,
@@ -14598,7 +14820,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14598
14820
  checkpoint: () => this.signal.throwIfAborted(),
14599
14821
  requireParameter: (name2, value3) => this.requireParameter(value3, name2, state, io, part.line),
14600
14822
  limit: () => this.budget.fail("maxExpansionBytes")
14601
- }, (prepared) => evaluateArithmetic(prepared, this.arithmeticVariables(state, io.diagnosticLine ?? part.line))));
14823
+ }, (prepared) => evaluateArithmetic(prepared, this.arithmeticVariables(state, io.diagnosticLine ?? part.line), this.budget.parsing)));
14602
14824
  } catch (error) {
14603
14825
  this.rethrowArithmeticControl(error);
14604
14826
  throw new ExpansionFailure(message(error), io.diagnosticLine ?? part.line);
@@ -14737,7 +14959,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14737
14959
  }
14738
14960
  this.signal.throwIfAborted();
14739
14961
  try {
14740
- return { value: evaluateArithmetic(prepareArithmetic(source), variables), source };
14962
+ return { value: evaluateArithmetic(prepareArithmetic(source, this.budget.parsing), variables, this.budget.parsing), source };
14741
14963
  } catch (error) {
14742
14964
  this.rethrowArithmeticControl(error);
14743
14965
  throw new ExpansionFailure(`${part.name}: ${message(error)}`, line);
@@ -14867,29 +15089,67 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14867
15089
  const arrayOwned = word.parts.some((part) => part.kind === "variable" && (getArraySelector(part) !== void 0 || arrayStore(state)?.get(part.name) !== void 0));
14868
15090
  const owner = arrayOwned ? requireArrays(state).owner : void 0;
14869
15091
  const holding = owner?.hold();
15092
+ const scratch = !owner && split && state.variables.IFS !== "" && word.parts.some((part) => !part.quoted && part.kind !== "text") ? this.budget.values.scope() : void 0;
14870
15093
  try {
14871
15094
  if (owner) await this.prepareArrayObservers(state, owner);
14872
15095
  owner?.reserve({ metadata: 128 + word.parts.length * 32, allocatedSlots: word.parts.length + 1, work: word.parts.length + 5 });
14873
- const fields = [{ value: "", fragments: [], bytes: false, pattern: "", present: false }];
15096
+ scratch?.reserve(word.parts.length * 32, 0);
15097
+ const fields = [];
15098
+ const addField = () => {
15099
+ if (fields.length >= this.budget.limits.maxExpansionFields) this.budget.fail("maxExpansionFields");
15100
+ scratch?.reserve(32, 0);
15101
+ owner?.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
15102
+ fields.push({ fragments: [], bytes: false, patterns: void 0, present: false });
15103
+ };
15104
+ addField();
14874
15105
  let expansionBytes = 0;
14875
15106
  const append = (value2, glob, present) => {
14876
15107
  const text = shellValueText(value2);
14877
15108
  const size = shellValueByteLength(value2);
14878
15109
  if (size > this.budget.limits.maxExpansionBytes - expansionBytes) this.budget.fail("maxExpansionBytes");
14879
15110
  expansionBytes += size;
14880
- regexAppend?.(text, !glob);
14881
15111
  const field = fields.at(-1);
14882
- if (owner) owner.reserve({ payload: exactSum(import_buffer.Buffer.byteLength(field.value) + size, import_buffer.Buffer.byteLength(field.pattern) + (glob ? size : size * 2)), metadata: 64, work: text.length + 8 });
15112
+ let escapes = 0;
15113
+ if (!glob) {
15114
+ const special2 = conditionalPattern ? "\\*?[]-^()|+!@" : "\\*?[]-^";
15115
+ for (const character of text) if (special2.includes(character)) escapes++;
15116
+ }
15117
+ scratch?.reserve(32, 0);
15118
+ if (owner) owner.reserve({ payload: size + (escapes ? size + escapes : 0), metadata: 64, work: text.length + 8 });
15119
+ if (escapes) {
15120
+ scratch?.reserve((field.patterns ? 32 : 32 * (field.fragments.length + 1)) + (text.length + escapes) * 2, 0);
15121
+ field.patterns ??= field.fragments.map(shellValueText);
15122
+ field.patterns.push(text.replace(conditionalPattern ? /[\\*?[\]\-^()|+!@]/gu : /[\\*?[\]\-^]/gu, "\\$&"));
15123
+ } else if (field.patterns) {
15124
+ scratch?.reserve(32, 0);
15125
+ field.patterns.push(text);
15126
+ }
14883
15127
  if (typeof value2 !== "string" || field.bytes) {
14884
- io[valueScope]?.reserve(32 * (field.bytes ? 1 : field.fragments.length + 1), field.bytes ? 1 : field.fragments.length + 1);
15128
+ if (!scratch) io[valueScope]?.reserve(32 * (field.bytes ? 1 : field.fragments.length + 1), field.bytes ? 1 : field.fragments.length + 1);
14885
15129
  if (typeof value2 !== "string") io[valueScope]?.hold(value2);
14886
15130
  field.bytes = true;
14887
15131
  }
15132
+ regexAppend?.(text, !glob);
14888
15133
  field.fragments.push(value2);
14889
- field.value += text;
14890
- field.pattern += glob ? text : text.replace(conditionalPattern ? /[\\*?[\]\-^()|+!@]/gu : /[\\*?[\]\-^]/gu, "\\$&");
14891
15134
  field.present ||= present;
14892
15135
  };
15136
+ const appendSplit = async (value2) => {
15137
+ const separators = state.variables.IFS ?? " \n";
15138
+ let boundary = false;
15139
+ for await (const piece of this.splitValue(value2, separators, io, scratch)) {
15140
+ if (typeof piece === "string" && separators.includes(piece)) {
15141
+ if (!" \n".includes(piece)) {
15142
+ fields.at(-1).present = true;
15143
+ addField();
15144
+ } else if (fields.at(-1).present) boundary = true;
15145
+ } else {
15146
+ if (boundary) addField();
15147
+ boundary = false;
15148
+ append(piece, true, true);
15149
+ }
15150
+ }
15151
+ if (boundary) addField();
15152
+ };
14893
15153
  const parts = word.parts.map((part) => ({ part, splitText: false }));
14894
15154
  for (let index = 0; index < parts.length; index++) {
14895
15155
  const { part, splitText } = parts[index];
@@ -14898,6 +15158,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14898
15158
  const value2 = this.variable(state, part.name);
14899
15159
  const missing = value2 === void 0 || part.operator.startsWith(":") && value2 === "";
14900
15160
  if (part.operator.endsWith("+") ? !missing : missing) {
15161
+ scratch?.reserve(part.alternate.parts.length * 32, 0);
14901
15162
  const alternate = part.alternate.parts.map((entry) => ({ part: copyArraySelector(entry, { ...entry, quoted: entry.quoted || part.quoted }), splitText: true }));
14902
15163
  if (!alternate.length && part.quoted) append("", false, true);
14903
15164
  parts.splice(index + 1, 0, ...alternate);
@@ -14908,37 +15169,10 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14908
15169
  if (part.kind === "variable" && selector?.kind === "members" && !part.length && split && (!part.quoted || selector.separator === "@")) {
14909
15170
  const members = await this.arrayMembers(part.name, state);
14910
15171
  for (let position = 0; position < members.length; position++) {
14911
- if (position > 0) {
14912
- owner?.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
14913
- fields.push({ value: "", fragments: [], bytes: false, pattern: "", present: false });
14914
- }
15172
+ if (position > 0) addField();
14915
15173
  const value2 = members[position];
14916
15174
  if (part.quoted || state.variables.IFS === "") append(value2, !part.quoted, part.quoted || value2.length > 0);
14917
- else {
14918
- const separators = state.variables.IFS ?? " \n";
14919
- let boundary = false;
14920
- for (const character of value2) {
14921
- if (separators.includes(character)) {
14922
- if (!/[ \t\n]/u.test(character)) {
14923
- fields.at(-1).present = true;
14924
- owner?.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
14925
- fields.push({ value: "", fragments: [], bytes: false, pattern: "", present: false });
14926
- } else if (fields.at(-1).present) boundary = true;
14927
- } else {
14928
- if (boundary) {
14929
- owner?.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
14930
- fields.push({ value: "", fragments: [], bytes: false, pattern: "", present: false });
14931
- }
14932
- boundary = false;
14933
- append(character, true, true);
14934
- }
14935
- await owner.ledger.checkpoint(this.signal);
14936
- }
14937
- if (boundary) {
14938
- owner?.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
14939
- fields.push({ value: "", fragments: [], bytes: false, pattern: "", present: false });
14940
- }
14941
- }
15175
+ else await appendSplit(value2);
14942
15176
  }
14943
15177
  } else if (part.kind === "text" && !splitText) {
14944
15178
  let value2 = invokedValues.get(part) ?? part.byteValue ?? part.value;
@@ -14946,32 +15180,14 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14946
15180
  append(value2, !part.quoted, quotedPresence || shellValueByteLength(value2) > 0);
14947
15181
  } else if (part.kind === "variable" && part.name === "@" && part.quoted && !part.operator && split) {
14948
15182
  for (let position = 0; position < state.positional.length; position++) {
14949
- if (position > 0) fields.push({ value: "", fragments: [], bytes: false, pattern: "", present: false });
15183
+ if (position > 0) addField();
14950
15184
  append(stateMonitor(state)?.positionals.get(String(position), state.positional[position]) ?? state.positional[position], false, true);
14951
15185
  }
14952
15186
  if (state.positional.length === 0 && word.parts.every((entry) => entry.kind === "text" && entry.value === "" || entry === part)) fields[0].present = false;
14953
15187
  } else {
14954
15188
  const value2 = part.kind === "text" ? part.byteValue ?? part.value : await this.valuePart(part, state, io, hereString);
14955
15189
  if (part.quoted || !split || state.variables.IFS === "") append(value2, !part.quoted, quotedPresence || !split || shellValueByteLength(value2) > 0);
14956
- else {
14957
- const separators = state.variables.IFS ?? " \n";
14958
- let boundary = false;
14959
- const pieces = this.splitValue(value2, separators, io);
14960
- for (const character of pieces) {
14961
- const text = shellValueText(character);
14962
- if (typeof character === "string" && separators.includes(character)) {
14963
- if (!/[ \t\n]/u.test(text)) {
14964
- fields.at(-1).present = true;
14965
- fields.push({ value: "", fragments: [], bytes: false, pattern: "", present: false });
14966
- } else if (fields.at(-1).present) boundary = true;
14967
- } else {
14968
- if (boundary) fields.push({ value: "", fragments: [], bytes: false, pattern: "", present: false });
14969
- boundary = false;
14970
- append(character, true, true);
14971
- }
14972
- }
14973
- if (boundary) fields.push({ value: "", fragments: [], bytes: false, pattern: "", present: false });
14974
- }
15190
+ else await appendSplit(value2);
14975
15191
  }
14976
15192
  if (fields.length > this.budget.limits.maxExpansionFields) this.budget.fail("maxExpansionFields");
14977
15193
  if (owner) await owner.ledger.checkpoint(this.signal);
@@ -14980,14 +15196,23 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14980
15196
  let resultBytes = 0;
14981
15197
  for (const field of fields) {
14982
15198
  if (!field.present && split) continue;
15199
+ if (scratch && field.fragments.length > 1 && !field.bytes) scratch.reserve(field.fragments.reduce((bytes2, value2) => bytes2 + shellValueText(value2).length * 2, 0), 0);
14983
15200
  const assembled = concatShellValues(field.fragments, io[valueScope]);
14984
15201
  const projection = shellValueText(assembled);
14985
- const expanded = split ? await this.glob(projection, field.pattern, state) : [pattern ? field.pattern : projection];
15202
+ if (field.bytes && field.fragments.length > 1 && !field.patterns) {
15203
+ scratch?.reserve(field.fragments.length * 32, 0);
15204
+ field.patterns = field.fragments.map(shellValueText);
15205
+ }
15206
+ if (scratch && field.patterns && field.patterns.length > 1) scratch.reserve(field.patterns.reduce((bytes2, text) => bytes2 + text.length * 2, 0), 0);
15207
+ const fieldPattern = field.patterns ? field.patterns.join("") : projection;
15208
+ const expanded = split ? await this.glob(projection, fieldPattern, state) : [pattern ? fieldPattern : projection];
14986
15209
  for (const text of expanded) {
15210
+ if (result.length >= this.budget.limits.maxExpansionFields) this.budget.fail("maxExpansionFields");
14987
15211
  const value2 = !pattern && expanded.length === 1 && text === projection ? assembled : text;
14988
15212
  const size = shellValueByteLength(value2);
14989
15213
  if (size > this.budget.limits.maxExpansionBytes - resultBytes) this.budget.fail("maxExpansionBytes");
14990
15214
  resultBytes += size;
15215
+ scratch?.reserve(32, 0);
14991
15216
  owner?.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
14992
15217
  result.push(value2);
14993
15218
  }
@@ -14995,17 +15220,50 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14995
15220
  }
14996
15221
  return result;
14997
15222
  } finally {
15223
+ scratch?.close();
14998
15224
  holding?.release();
14999
15225
  }
15000
15226
  }
15001
- *splitValue(value2, separators, io) {
15227
+ splitWork;
15228
+ async *splitValue(value2, separators, io, scratch) {
15229
+ this.budget.cpuCheckpoint();
15230
+ const work = this.splitWork ??= { scanned: 0 };
15002
15231
  if (typeof value2 === "string" || Array.from(separators).some((character) => character.charCodeAt(0) > 127)) {
15003
- yield* shellValueText(value2);
15232
+ const text = shellValueText(value2);
15233
+ const slice = (start3, end) => {
15234
+ if (start3 === 0 && end === text.length) return text;
15235
+ scratch?.reserve((end - start3) * 2, 0);
15236
+ return text.slice(start3, end);
15237
+ };
15238
+ let start2 = 0;
15239
+ for (let index = 0; index < text.length; ) {
15240
+ const character = String.fromCodePoint(text.codePointAt(index));
15241
+ const end = index + character.length;
15242
+ if (separators.includes(character)) {
15243
+ if (start2 < index) yield slice(start2, index);
15244
+ yield character;
15245
+ start2 = end;
15246
+ } else if (end - start2 >= 4096) {
15247
+ yield slice(start2, end);
15248
+ start2 = end;
15249
+ }
15250
+ work.scanned += character.length;
15251
+ index = end;
15252
+ if (work.scanned >= 4096) {
15253
+ work.scanned = 0;
15254
+ await yieldTurn(this.signal);
15255
+ }
15256
+ }
15257
+ if (start2 < text.length) yield slice(start2, text.length);
15004
15258
  return;
15005
15259
  }
15006
15260
  const bytes2 = shellValueBytes(value2, io[valueScope]);
15007
15261
  let start = 0;
15008
15262
  for (let index = 0; index < bytes2.length; index++) {
15263
+ if (++work.scanned >= 4096) {
15264
+ work.scanned = 0;
15265
+ await yieldTurn(this.signal);
15266
+ }
15009
15267
  const byte = bytes2[index];
15010
15268
  if (byte > 127 || !separators.includes(String.fromCharCode(byte))) continue;
15011
15269
  if (start < index) yield shellValueFromBytes(bytes2.subarray(start, index), io[valueScope]);
@@ -15696,7 +15954,7 @@ var Shell = class {
15696
15954
  let failed = false;
15697
15955
  try {
15698
15956
  try {
15699
- let unit = parseShellUnit(source, 0, byteLocale({ ...this.#options.env, ...options3.env }));
15957
+ let unit = parseShellUnit(source, 0, byteLocale({ ...this.#options.env, ...options3.env }), budget.parsing);
15700
15958
  stdin = new ShellInput(typeof options3.stdin === "string" || options3.stdin instanceof Uint8Array ? toByteSource(options3.stdin) : options3.stdin ?? toByteSource(""), budget);
15701
15959
  io.stdin = stdin;
15702
15960
  await interruptible(this.#ready, budget.signal);
@@ -15753,7 +16011,7 @@ var Shell = class {
15753
16011
  }
15754
16012
  if (unit.next >= source.length) break;
15755
16013
  budget.signal.throwIfAborted();
15756
- unit = parseShellUnit(source, unit.next, byteLocale(state.variables));
16014
+ unit = parseShellUnit(source, unit.next, byteLocale(state.variables), budget.parsing);
15757
16015
  }
15758
16016
  } catch (error) {
15759
16017
  if (!(error instanceof ShellSyntaxError)) throw error;
@@ -15830,6 +16088,7 @@ shell: -c: line ${line}: \`${source.split("\n")[line - 1] ?? ""}'
15830
16088
  // packages/safe-bash/src/shell/worker-limits.ts
15831
16089
  init_platform();
15832
16090
  var cloudflareWorkerLimits = Object.freeze({
16091
+ maxParseUnits: 65536,
15833
16092
  maxInputBytes: 4 * 1024 * 1024,
15834
16093
  maxOutputBytes: 4 * 1024 * 1024,
15835
16094
  maxCommands: 1e3,