@poe-platform/safe-bash 0.1.526 → 0.1.528

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.
@@ -30,7 +30,8 @@ import { cloneGetoptsState, createGetoptsInput, createGetoptsState, GetoptsError
30
30
  import { activateChildCancellation, prepareChildCancellation, selectRuntimeCancellationOutcome, subscribeCancellation, } from "./cancellation.js";
31
31
  import { getArrayAssignment, getArraySelector, copyArraySelector, numericIndex, literalIndex, isQuoteMarker, prefixNameQuoteGroups } from "./arrays/syntax.js";
32
32
  import { ArrayFailure, ArrayOwner, exactSum } from "./arrays/ledger.js";
33
- import { controlNames, IndexedBinding, textToken } from "./arrays/bindings.js";
33
+ import { controlNames, IndexedBinding, textToken, valueToken } from "./arrays/bindings.js";
34
+ import { collectMapfile, mapfileOptions, MapfileUsageError } from "./mapfile.js";
34
35
  import { arrayStore, guestArrays, requireArrays, snapshotState, stateMonitor, trackState } from "./arrays/state.js";
35
36
  import { publishPipelineStatus } from "./pipestatus.js";
36
37
  import { EreProfileLimitError, EreSyntaxError, EreUnsupportedError } from "../commands/regex-execution/ere/errors.js";
@@ -58,7 +59,7 @@ export const defaultLimits = {
58
59
  pipeHighWaterMark: 64 * 1024,
59
60
  };
60
61
  const shellBuiltinNames = new Set([
61
- ":", "true", "false", "pwd", "cd", "set", "shift", "export", "local", "unset", "read",
62
+ ":", "true", "false", "pwd", "cd", "set", "shift", "export", "local", "unset", "read", "mapfile", "readarray",
62
63
  "exit", "return", "break", "continue", "command", "builtin", "type", "readonly", "echo", "printf", "test", "[", ".", "source", "eval", "getopts", "let", "pushd", "dirs", "popd", "shopt",
63
64
  ]);
64
65
  const implementedBuiltins = new Set([...shellBuiltinNames].filter(name => !["echo", "printf", "test", "["].includes(name)));
@@ -1081,6 +1082,7 @@ class InvocationCancellationOwner {
1081
1082
  this.#failures.push(...result.failures);
1082
1083
  }
1083
1084
  }
1085
+ const mapfileCallbackStates = new WeakSet();
1084
1086
  export class Runtime {
1085
1087
  fs;
1086
1088
  commands;
@@ -1409,6 +1411,16 @@ export class Runtime {
1409
1411
  if (error instanceof NounsetFailure || error instanceof Flow || error instanceof ShellLimitError || error instanceof ShellSyntaxError)
1410
1412
  throw error;
1411
1413
  }
1414
+ arithmeticValue(program, state, io) {
1415
+ return evaluatePositionalArithmetic(program, {
1416
+ parseBudget: this.budget.parsing,
1417
+ positional: state.positional, arg0: state.arg0 ?? "virtual-bash", owner: arrayStore(state)?.owner,
1418
+ maximumBytes: this.budget.limits.maxExpansionBytes,
1419
+ checkpoint: () => this.signal.throwIfAborted(),
1420
+ requireParameter: (name, value) => this.requireParameter(value, name, state, io),
1421
+ limit: () => this.budget.fail("maxExpansionBytes"),
1422
+ }, prepared => evaluateArithmetic(prepared, this.arithmeticVariables(state, io.diagnosticLine), this.budget.parsing));
1423
+ }
1412
1424
  arithmeticVariables(state, line) {
1413
1425
  return new Proxy(state.variables, {
1414
1426
  get: (target, key) => {
@@ -1436,7 +1448,7 @@ export class Runtime {
1436
1448
  this.writeVariable(state, name, value, origin);
1437
1449
  return;
1438
1450
  }
1439
- await this.arrayZero(state, name, async () => shellValueText(value));
1451
+ await this.arrayZero(state, name, async () => value);
1440
1452
  }
1441
1453
  async prepareVariable(state, name, saved, scalarLegacy = false) {
1442
1454
  const store = requireArrays(state);
@@ -1619,6 +1631,7 @@ export class Runtime {
1619
1631
  const store = requireArrays(state);
1620
1632
  const operation = ArrayOwner.create(store.owner.ledger, store.owner);
1621
1633
  const holding = store.owner.hold();
1634
+ const valueAllocation = this.budget.values.scope();
1622
1635
  let staged;
1623
1636
  try {
1624
1637
  const watch = await store.watch(name, operation, this.signal);
@@ -1639,8 +1652,10 @@ export class Runtime {
1639
1652
  if (!current)
1640
1653
  throw new ArrayFailure("stale binding");
1641
1654
  staged = await current.copy(this.signal);
1642
- const value = append ? await this.arrayJoin(operation, [current.get(0) ?? "", expanded], "") : expanded;
1643
- const token = await textToken(staged.owner, value, this.signal);
1655
+ const previous = current.values.get(0)?.text.rawValue ?? current.get(0) ?? "";
1656
+ const value = !append ? expanded : typeof previous === "string" && typeof expanded === "string"
1657
+ ? await this.arrayJoin(operation, [previous, expanded], "") : concatShellValues([previous, expanded], valueAllocation);
1658
+ const token = await valueToken(staged.owner, value, this.signal);
1644
1659
  try {
1645
1660
  staged.insert(0, token);
1646
1661
  }
@@ -1673,6 +1688,7 @@ export class Runtime {
1673
1688
  await operation.close();
1674
1689
  }
1675
1690
  finally {
1691
+ valueAllocation.close();
1676
1692
  holding.release();
1677
1693
  }
1678
1694
  }
@@ -1747,7 +1763,7 @@ export class Runtime {
1747
1763
  const preserve = assignment.kind === "element" || assignment.append;
1748
1764
  staged = preserve && current ? await current.copy(this.signal) : IndexedBinding.create(store.owner);
1749
1765
  if (preserve && !current && state.variables[name] !== undefined) {
1750
- const token = await textToken(staged.owner, state.variables[name], this.signal);
1766
+ const token = await valueToken(staged.owner, stateMonitor(state)?.values.get(name, state.variables[name]) ?? state.variables[name], this.signal);
1751
1767
  try {
1752
1768
  staged.insert(0, token);
1753
1769
  }
@@ -1761,7 +1777,7 @@ export class Runtime {
1761
1777
  const insert = async (index, value) => {
1762
1778
  if (index > 2147483647)
1763
1779
  throw new ArrayFailure("index outside 0..2147483647");
1764
- const token = await textToken(staged.owner, value, this.signal);
1780
+ const token = await valueToken(staged.owner, value, this.signal);
1765
1781
  try {
1766
1782
  staged.insert(index, token);
1767
1783
  }
@@ -1771,12 +1787,14 @@ export class Runtime {
1771
1787
  }
1772
1788
  writes++;
1773
1789
  };
1790
+ const join = async (values) => values.every(value => typeof value === "string")
1791
+ ? this.arrayJoin(operation, values, "") : concatShellValues(values, io[valueScope]);
1774
1792
  if (assignment.kind === "element") {
1775
1793
  const index = numericIndex(assignment.index);
1776
- const fields = await this.word(assignment.value, state, io, false);
1777
- let value = await this.arrayJoin(operation, fields, "");
1794
+ const fields = await this.valueWord(assignment.value, state, io, false);
1795
+ let value = await join(fields);
1778
1796
  if (assignment.append)
1779
- value = await this.arrayJoin(operation, [staged.get(index) ?? "", value], "");
1797
+ value = await join([staged.values.get(index)?.text.rawValue ?? staged.get(index) ?? "", value]);
1780
1798
  await insert(index, value);
1781
1799
  }
1782
1800
  else
@@ -1790,17 +1808,17 @@ export class Runtime {
1790
1808
  expandedEntry = true;
1791
1809
  const values = await this.valueWord(expanded, state, io, true, false, false, false, undefined, false);
1792
1810
  for (const value of values) {
1793
- await insert(cursor, shellValueText(value));
1811
+ await insert(cursor, value);
1794
1812
  cursor++;
1795
1813
  }
1796
1814
  }
1797
1815
  if (expandedEntry)
1798
1816
  continue;
1799
1817
  }
1800
- const fields = await this.word(entry.value, state, io, entry.index === undefined);
1818
+ const fields = await this.valueWord(entry.value, state, io, entry.index === undefined);
1801
1819
  if (entry.index) {
1802
1820
  const index = numericIndex(entry.index);
1803
- await insert(index, await this.arrayJoin(operation, fields, ""));
1821
+ await insert(index, await join(fields));
1804
1822
  cursor = index + 1;
1805
1823
  }
1806
1824
  else
@@ -2190,14 +2208,7 @@ export class Runtime {
2190
2208
  }
2191
2209
  if (command.kind === "arithmetic") {
2192
2210
  try {
2193
- return Number(evaluatePositionalArithmetic(command.expression, {
2194
- parseBudget: this.budget.parsing,
2195
- positional: state.positional, arg0: state.arg0 ?? "virtual-bash", owner: arrayStore(state)?.owner,
2196
- maximumBytes: this.budget.limits.maxExpansionBytes,
2197
- checkpoint: () => this.signal.throwIfAborted(),
2198
- requireParameter: (name, value) => this.requireParameter(value, name, state, io),
2199
- limit: () => this.budget.fail("maxExpansionBytes"),
2200
- }, (prepared) => evaluateArithmetic(prepared, this.arithmeticVariables(state, io.diagnosticLine), this.budget.parsing)) === 0n);
2211
+ return Number(this.arithmeticValue(command.expression, state, io) === 0n);
2201
2212
  }
2202
2213
  catch (error) {
2203
2214
  this.rethrowArithmeticControl(error);
@@ -2267,6 +2278,37 @@ export class Runtime {
2267
2278
  break;
2268
2279
  }
2269
2280
  }
2281
+ else if (command.kind === "arithmetic-for") {
2282
+ const evaluate = async (program) => {
2283
+ if (!program)
2284
+ return 1n;
2285
+ try {
2286
+ return this.arithmeticValue(program, state, io);
2287
+ }
2288
+ catch (error) {
2289
+ this.rethrowArithmeticControl(error);
2290
+ await this.diagnostic(io, `((: ${message(error, this.budget.onInternalError)}`);
2291
+ return undefined;
2292
+ }
2293
+ };
2294
+ if (await evaluate(command.expressions[0]) === undefined)
2295
+ return 1;
2296
+ while (true) {
2297
+ this.budget.loop();
2298
+ await yieldTurn(this.signal);
2299
+ const condition = await evaluate(command.expressions[1]);
2300
+ if (condition === undefined)
2301
+ return 1;
2302
+ if (condition === 0n)
2303
+ break;
2304
+ const result = await this.loopBody(command.body, state, io);
2305
+ status = result.status;
2306
+ if (result.stop)
2307
+ break;
2308
+ if (await evaluate(command.expressions[2]) === undefined)
2309
+ return 1;
2310
+ }
2311
+ }
2270
2312
  else if (command.kind === "select") {
2271
2313
  const values = command.words ? await this.valueWords(command.words, state, io) : this.positionalValues(state);
2272
2314
  if (!values.length)
@@ -2784,8 +2826,8 @@ export class Runtime {
2784
2826
  throw new ArrayFailure("indexed binding cannot be a command prefix");
2785
2827
  }
2786
2828
  await this.arrayZero(state, assignment.name, async () => {
2787
- const fields = await this.word(assignment.value, state, io, false);
2788
- return this.arrayJoin(requireArrays(state).owner, fields, "");
2829
+ const fields = await this.valueWord(assignment.value, state, io, false);
2830
+ return fields.every(value => typeof value === "string") ? this.arrayJoin(requireArrays(state).owner, fields, "") : concatShellValues(fields, io[valueScope]);
2789
2831
  }, assignment.append);
2790
2832
  continue;
2791
2833
  }
@@ -3127,6 +3169,9 @@ export class Runtime {
3127
3169
  functionRestoration?.close();
3128
3170
  throw error;
3129
3171
  }
3172
+ const callerLoopDepth = state.loopDepth;
3173
+ if (mapfileCallbackStates.has(state))
3174
+ state.loopDepth = 0;
3130
3175
  try {
3131
3176
  return { exitCode: await this.command(body, state, { ...io, ...context, scriptName: body.sourceName ?? io.scriptName ?? "shell" }) };
3132
3177
  }
@@ -3141,6 +3186,7 @@ export class Runtime {
3141
3186
  savedPositionals.close();
3142
3187
  state.positionalSetVersion = positionalSetVersion;
3143
3188
  state.functionDepth--;
3189
+ state.loopDepth = callerLoopDepth;
3144
3190
  state.depth--;
3145
3191
  state.locals.pop();
3146
3192
  };
@@ -4759,6 +4805,135 @@ export class Runtime {
4759
4805
  allocation.close();
4760
4806
  }
4761
4807
  }
4808
+ async mapfileBuiltin(context, state) {
4809
+ const allocation = this.budget.values.scope();
4810
+ const work = { remaining: this.budget.limits.maxExpansionBytes * 8, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
4811
+ let pinned;
4812
+ let releaseHolding;
4813
+ try {
4814
+ const options = await mapfileOptions(context, work, allocation);
4815
+ if (state.readonlyVariables?.has(options.name))
4816
+ throw new MapfileUsageError(`${options.name}: readonly variable`, 1);
4817
+ if (controlNames.has(options.name) || state.exported.has(options.name))
4818
+ throw new ArrayFailure("control or exported binding cannot be indexed");
4819
+ const store = requireArrays(state);
4820
+ const holding = store.owner.hold();
4821
+ releaseHolding = () => holding.release();
4822
+ if (!options.preserve || !store.get(options.name))
4823
+ await this.arrayAssignment({ kind: "compound", name: options.name, append: options.preserve, entries: [] }, state, context);
4824
+ const entry = store.bindings.get(options.name);
4825
+ allocation.reserve(128 + Buffer.byteLength(options.name) * 2, 0);
4826
+ pinned = entry.binding.retain();
4827
+ const input = context.stdin instanceof ShellInput ? context.stdin : new ShellInput(context.stdin, this.budget, this.signal);
4828
+ let pendingFlow;
4829
+ await collectMapfile(options, input, {
4830
+ allocation: () => this.budget.values.scope(),
4831
+ loop: () => this.budget.loop(),
4832
+ write: async (index, value) => {
4833
+ if (pinned !== entry.binding) {
4834
+ const previous = pinned;
4835
+ pinned = entry.binding.retain();
4836
+ await previous.release();
4837
+ }
4838
+ const operation = ArrayOwner.create(store.owner.ledger, store.owner);
4839
+ let staged;
4840
+ try {
4841
+ const current = pinned;
4842
+ const attached = store.bindings.get(options.name) === entry;
4843
+ const watch = attached ? await store.watch(options.name, operation, this.signal) : undefined;
4844
+ const supersede = attached ? await stateMonitor(state).prepareTypedPublication(options.name, operation, this.signal) : undefined;
4845
+ if (attached)
4846
+ await this.prepareArrayObservers(state, operation);
4847
+ const tickets = operation.reserve({ generation: true, version: true, epoch: true, work: 8 });
4848
+ // Exclude the mapfile pin and the live name's reference from COW.
4849
+ staged = current.references > (attached ? 2 : 1) ? await current.copy(this.signal) : current.retain();
4850
+ const token = await valueToken(staged.owner, value, this.signal);
4851
+ try {
4852
+ this.signal.throwIfAborted();
4853
+ if (watch && !watch.valid())
4854
+ throw new ArrayFailure("stale binding");
4855
+ staged.insert(index, token);
4856
+ }
4857
+ catch (error) {
4858
+ token.release();
4859
+ throw error;
4860
+ }
4861
+ if (attached) {
4862
+ let released;
4863
+ stateMonitor(state).publish(tickets, options.name, () => {
4864
+ supersede();
4865
+ released = store.publish(options.name, staged, tickets);
4866
+ });
4867
+ staged = undefined;
4868
+ await released;
4869
+ }
4870
+ else {
4871
+ const previous = pinned;
4872
+ pinned = staged;
4873
+ entry.binding = staged;
4874
+ staged = undefined;
4875
+ await previous.release();
4876
+ }
4877
+ }
4878
+ finally {
4879
+ try {
4880
+ await staged?.release();
4881
+ }
4882
+ finally {
4883
+ await operation.close();
4884
+ }
4885
+ }
4886
+ },
4887
+ callback: async (source, index, value) => {
4888
+ if (pendingFlow)
4889
+ return;
4890
+ const callbackAllocation = this.budget.values.scope();
4891
+ const nested = mapfileCallbackStates.has(state);
4892
+ try {
4893
+ const quoted = shellValueText(await transformParameter(value, "Q", { maximumBytes: this.budget.limits.maxExpansionBytes, byteLocale: byteLocale(state.variables), work, allocation: callbackAllocation }));
4894
+ if (Buffer.byteLength(source) + Buffer.byteLength(quoted) + String(index).length + 2 > this.budget.limits.maxExpansionBytes)
4895
+ this.budget.fail("maxExpansionBytes");
4896
+ mapfileCallbackStates.add(state);
4897
+ await this.evalBuiltin({ ...context, args: [source, String(index), quoted] }, state, context, false);
4898
+ }
4899
+ catch (error) {
4900
+ if (error instanceof Flow && (error.kind === "break" || error.kind === "continue"))
4901
+ pendingFlow = error;
4902
+ else
4903
+ throw error;
4904
+ }
4905
+ finally {
4906
+ if (!nested)
4907
+ mapfileCallbackStates.delete(state);
4908
+ callbackAllocation.close();
4909
+ }
4910
+ },
4911
+ });
4912
+ if (pendingFlow)
4913
+ throw pendingFlow;
4914
+ return 0;
4915
+ }
4916
+ catch (error) {
4917
+ this.signal.throwIfAborted();
4918
+ if (!(error instanceof MapfileUsageError))
4919
+ throw error;
4920
+ await this.diagnostic(context, `${context.command}: ${error.message}`);
4921
+ return error.status;
4922
+ }
4923
+ finally {
4924
+ try {
4925
+ await pinned?.release();
4926
+ }
4927
+ finally {
4928
+ try {
4929
+ releaseHolding?.();
4930
+ }
4931
+ finally {
4932
+ allocation.close();
4933
+ }
4934
+ }
4935
+ }
4936
+ }
4762
4937
  async builtin(context, state, assignments, diagnose, suppressSpecial = false) {
4763
4938
  const { command, args, stdout, stderr } = context;
4764
4939
  if (command === ":" || command === "true")
@@ -4769,6 +4944,8 @@ export class Runtime {
4769
4944
  return this.shoptBuiltin(context, state);
4770
4945
  if (command === "let")
4771
4946
  return this.letBuiltin(context, state);
4947
+ if (command === "mapfile" || command === "readarray")
4948
+ return this.mapfileBuiltin(context, state);
4772
4949
  if (command === "getopts")
4773
4950
  return this.getoptsBuiltin(context, state);
4774
4951
  if (command === "pushd" || command === "dirs" || command === "popd")
@@ -5389,7 +5566,7 @@ export class Runtime {
5389
5566
  if (part.kind === "variable" && part.transform) {
5390
5567
  const selector = getArraySelector(part);
5391
5568
  if (selector?.kind === "members" || part.name === "@" || part.name === "*") {
5392
- const members = selector ? await this.arrayMembers(part.name, state) : this.positionalValues(state);
5569
+ const members = selector ? await this.arrayMembers(part.name, state, io) : this.positionalValues(state);
5393
5570
  const ifs = state.variables.IFS ?? " ";
5394
5571
  const separator = ifs.length ? String.fromCodePoint(ifs.codePointAt(0)) : "";
5395
5572
  const fragments = [];
@@ -5439,6 +5616,8 @@ export class Runtime {
5439
5616
  const token = index === undefined || selector?.kind === "members" ? undefined : binding?.values.get(index)?.text;
5440
5617
  token?.retain();
5441
5618
  try {
5619
+ if (token?.rawValue && part.kind === "variable" && !part.length && !part.operator && !part.substring)
5620
+ return shellValueFromBytes(shellValueBytes(token.rawValue, io[valueScope]), io[valueScope]);
5442
5621
  const value = await this.partValue(part, state, io, hereString);
5443
5622
  if (binding)
5444
5623
  await textToken(requireArrays(state).owner, shellValueText(value), this.signal);
@@ -5568,8 +5747,12 @@ export class Runtime {
5568
5747
  }
5569
5748
  if (part.length)
5570
5749
  return String(binding?.values.size ?? (state.variables[part.name] === undefined ? 0 : 1));
5571
- const values = await this.arrayMembers(part.name, state);
5572
- return this.arrayJoin(store.owner, values, Array.from(state.variables.IFS ?? " ")[0] ?? "");
5750
+ const values = await this.arrayMembers(part.name, state, io);
5751
+ const separator = Array.from(state.variables.IFS ?? " ")[0] ?? "";
5752
+ if (values.every(value => typeof value === "string"))
5753
+ return this.arrayJoin(store.owner, values, separator);
5754
+ io[valueScope]?.reserve(values.length * 32 + 64, 0);
5755
+ return concatShellValues(values.flatMap((value, index) => index ? [separator, value] : [value]), io[valueScope]);
5573
5756
  }
5574
5757
  let value = part.name === "?" ? String(state.status)
5575
5758
  : part.name === "-" ? `${state.errexit ? "e" : ""}${state.nounset ? "u" : ""}${state.braceexpand !== false ? "B" : ""}`
@@ -6046,7 +6229,7 @@ export class Runtime {
6046
6229
  }
6047
6230
  }
6048
6231
  else if (part.kind === "variable" && split && (selector?.kind === "members" && !part.length && (!part.quoted || selector.separator === "@") || part.transform && part.name === "@")) {
6049
- const members = selector?.kind === "members" ? await this.arrayMembers(part.name, state) : this.positionalValues(state);
6232
+ const members = selector?.kind === "members" ? await this.arrayMembers(part.name, state, io) : this.positionalValues(state);
6050
6233
  for (let position = 0; position < members.length; position++) {
6051
6234
  if (position > 0)
6052
6235
  addField();
@@ -6221,7 +6404,7 @@ export class Runtime {
6221
6404
  allocation.hold(value);
6222
6405
  return createCommandArguments(values, allocation);
6223
6406
  }
6224
- async arrayMembers(name, state) {
6407
+ async arrayMembers(name, state, io) {
6225
6408
  const store = requireArrays(state);
6226
6409
  const holding = store.owner.hold();
6227
6410
  try {
@@ -6241,8 +6424,10 @@ export class Runtime {
6241
6424
  const values = [];
6242
6425
  for (const index of indices) {
6243
6426
  store.owner.reserve({ metadata: 32, allocatedSlots: 1, work: 4 });
6244
- const value = binding.get(index);
6245
- await textToken(store.owner, value, this.signal);
6427
+ const token = binding.values.get(index).text;
6428
+ const value = token.rawValue === undefined ? token.value : shellValueFromBytes(shellValueBytes(token.rawValue, io[valueScope]), io[valueScope]);
6429
+ if (typeof value === "string")
6430
+ await textToken(store.owner, value, this.signal);
6246
6431
  values.push(value);
6247
6432
  await store.owner.ledger.checkpoint(this.signal);
6248
6433
  }