@poe-platform/safe-bash 0.1.79 → 0.1.80

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.
@@ -5734,6 +5734,46 @@ init_platform();
5734
5734
 
5735
5735
  // packages/safe-bash/src/shell/pattern.ts
5736
5736
  init_platform();
5737
+
5738
+ // packages/safe-bash/src/shell/string-operations.ts
5739
+ init_platform();
5740
+ function stringCheckpoint(work, units = 1) {
5741
+ work.signal.throwIfAborted();
5742
+ work.remaining -= units;
5743
+ if (work.remaining < 0) work.exhausted();
5744
+ work.steps = (work.steps ?? 0) + units;
5745
+ if (work.steps < 128) return void 0;
5746
+ work.steps %= 128;
5747
+ return yieldTurn(work.signal);
5748
+ }
5749
+ function nextCodePointOffset(value2, offset) {
5750
+ if (offset >= value2.length) return value2.length;
5751
+ return offset + (value2.codePointAt(offset) > 65535 ? 2 : 1);
5752
+ }
5753
+ function previousCodePointOffset(value2, offset) {
5754
+ if (offset <= 0) return 0;
5755
+ const last = value2.charCodeAt(offset - 1);
5756
+ const previous = value2.charCodeAt(offset - 2);
5757
+ return offset - (last >= 56320 && last <= 57343 && previous >= 55296 && previous <= 56319 ? 2 : 1);
5758
+ }
5759
+ async function scanString(value2, work, start = 0, end = value2.length, maximum = Infinity) {
5760
+ let position = start;
5761
+ let count2 = 0;
5762
+ let bytes2 = 0;
5763
+ while (position < end && count2 < maximum) {
5764
+ const pending = stringCheckpoint(work);
5765
+ if (pending) await pending;
5766
+ work.signal.throwIfAborted();
5767
+ const point = position + 1 < end ? value2.codePointAt(position) : value2.charCodeAt(position);
5768
+ position += point > 65535 ? 2 : 1;
5769
+ bytes2 += point <= 127 ? 1 : point <= 2047 ? 2 : point <= 65535 ? 3 : 4;
5770
+ count2++;
5771
+ }
5772
+ work.signal.throwIfAborted();
5773
+ return { end: position, count: count2, bytes: bytes2 };
5774
+ }
5775
+
5776
+ // packages/safe-bash/src/shell/pattern.ts
5737
5777
  var characterClasses = {
5738
5778
  alnum: "a-zA-Z0-9",
5739
5779
  alpha: "a-zA-Z",
@@ -5751,6 +5791,9 @@ var characterClasses = {
5751
5791
  xdigit: "a-fA-F0-9"
5752
5792
  };
5753
5793
  async function tokens(pattern, work) {
5794
+ const admission = stringCheckpoint(work, pattern.length);
5795
+ if (admission) await admission;
5796
+ work.allocation?.reserve(128 + pattern.length * 64, 0);
5754
5797
  const result = [];
5755
5798
  const characters = Array.from(pattern);
5756
5799
  const lastClosingBracket = characters.lastIndexOf("]");
@@ -5818,37 +5861,36 @@ async function tokens(pattern, work) {
5818
5861
  async function compilePattern(pattern, work) {
5819
5862
  work.signal.throwIfAborted();
5820
5863
  const patternTokens = await tokens(pattern, work);
5821
- return (value2) => matchTokens(patternTokens, value2, work);
5864
+ return (value2, start = 0, end = value2.length) => matchTokens(patternTokens, value2, work, start, end);
5822
5865
  }
5823
5866
  async function matchesPattern(pattern, value2, work) {
5824
5867
  return (await compilePattern(pattern, work))(value2);
5825
5868
  }
5826
- async function matchTokens(patternTokens, value2, work) {
5869
+ async function matchTokens(patternTokens, value2, work, start, end) {
5827
5870
  work.signal.throwIfAborted();
5828
- const characters = Array.from(value2);
5829
- let position = 0;
5871
+ let position = start;
5830
5872
  let tokenIndex = 0;
5831
5873
  let star = -1;
5832
- let retry = 0;
5833
- let steps = 0;
5834
- while (position < characters.length) {
5835
- if (--work.remaining < 0) work.exhausted();
5836
- if (++steps % 1024 === 0) {
5837
- await yieldTurn(work.signal);
5838
- work.signal.throwIfAborted();
5839
- }
5874
+ let retry = start;
5875
+ while (position < end) {
5876
+ const pending = stringCheckpoint(work);
5877
+ if (pending) await pending;
5878
+ work.signal.throwIfAborted();
5840
5879
  const token = patternTokens[tokenIndex];
5880
+ const point = value2.codePointAt(position);
5841
5881
  if (token?.kind === "star") {
5842
5882
  star = tokenIndex++;
5843
5883
  retry = position;
5844
- } else if (token && (token.kind === "any" || (token.kind === "literal" ? token.value === characters[position] : token.expression.test(characters[position])))) {
5845
- position++;
5884
+ } else if (token && (token.kind === "any" || (token.kind === "literal" ? token.value.codePointAt(0) === point : token.expression.test(String.fromCodePoint(point))))) {
5885
+ position += point > 65535 ? 2 : 1;
5846
5886
  tokenIndex++;
5847
5887
  } else if (star !== -1) {
5848
5888
  tokenIndex = star + 1;
5849
- position = ++retry;
5889
+ retry = nextCodePointOffset(value2, retry);
5890
+ position = retry;
5850
5891
  } else return false;
5851
5892
  }
5893
+ work.signal.throwIfAborted();
5852
5894
  while (patternTokens[tokenIndex]?.kind === "star") tokenIndex++;
5853
5895
  return tokenIndex === patternTokens.length;
5854
5896
  }
@@ -6082,6 +6124,7 @@ var Lexer = class {
6082
6124
  documentLine;
6083
6125
  partial;
6084
6126
  position = 0;
6127
+ operandDepth = 0;
6085
6128
  printedNewlineReduction = 0;
6086
6129
  unprintedWords = 0;
6087
6130
  delimiterOperator;
@@ -6464,7 +6507,7 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6464
6507
  if (this.source[this.position] !== "`") this.error("Unterminated command substitution");
6465
6508
  this.position++;
6466
6509
  try {
6467
- parts.push({ kind: "substitution", script: parseSource(source, this.depth + 1, this.warnings, line - 1, this.byteLocale, this.budget), line, quoted });
6510
+ parts.push({ kind: "substitution", script: parseSource(source, this.depth + this.operandDepth + 1, this.warnings, line - 1, this.byteLocale, this.budget), line, quoted });
6468
6511
  } catch (error) {
6469
6512
  if (this.documentLine === void 0 || !(error instanceof ShellSyntaxError) || /nesting|exceeds/u.test(error.reason)) throw error;
6470
6513
  this.budget.admit();
@@ -6486,7 +6529,7 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6486
6529
  let script;
6487
6530
  try {
6488
6531
  this.budget.admit();
6489
- nested = new Parser(this.budget, this.source.slice(start), this.depth + 1, this.warnings, this.lineAt(start) - 1, void 0, this.byteLocale);
6532
+ nested = new Parser(this.budget, this.source.slice(start), this.depth + this.operandDepth + 1, this.warnings, this.lineAt(start) - 1, void 0, this.byteLocale);
6490
6533
  script = nested.script(/* @__PURE__ */ new Set([")"]));
6491
6534
  } catch (error) {
6492
6535
  if (this.documentLine !== void 0 && error instanceof ShellSyntaxError && !/nesting|exceeds/u.test(error.reason)) throw this.documentSubstitutionError(this.source.slice(start), error);
@@ -6524,25 +6567,33 @@ shell: command substitution: line ${line}: \`${sourceLine}'
6524
6567
  let alternate;
6525
6568
  let replacement;
6526
6569
  let substring;
6527
- if (operator) {
6528
- if (length) this.error("Invalid length expansion");
6529
- this.position += operator.length;
6530
- alternate = this.word(operator.startsWith("/") ? "/}" : "}", quoted && !["#", "##", "%", "%%"].includes(operator) && !operator.startsWith("/"));
6531
- if (operator.startsWith("/") && this.source[this.position] === "/") {
6532
- this.position++;
6533
- replacement = this.word("}");
6534
- }
6535
- } else if (this.source[this.position] === ":") {
6536
- if (length || !/^(?:[a-zA-Z_][a-zA-Z_0-9]*|[0-9]+)$/u.test(name2)) this.error("Unsupported non-scalar substring expansion");
6537
- this.position++;
6538
- const offset = this.word(":}", true, false, true);
6539
- let substringLength;
6540
- if (this.source[this.position] === ":") {
6541
- this.position++;
6542
- substringLength = this.word("}", true, false, true);
6570
+ if (operator || this.source[this.position] === ":") {
6571
+ if (this.depth + this.operandDepth + 1 > 64) this.error("Syntax nesting exceeds 64");
6572
+ this.operandDepth++;
6573
+ try {
6574
+ if (operator) {
6575
+ if (length) this.error("Invalid length expansion");
6576
+ this.position += operator.length;
6577
+ alternate = this.word(operator.startsWith("/") ? "/}" : "}", quoted && !["#", "##", "%", "%%"].includes(operator) && !operator.startsWith("/"));
6578
+ if (operator.startsWith("/") && this.source[this.position] === "/") {
6579
+ this.position++;
6580
+ replacement = this.word("}");
6581
+ }
6582
+ } else {
6583
+ if (length || !/^(?:[a-zA-Z_][a-zA-Z_0-9]*|[0-9]+)$/u.test(name2)) this.error("Unsupported non-scalar substring expansion");
6584
+ this.position++;
6585
+ const offset = this.word(":}", true, false, true);
6586
+ let substringLength;
6587
+ if (this.source[this.position] === ":") {
6588
+ this.position++;
6589
+ substringLength = this.word("}", true, false, true);
6590
+ }
6591
+ this.budget.admit();
6592
+ substring = { offset, ...substringLength ? { length: substringLength } : {}, source: this.source.slice(parameterStart, this.position + 1) };
6593
+ }
6594
+ } finally {
6595
+ this.operandDepth--;
6543
6596
  }
6544
- this.budget.admit();
6545
- substring = { offset, ...substringLength ? { length: substringLength } : {}, source: this.source.slice(parameterStart, this.position + 1) };
6546
6597
  }
6547
6598
  if (this.source[this.position] !== "}") this.error("Unterminated or unsupported parameter expansion");
6548
6599
  this.position++;
@@ -14822,7 +14873,14 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14822
14873
  holding?.release();
14823
14874
  }
14824
14875
  }
14876
+ parameterOperandIO(word, state, io) {
14877
+ this.signal.throwIfAborted();
14878
+ const parameterDepth = (io.parameterDepth ?? 0) + 1;
14879
+ if (state.depth + parameterDepth > 64) throw new ShellSyntaxError("Syntax nesting exceeds 64", word.offset);
14880
+ return { ...io, parameterDepth };
14881
+ }
14825
14882
  async partValue(part, state, io, hereString) {
14883
+ this.signal.throwIfAborted();
14826
14884
  if (part.kind === "failed-substitution") {
14827
14885
  if (state.depth >= this.budget.limits.maxSubstitutionDepth) this.budget.fail("maxSubstitutionDepth");
14828
14886
  await writeText(io.stderr, part.diagnostic);
@@ -14848,6 +14906,8 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14848
14906
  }
14849
14907
  if (part.kind === "substitution") {
14850
14908
  if (state.depth >= this.budget.limits.maxSubstitutionDepth) this.budget.fail("maxSubstitutionDepth");
14909
+ const parameterDepth = io.parameterDepth ?? 0;
14910
+ if (parameterDepth > 0 && state.depth + parameterDepth + 1 > 64) throw new ShellSyntaxError("Syntax nesting exceeds 64", 0);
14851
14911
  const capture = new Capture();
14852
14912
  const child = await cloneState(state, this.signal);
14853
14913
  child.isolated = true;
@@ -14898,7 +14958,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14898
14958
  if (index === void 0) throw new ArrayFailure("index outside 0..2147483647");
14899
14959
  const value3 = binding ? binding.get(index) : index === 0 ? state.variables[part.name] : void 0;
14900
14960
  this.requireParameter(value3, `${part.name}[${selector.index}]`, state, io, part.line);
14901
- return part.length ? String(Array.from(value3 ?? "").length) : value3 ?? "";
14961
+ return part.length ? this.parameterLength(value3 ?? "") : value3 ?? "";
14902
14962
  }
14903
14963
  if (part.length) return String(binding?.values.size ?? (state.variables[part.name] === void 0 ? 0 : 1));
14904
14964
  const values = await this.arrayMembers(part.name, state);
@@ -14927,17 +14987,18 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14927
14987
  const missing = value2 === void 0 || part.operator.startsWith(":") && value2 === "";
14928
14988
  const operator = part.operator.at(-1);
14929
14989
  if (operator === "+" && !missing || operator !== "+" && missing) {
14990
+ const operandIO = this.parameterOperandIO(part.alternate, state, io);
14930
14991
  let alternate;
14931
14992
  if (operator === "=" && arrayStore(state)?.get(part.name)) {
14932
14993
  alternate = "";
14933
14994
  await this.arrayZero(state, part.name, async () => {
14934
- alternate = await this.arrayJoin(requireArrays(state).owner, await this.word(part.alternate, state, io, false, false, hereString), "");
14995
+ alternate = await this.arrayJoin(requireArrays(state).owner, await this.word(part.alternate, state, operandIO, false, false, hereString), "");
14935
14996
  return alternate;
14936
14997
  });
14937
14998
  value2 = alternate;
14938
- return part.length ? String(Array.from(value2).length) : value2;
14999
+ return part.length ? this.parameterLength(value2) : value2;
14939
15000
  }
14940
- retained = concatShellValues(await this.valueWord(part.alternate, state, io, false, false, hereString), io[valueScope]);
15001
+ retained = concatShellValues(await this.valueWord(part.alternate, state, operandIO, false, false, hereString), io[valueScope]);
14941
15002
  alternate = shellValueText(retained);
14942
15003
  if (operator === "?") throw new ParameterExpansionFailure(`${part.name}: ${alternate || (part.operator.startsWith(":") ? "parameter null or not set" : "parameter not set")}`, io.diagnosticLine ?? part.line);
14943
15004
  if (operator === "=") {
@@ -14950,7 +15011,14 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14950
15011
  retained = "";
14951
15012
  }
14952
15013
  } else this.requireParameter(value2, part.name, state, io, part.line);
14953
- return part.length ? String(Array.from(value2 ?? "").length) : retained ?? "";
15014
+ return part.length ? this.parameterLength(value2 ?? "") : retained ?? "";
15015
+ }
15016
+ async parameterLength(value2) {
15017
+ const limit = this.budget.limits.maxExpansionBytes;
15018
+ const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, limit * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
15019
+ const scanned = await scanString(value2, work);
15020
+ if (scanned.bytes > limit) this.budget.fail("maxExpansionBytes");
15021
+ return String(scanned.count);
14954
15022
  }
14955
15023
  async substring(part, value2, state, io) {
14956
15024
  const owner = arrayStore(state)?.get(part.name) ? requireArrays(state).owner : void 0;
@@ -14960,147 +15028,198 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14960
15028
  if (value2 === void 0) return "";
14961
15029
  const limit = this.budget.limits.maxExpansionBytes;
14962
15030
  if (import_buffer.Buffer.byteLength(value2) > limit) this.budget.fail("maxExpansionBytes");
14963
- const variables = new Proxy(this.arithmeticVariables(state, line), { get: (target, key) => {
14964
- this.signal.throwIfAborted();
14965
- const value3 = Reflect.get(target, key);
14966
- if (typeof value3 === "string" && import_buffer.Buffer.byteLength(value3) > limit) this.budget.fail("maxExpansionBytes");
14967
- return value3;
14968
- } });
14969
- const arithmetic = async (word) => {
14970
- let source = "";
14971
- let bytes3 = 0;
14972
- for (const entry of word.parts) {
15031
+ const scratch = this.budget.values.scope();
15032
+ const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, limit * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
15033
+ try {
15034
+ const variables = new Proxy(this.arithmeticVariables(state, line), { get: (target, key) => {
15035
+ this.signal.throwIfAborted();
15036
+ const value3 = Reflect.get(target, key);
15037
+ if (typeof value3 === "string" && import_buffer.Buffer.byteLength(value3) > limit) this.budget.fail("maxExpansionBytes");
15038
+ return value3;
15039
+ } });
15040
+ const arithmetic = async (word) => {
15041
+ const operandIO = this.parameterOperandIO(word, state, io);
15042
+ let source = "";
15043
+ let bytes3 = 0;
15044
+ let retained;
15045
+ for (const entry of word.parts) {
15046
+ this.signal.throwIfAborted();
15047
+ const text = entry.kind === "text" ? entry.value : await this.part(entry, state, operandIO);
15048
+ bytes3 += import_buffer.Buffer.byteLength(text);
15049
+ if (bytes3 > limit) this.budget.fail("maxExpansionBytes");
15050
+ owner?.reserve({ metadata: 32, payload: bytes3, work: text.length + 4 });
15051
+ const pending = stringCheckpoint(work, text.length + 1);
15052
+ if (pending) await pending;
15053
+ const next = scratch.reserve((source.length + text.length) * 2, 0);
15054
+ source += text;
15055
+ retained?.release();
15056
+ retained = next;
15057
+ }
14973
15058
  this.signal.throwIfAborted();
14974
- const text = entry.kind === "text" ? entry.value : await this.part(entry, state, io);
14975
- bytes3 += import_buffer.Buffer.byteLength(text);
14976
- if (bytes3 > limit) this.budget.fail("maxExpansionBytes");
14977
- owner?.reserve({ metadata: 32, payload: bytes3, work: text.length + 4 });
14978
- source += text;
15059
+ try {
15060
+ return { value: evaluateArithmetic(prepareArithmetic(source, this.budget.parsing), variables, this.budget.parsing), source };
15061
+ } catch (error) {
15062
+ this.rethrowArithmeticControl(error);
15063
+ throw new ExpansionFailure(`${part.name}: ${message(error)}`, line);
15064
+ } finally {
15065
+ retained?.release();
15066
+ }
15067
+ };
15068
+ const offsetExpression = await arithmetic(expression.offset);
15069
+ let bytes2;
15070
+ if (byteLocale(state.variables)) {
15071
+ scratch.reserve(import_buffer.Buffer.byteLength(value2), 0);
15072
+ bytes2 = import_buffer.Buffer.from(value2);
15073
+ }
15074
+ const size = BigInt(bytes2?.byteLength ?? (await scanString(value2, work)).count);
15075
+ const offset = offsetExpression.value < 0n ? size + offsetExpression.value : offsetExpression.value;
15076
+ if (offset < 0n || offset > size) return "";
15077
+ let end = size;
15078
+ if (expression.length) {
15079
+ const length = await arithmetic(expression.length);
15080
+ end = length.value < 0n ? size + length.value : offset + length.value;
15081
+ if (end < offset) throw new ExpansionFailure(`${length.source}: substring expression < 0`, line);
15082
+ if (end > size) end = size;
14979
15083
  }
14980
15084
  this.signal.throwIfAborted();
15085
+ if (!bytes2) {
15086
+ const start = (await scanString(value2, work, 0, value2.length, Number(offset))).end;
15087
+ const finish = (await scanString(value2, work, start, value2.length, Number(end - offset))).end;
15088
+ if (finish > start) scratch.reserve((finish - start) * 2, 0);
15089
+ return value2.slice(start, finish);
15090
+ }
15091
+ scratch.reserve(Number(end - offset) * 2, 0);
14981
15092
  try {
14982
- return { value: evaluateArithmetic(prepareArithmetic(source, this.budget.parsing), variables, this.budget.parsing), source };
14983
- } catch (error) {
14984
- this.rethrowArithmeticControl(error);
14985
- throw new ExpansionFailure(`${part.name}: ${message(error)}`, line);
15093
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes2.subarray(Number(offset), Number(end)));
15094
+ } catch {
15095
+ throw new ExpansionFailure("substring expansion splits a UTF-8 character in a byte locale", line);
14986
15096
  }
14987
- };
14988
- const offsetExpression = await arithmetic(expression.offset);
14989
- owner?.reserve({ metadata: 128 + value2.length * 64, payload: import_buffer.Buffer.byteLength(value2), allocatedSlots: value2.length, work: value2.length + 8 });
14990
- const characters = byteLocale(state.variables) ? void 0 : Array.from(value2);
14991
- const bytes2 = characters ? void 0 : import_buffer.Buffer.from(value2);
14992
- const size = BigInt(characters?.length ?? bytes2.byteLength);
14993
- const offset = offsetExpression.value < 0n ? size + offsetExpression.value : offsetExpression.value;
14994
- if (offset < 0n || offset > size) return "";
14995
- let end = size;
14996
- if (expression.length) {
14997
- const length = await arithmetic(expression.length);
14998
- end = length.value < 0n ? size + length.value : offset + length.value;
14999
- if (end < offset) throw new ExpansionFailure(`${length.source}: substring expression < 0`, line);
15000
- if (end > size) end = size;
15001
- }
15002
- this.signal.throwIfAborted();
15003
- owner?.reserve({ metadata: 96 + value2.length * 32, payload: import_buffer.Buffer.byteLength(value2) * 3, allocatedSlots: value2.length, work: value2.length + 7 });
15004
- if (characters) return characters.slice(Number(offset), Number(end)).join("");
15005
- try {
15006
- return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes2.subarray(Number(offset), Number(end)));
15007
- } catch {
15008
- throw new ExpansionFailure("substring expansion splits a UTF-8 character in a byte locale", line);
15097
+ } finally {
15098
+ scratch.close();
15009
15099
  }
15010
15100
  }
15011
15101
  async parameterPattern(part, text, state, io, hereString) {
15012
- const owner = arrayStore(state)?.get(part.name) ? requireArrays(state).owner : void 0;
15013
15102
  const limit = this.budget.limits.maxExpansionBytes;
15014
15103
  if (import_buffer.Buffer.byteLength(text) > limit) this.budget.fail("maxExpansionBytes");
15015
- const patternFields = await this.word(part.alternate, state, io, false, true, hereString);
15016
- const pattern = owner ? await this.arrayJoin(owner, patternFields, "") : patternFields.join("");
15017
- owner?.reserve({ metadata: 128 + text.length * 64, payload: import_buffer.Buffer.byteLength(text), allocatedSlots: text.length, work: text.length + 8 });
15018
- const characters = Array.from(text);
15019
- const slice = (start, end = characters.length) => {
15020
- owner?.reserve({ metadata: 96 + (end - start) * 32, payload: (end - start) * 4, allocatedSlots: end - start, work: end - start + 7 });
15021
- return characters.slice(start, end).join("");
15022
- };
15023
- const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, limit * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
15024
- const matches = await compilePattern(pattern, work);
15025
- let attempts = 0;
15026
- const match = async (start, end) => {
15027
- work.remaining -= end - start + 1;
15028
- if (work.remaining < 0) work.exhausted();
15029
- if (++attempts % 256 === 0) await yieldTurn(this.signal);
15030
- this.signal.throwIfAborted();
15031
- if (owner) await owner.ledger.checkpoint(this.signal, end - start + 1);
15032
- return matches(slice(start, end));
15033
- };
15034
- const operator = part.operator;
15035
- if (!operator.startsWith("/")) {
15036
- const longest = operator.length === 2;
15037
- for (let length = longest ? characters.length : 0; longest ? length >= 0 : length <= characters.length; length += longest ? -1 : 1) {
15104
+ const scratch = this.budget.values.scope();
15105
+ const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, limit * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes"), allocation: scratch };
15106
+ try {
15107
+ const patternFields = await this.word(part.alternate, state, this.parameterOperandIO(part.alternate, state, io), false, true, hereString);
15108
+ let patternUnits = 0;
15109
+ for (const field of patternFields) {
15110
+ const pending = stringCheckpoint(work, field.length + 1);
15111
+ if (pending) await pending;
15112
+ patternUnits += field.length;
15113
+ }
15114
+ scratch.reserve(patternUnits * 2, 0);
15115
+ const pattern = patternFields.join("");
15116
+ const size = (await scanString(text, work)).count;
15117
+ const matches = await compilePattern(pattern, work);
15118
+ const match = async (start, end, length) => {
15119
+ const pending = stringCheckpoint(work, length + 1);
15120
+ if (pending) await pending;
15121
+ return matches(text, start, end);
15122
+ };
15123
+ const operator = part.operator;
15124
+ if (!operator.startsWith("/")) {
15125
+ const longest = operator.length === 2;
15038
15126
  const prefix2 = operator.startsWith("#");
15039
- if (await match(prefix2 ? 0 : characters.length - length, prefix2 ? length : characters.length)) return prefix2 ? slice(length) : slice(0, characters.length - length);
15040
- }
15041
- return text;
15042
- }
15043
- const replacements = [];
15044
- owner?.reserve({ metadata: 64, work: 3 });
15045
- let replacementBytes = 0;
15046
- for (const [index, entry] of (part.replacement?.parts ?? []).entries()) {
15047
- let value2 = entry.kind === "text" ? entry.value : await this.part(entry, state, io, hereString);
15048
- if (index === 0 && !entry.quoted && /^~(?:\/|$)/u.test(value2)) value2 = (state.variables.HOME ?? "~") + value2.slice(1);
15049
- replacementBytes += import_buffer.Buffer.byteLength(value2);
15050
- if (replacementBytes > limit) this.budget.fail("maxExpansionBytes");
15051
- owner?.reserve({ metadata: 64, payload: import_buffer.Buffer.byteLength(value2), allocatedSlots: 1, work: 5 });
15052
- replacements.push({ value: value2, quoted: entry.quoted });
15053
- }
15054
- if (!pattern && operator !== "/#" && operator !== "/%") return text;
15055
- let result = "";
15056
- let resultBytes = 0;
15057
- const append = (value2) => {
15058
- resultBytes += import_buffer.Buffer.byteLength(value2);
15059
- if (resultBytes > limit) this.budget.fail("maxExpansionBytes");
15060
- owner?.reserve({ metadata: 32, payload: resultBytes, work: value2.length + 4 });
15061
- result += value2;
15062
- };
15063
- let position = 0;
15064
- while (position <= characters.length) {
15065
- let found = false;
15066
- for (let start = position; start <= characters.length; start++) {
15067
- if (operator === "/#" && start !== 0) break;
15068
- for (let end = characters.length; end >= start; end--) {
15069
- if (operator === "/%" && end !== characters.length) break;
15070
- if (!await match(start, end)) continue;
15071
- append(slice(position, start));
15072
- const matched = slice(start, end);
15073
- for (const replacement of replacements) {
15074
- if (replacement.quoted) append(replacement.value);
15075
- else {
15076
- owner?.reserve({ metadata: 64 + replacement.value.length * 64, payload: import_buffer.Buffer.byteLength(replacement.value), allocatedSlots: replacement.value.length, work: replacement.value.length + 4 });
15077
- const pieces = replacement.value.split("&");
15078
- for (const [index, piece] of pieces.entries()) {
15079
- if (index) append(matched);
15080
- append(piece);
15127
+ let boundary = longest === prefix2 ? text.length : 0;
15128
+ for (let length = longest ? size : 0; longest ? length >= 0 : length <= size; length += longest ? -1 : 1) {
15129
+ if (await match(prefix2 ? 0 : boundary, prefix2 ? boundary : text.length, length)) {
15130
+ const start = prefix2 ? boundary : 0;
15131
+ const end = prefix2 ? text.length : boundary;
15132
+ scratch.reserve((end - start) * 2, 0);
15133
+ return text.slice(start, end);
15134
+ }
15135
+ boundary = longest === prefix2 ? previousCodePointOffset(text, boundary) : nextCodePointOffset(text, boundary);
15136
+ }
15137
+ return text;
15138
+ }
15139
+ scratch.reserve(64, 0);
15140
+ const replacements = [];
15141
+ let replacementBytes = 0;
15142
+ const replacementIO = part.replacement ? this.parameterOperandIO(part.replacement, state, io) : io;
15143
+ for (const [index, entry] of (part.replacement?.parts ?? []).entries()) {
15144
+ let value2 = entry.kind === "text" ? entry.value : await this.part(entry, state, replacementIO, hereString);
15145
+ if (index === 0 && !entry.quoted && /^~(?:\/|$)/u.test(value2)) {
15146
+ const home = state.variables.HOME ?? "~";
15147
+ scratch.reserve((home.length + value2.length - 1) * 2, 0);
15148
+ value2 = home + value2.slice(1);
15149
+ }
15150
+ replacementBytes += import_buffer.Buffer.byteLength(value2);
15151
+ if (replacementBytes > limit) this.budget.fail("maxExpansionBytes");
15152
+ const pending = stringCheckpoint(work, value2.length + 1);
15153
+ if (pending) await pending;
15154
+ scratch.reserve(64 + value2.length * 2, 0);
15155
+ replacements.push({ value: value2, quoted: entry.quoted });
15156
+ }
15157
+ if (!pattern && operator !== "/#" && operator !== "/%") return text;
15158
+ let result = "";
15159
+ let resultBytes = 0;
15160
+ let retained;
15161
+ const append = async (value2, start = 0, end = value2.length) => {
15162
+ resultBytes += (await scanString(value2, work, start, end)).bytes;
15163
+ if (resultBytes > limit) this.budget.fail("maxExpansionBytes");
15164
+ if (start === end) return;
15165
+ const fragment = scratch.reserve((end - start) * 2, 0);
15166
+ const next = scratch.reserve((result.length + end - start) * 2, 0);
15167
+ result += value2.slice(start, end);
15168
+ fragment.release();
15169
+ retained?.release();
15170
+ retained = next;
15171
+ };
15172
+ let position = 0;
15173
+ let positionIndex = 0;
15174
+ while (positionIndex <= size) {
15175
+ let found = false;
15176
+ for (let start = position, startIndex = positionIndex; startIndex <= size; startIndex++, start = nextCodePointOffset(text, start)) {
15177
+ if (operator === "/#" && start !== 0) break;
15178
+ for (let end = text.length, endIndex = size; endIndex >= startIndex; endIndex--, end = previousCodePointOffset(text, end)) {
15179
+ if (operator === "/%" && end !== text.length) break;
15180
+ if (!await match(start, end, endIndex - startIndex)) continue;
15181
+ await append(text, position, start);
15182
+ for (const replacement of replacements) {
15183
+ if (replacement.quoted) await append(replacement.value);
15184
+ else {
15185
+ let fragment = 0;
15186
+ for (let cursor = 0; cursor < replacement.value.length; cursor++) {
15187
+ const pending = stringCheckpoint(work);
15188
+ if (pending) await pending;
15189
+ if (replacement.value[cursor] !== "&") continue;
15190
+ await append(replacement.value, fragment, cursor);
15191
+ await append(text, start, end);
15192
+ fragment = cursor + 1;
15193
+ }
15194
+ await append(replacement.value, fragment);
15081
15195
  }
15082
15196
  }
15197
+ position = end;
15198
+ positionIndex = endIndex;
15199
+ found = true;
15200
+ if (operator !== "//" || end === text.length) {
15201
+ await append(text, end);
15202
+ return result;
15203
+ }
15204
+ if (end === start) {
15205
+ position = nextCodePointOffset(text, end);
15206
+ positionIndex++;
15207
+ await append(text, end, position);
15208
+ }
15209
+ break;
15083
15210
  }
15084
- position = end;
15085
- found = true;
15086
- if (operator !== "//" || end === characters.length) {
15087
- append(slice(end));
15088
- return result;
15089
- }
15090
- if (end === start) {
15091
- append(characters[position]);
15092
- position++;
15093
- }
15211
+ if (found) break;
15212
+ }
15213
+ if (!found) {
15214
+ if (positionIndex === 0) return text;
15215
+ await append(text, position);
15094
15216
  break;
15095
15217
  }
15096
- if (found) break;
15097
- }
15098
- if (!found) {
15099
- append(slice(position));
15100
- break;
15101
15218
  }
15219
+ return result;
15220
+ } finally {
15221
+ scratch.close();
15102
15222
  }
15103
- return result;
15104
15223
  }
15105
15224
  async word(word, state, io, split = true, pattern = false, hereString = false, conditionalPattern = false, regexAppend) {
15106
15225
  return (await this.valueWord(word, state, io, split, pattern, hereString, conditionalPattern, regexAppend)).map(shellValueText);
@@ -15170,16 +15289,17 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
15170
15289
  }
15171
15290
  if (boundary) addField();
15172
15291
  };
15173
- const parts = word.parts.map((part) => ({ part, splitText: false }));
15292
+ const parts = word.parts.map((part) => ({ part, splitText: false, io }));
15174
15293
  for (let index = 0; index < parts.length; index++) {
15175
- const { part, splitText } = parts[index];
15294
+ const { part, splitText, io: partIO } = parts[index];
15176
15295
  const quotedPresence = part.quoted && !(arrayOwned && isQuoteMarker(part));
15177
15296
  if (part.kind === "variable" && ["-", "+", ":-", ":+"].includes(part.operator ?? "") && /^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(part.name)) {
15178
15297
  const value2 = this.variable(state, part.name);
15179
15298
  const missing = value2 === void 0 || part.operator.startsWith(":") && value2 === "";
15180
15299
  if (part.operator.endsWith("+") ? !missing : missing) {
15300
+ const operandIO = this.parameterOperandIO(part.alternate, state, partIO);
15181
15301
  scratch?.reserve(part.alternate.parts.length * 32, 0);
15182
- const alternate = part.alternate.parts.map((entry) => ({ part: copyArraySelector(entry, { ...entry, quoted: entry.quoted || part.quoted }), splitText: true }));
15302
+ const alternate = part.alternate.parts.map((entry) => ({ part: copyArraySelector(entry, { ...entry, quoted: entry.quoted || part.quoted }), splitText: true, io: operandIO }));
15183
15303
  if (!alternate.length && part.quoted) append("", false, true);
15184
15304
  parts.splice(index + 1, 0, ...alternate);
15185
15305
  continue;
@@ -15205,7 +15325,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
15205
15325
  }
15206
15326
  if (state.positional.length === 0 && word.parts.every((entry) => entry.kind === "text" && entry.value === "" || entry === part)) fields[0].present = false;
15207
15327
  } else {
15208
- const value2 = part.kind === "text" ? part.byteValue ?? part.value : await this.valuePart(part, state, io, hereString);
15328
+ const value2 = part.kind === "text" ? part.byteValue ?? part.value : await this.valuePart(part, state, partIO, hereString);
15209
15329
  if (part.quoted || !split || state.variables.IFS === "") append(value2, !part.quoted, quotedPresence || !split || shellValueByteLength(value2) > 0);
15210
15330
  else await appendSplit(value2);
15211
15331
  }