@poe-platform/safe-bash 0.1.521 → 0.1.523
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.
- package/dist/safe-bash/core.browser.js +342 -23
- package/dist/safe-bash/core.browser.js.map +3 -3
- package/dist/safe-bash/shell/arrays/bindings.d.ts +1 -0
- package/dist/safe-bash/shell/arrays/bindings.d.ts.map +1 -1
- package/dist/safe-bash/shell/arrays/bindings.js +3 -0
- package/dist/safe-bash/shell/arrays/bindings.js.map +1 -1
- package/dist/safe-bash/shell/arrays/syntax.d.ts +1 -0
- package/dist/safe-bash/shell/arrays/syntax.d.ts.map +1 -1
- package/dist/safe-bash/shell/arrays/syntax.js +4 -0
- package/dist/safe-bash/shell/arrays/syntax.js.map +1 -1
- package/dist/safe-bash/shell/parser.d.ts +1 -0
- package/dist/safe-bash/shell/parser.d.ts.map +1 -1
- package/dist/safe-bash/shell/parser.js +31 -4
- package/dist/safe-bash/shell/parser.js.map +1 -1
- package/dist/safe-bash/shell/runtime.d.ts +6 -0
- package/dist/safe-bash/shell/runtime.d.ts.map +1 -1
- package/dist/safe-bash/shell/runtime.js +381 -20
- package/dist/safe-bash/shell/runtime.js.map +1 -1
- package/dist/safe-bash/shell/shell.d.ts.map +1 -1
- package/dist/safe-bash/shell/shell.js +1 -0
- package/dist/safe-bash/shell/shell.js.map +1 -1
- package/package.json +2 -2
|
@@ -27,7 +27,7 @@ import { formatPrintf, printfCommand } from "../commands/basic.js";
|
|
|
27
27
|
import { pathOf, UsageError } from "../commands/internal.js";
|
|
28
28
|
import { cloneGetoptsState, createGetoptsInput, createGetoptsState, GetoptsError, getoptsInputAllocationSize, scanGetopts, withGetoptsIndex } from "./getopts.js";
|
|
29
29
|
import { activateChildCancellation, prepareChildCancellation, selectRuntimeCancellationOutcome, subscribeCancellation, } from "./cancellation.js";
|
|
30
|
-
import { getArrayAssignment, getArraySelector, copyArraySelector, numericIndex, literalIndex, isQuoteMarker } from "./arrays/syntax.js";
|
|
30
|
+
import { getArrayAssignment, getArraySelector, copyArraySelector, numericIndex, literalIndex, isQuoteMarker, prefixNameQuoteGroups } from "./arrays/syntax.js";
|
|
31
31
|
import { ArrayFailure, ArrayOwner, exactSum } from "./arrays/ledger.js";
|
|
32
32
|
import { controlNames, IndexedBinding, textToken } from "./arrays/bindings.js";
|
|
33
33
|
import { arrayStore, guestArrays, requireArrays, snapshotState, stateMonitor, trackState } from "./arrays/state.js";
|
|
@@ -76,6 +76,40 @@ export function resolveLimits(...limits) {
|
|
|
76
76
|
return result;
|
|
77
77
|
}
|
|
78
78
|
const budgetedSinks = new WeakMap();
|
|
79
|
+
async function sortExpansionStrings(values, work) {
|
|
80
|
+
const compare = async (left, right) => {
|
|
81
|
+
for (let index = 0; index < Math.min(left.length, right.length); index++) {
|
|
82
|
+
const pending = stringCheckpoint(work);
|
|
83
|
+
if (pending)
|
|
84
|
+
await pending;
|
|
85
|
+
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
86
|
+
if (difference)
|
|
87
|
+
return difference;
|
|
88
|
+
}
|
|
89
|
+
return left.length - right.length;
|
|
90
|
+
};
|
|
91
|
+
const sift = async (root, end) => {
|
|
92
|
+
while (root * 2 + 1 < end) {
|
|
93
|
+
let child = root * 2 + 1;
|
|
94
|
+
if (child + 1 < end && await compare(values[child], values[child + 1]) < 0)
|
|
95
|
+
child++;
|
|
96
|
+
if (await compare(values[root], values[child]) >= 0)
|
|
97
|
+
return;
|
|
98
|
+
const saved = values[root];
|
|
99
|
+
values[root] = values[child];
|
|
100
|
+
values[child] = saved;
|
|
101
|
+
root = child;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
for (let index = Math.floor(values.length / 2) - 1; index >= 0; index--)
|
|
105
|
+
await sift(index, values.length);
|
|
106
|
+
for (let end = values.length - 1; end > 0; end--) {
|
|
107
|
+
const saved = values[0];
|
|
108
|
+
values[0] = values[end];
|
|
109
|
+
values[end] = saved;
|
|
110
|
+
await sift(0, end);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
79
113
|
export class Budget {
|
|
80
114
|
limits;
|
|
81
115
|
onInternalError;
|
|
@@ -87,6 +121,8 @@ export class Budget {
|
|
|
87
121
|
iterations = 0;
|
|
88
122
|
bytes = 0;
|
|
89
123
|
sourceBytes = 0;
|
|
124
|
+
globstarEntries = 0;
|
|
125
|
+
globstarStates = 0;
|
|
90
126
|
controller = new AbortController();
|
|
91
127
|
signal;
|
|
92
128
|
#wallClockTimer;
|
|
@@ -259,7 +295,7 @@ const typedSavedVariables = new WeakMap();
|
|
|
259
295
|
const valueScope = Symbol("shell value allocation scope");
|
|
260
296
|
const invokedValues = new WeakMap();
|
|
261
297
|
function isolateIO(io) {
|
|
262
|
-
return { ...io, ...(io.descriptors ? { descriptors: new Map([...io.descriptors].map(([number, descriptor]) => [number, { ...descriptor }])) } : {}) };
|
|
298
|
+
return { ...io, nameExpansionContext: undefined, ...(io.descriptors ? { descriptors: new Map([...io.descriptors].map(([number, descriptor]) => [number, { ...descriptor }])) } : {}) };
|
|
263
299
|
}
|
|
264
300
|
function activeIO(io) {
|
|
265
301
|
const input = io.descriptors?.get(0);
|
|
@@ -1534,6 +1570,7 @@ export class Runtime {
|
|
|
1534
1570
|
const current = store.get(name);
|
|
1535
1571
|
if (index !== undefined && current) {
|
|
1536
1572
|
staged = index === "members" ? IndexedBinding.create(store.owner) : await current.copy(this.signal);
|
|
1573
|
+
staged.assigned = current.assigned;
|
|
1537
1574
|
let maximum = -1;
|
|
1538
1575
|
for (const key of staged.values.keys()) {
|
|
1539
1576
|
operation.reserve({ work: 2 }).release();
|
|
@@ -1775,7 +1812,8 @@ export class Runtime {
|
|
|
1775
1812
|
throw new ArrayFailure("readonly binding");
|
|
1776
1813
|
if (!watch.valid())
|
|
1777
1814
|
throw new ArrayFailure("stale binding");
|
|
1778
|
-
|
|
1815
|
+
staged.assigned = true;
|
|
1816
|
+
if (!(assignment.kind === "compound" && assignment.append && writes === 0 && current?.assigned)) {
|
|
1779
1817
|
let released;
|
|
1780
1818
|
stateMonitor(state).publish(tickets, name, () => {
|
|
1781
1819
|
supersede();
|
|
@@ -2092,8 +2130,8 @@ export class Runtime {
|
|
|
2092
2130
|
fs: this.fs, cwd: state.cwd, signal: this.signal,
|
|
2093
2131
|
locale: state.variables.LC_ALL || state.variables.LC_COLLATE || state.variables.LANG || "C",
|
|
2094
2132
|
work: { remaining: this.budget.limits.maxExpansionBytes, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes"), allocation },
|
|
2095
|
-
expand: async (word, pattern = false) => (await this.word(word, state, io, false, pattern, false, pattern)).join(""),
|
|
2096
|
-
regex: (subject, pattern) => this.ere(subject, pattern, state, io),
|
|
2133
|
+
expand: async (word, pattern = false) => (await this.word(word, state, { ...io, nameExpansionContext: "conditional" }, false, pattern, false, pattern)).join(""),
|
|
2134
|
+
regex: (subject, pattern) => this.ere(subject, pattern, state, { ...io, nameExpansionContext: "conditional" }),
|
|
2097
2135
|
option: name => name === "braceexpand" ? state.braceexpand !== false : name === "errexit" ? !!state.errexit : name === "nounset" ? !!state.nounset : name === "pipefail" ? state.pipefail : false,
|
|
2098
2136
|
present: name => {
|
|
2099
2137
|
const match = /^([a-zA-Z_][a-zA-Z_0-9]*)(?:\[(0|[1-9][0-9]*|[@*])\])?$/u.exec(name);
|
|
@@ -2356,7 +2394,7 @@ export class Runtime {
|
|
|
2356
2394
|
await writeDiagnostic(io.stderr, `shell: warning: ${warning}\n`);
|
|
2357
2395
|
if (++words % 128 === 0)
|
|
2358
2396
|
await yieldTurn(this.signal);
|
|
2359
|
-
const part = (await this.word(word, state, io, false)).join("");
|
|
2397
|
+
const part = (await this.word(word, state, { ...io, nameExpansionContext: "document" }, false)).join("");
|
|
2360
2398
|
size += Buffer.byteLength(part);
|
|
2361
2399
|
if (size > this.budget.limits.maxExpansionBytes)
|
|
2362
2400
|
this.budget.fail("maxExpansionBytes");
|
|
@@ -3319,6 +3357,7 @@ export class Runtime {
|
|
|
3319
3357
|
cwd: state.cwd, variables, exported, functions: new Map(), getopts: { cursor: createGetoptsState(), integer: true },
|
|
3320
3358
|
directoryStack: { entries: [], bytes: 0 },
|
|
3321
3359
|
dotglob: false,
|
|
3360
|
+
globstar: false,
|
|
3322
3361
|
positional: [...args], arg0, profile: context.command === "sh" ? "sh" : "bash", status: 0, substitutionStatus: 0, depth: state.depth + 1,
|
|
3323
3362
|
loopDepth: 0, functionDepth: 0, locals: [], pipefail: false, isolated: true,
|
|
3324
3363
|
errexit: false,
|
|
@@ -4476,7 +4515,7 @@ export class Runtime {
|
|
|
4476
4515
|
unset = true;
|
|
4477
4516
|
else {
|
|
4478
4517
|
await this.diagnostic(context, `shopt: ${option.startsWith("--") ? option : `-${flag}`}: unsupported option`);
|
|
4479
|
-
await writeDiagnostic(context.stderr, "shopt: usage: shopt [-pqsu] [--] [dotglob ...]\n");
|
|
4518
|
+
await writeDiagnostic(context.stderr, "shopt: usage: shopt [-pqsu] [--] [dotglob globstar ...]\n");
|
|
4480
4519
|
return 2;
|
|
4481
4520
|
}
|
|
4482
4521
|
}
|
|
@@ -4485,28 +4524,29 @@ export class Runtime {
|
|
|
4485
4524
|
await this.diagnostic(context, "shopt: cannot set and unset shell options simultaneously");
|
|
4486
4525
|
return 1;
|
|
4487
4526
|
}
|
|
4488
|
-
const emit = async () => {
|
|
4527
|
+
const emit = async (name) => {
|
|
4489
4528
|
if (!quiet)
|
|
4490
|
-
await writeText(context.stdout, print ? `shopt -${state
|
|
4529
|
+
await writeText(context.stdout, print ? `shopt -${state[name] ? "s" : "u"} ${name}\n` : `${name.padEnd(20)}\t${state[name] ? "on" : "off"}\n`);
|
|
4491
4530
|
};
|
|
4492
4531
|
if (index === context.args.length) {
|
|
4493
|
-
|
|
4494
|
-
|
|
4532
|
+
for (const name of ["dotglob", "globstar"])
|
|
4533
|
+
if ((!set || state[name]) && (!unset || !state[name]))
|
|
4534
|
+
await emit(name);
|
|
4495
4535
|
return 0;
|
|
4496
4536
|
}
|
|
4497
4537
|
let status = 0;
|
|
4498
4538
|
for (; index < context.args.length; index++) {
|
|
4499
4539
|
this.signal.throwIfAborted();
|
|
4500
4540
|
const name = context.args[index];
|
|
4501
|
-
if (name !== "dotglob") {
|
|
4502
|
-
await this.diagnostic(context, `shopt: ${name}: unsupported shell option name (
|
|
4541
|
+
if (name !== "dotglob" && name !== "globstar") {
|
|
4542
|
+
await this.diagnostic(context, `shopt: ${name}: unsupported shell option name (supported: dotglob, globstar)`);
|
|
4503
4543
|
status = 1;
|
|
4504
4544
|
}
|
|
4505
4545
|
else if (set || unset)
|
|
4506
|
-
state
|
|
4546
|
+
state[name] = set;
|
|
4507
4547
|
else {
|
|
4508
|
-
await emit();
|
|
4509
|
-
if (!state
|
|
4548
|
+
await emit(name);
|
|
4549
|
+
if (!state[name])
|
|
4510
4550
|
status = 1;
|
|
4511
4551
|
}
|
|
4512
4552
|
}
|
|
@@ -5204,7 +5244,83 @@ export class Runtime {
|
|
|
5204
5244
|
async part(part, state, io, hereString = false) {
|
|
5205
5245
|
return shellValueText(await this.valuePart(part, state, io, hereString));
|
|
5206
5246
|
}
|
|
5247
|
+
async *prefixNames(prefix, state) {
|
|
5248
|
+
const allocation = this.budget.values.scope();
|
|
5249
|
+
const store = arrayStore(state);
|
|
5250
|
+
const holding = store?.owner.hold();
|
|
5251
|
+
const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, this.budget.limits.maxExpansionBytes * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
|
|
5252
|
+
try {
|
|
5253
|
+
allocation.reserveBytes(128);
|
|
5254
|
+
const names = [];
|
|
5255
|
+
const seen = new Set();
|
|
5256
|
+
let bytes = 0;
|
|
5257
|
+
const consider = async (name, assigned) => {
|
|
5258
|
+
const scanned = await scanString(name, work);
|
|
5259
|
+
const pending = stringCheckpoint(work, name.length + 1);
|
|
5260
|
+
if (pending)
|
|
5261
|
+
await pending;
|
|
5262
|
+
if (!assigned || !name.startsWith(prefix) || seen.has(name))
|
|
5263
|
+
return;
|
|
5264
|
+
for (let index = 0; index < name.length; index++) {
|
|
5265
|
+
const pending = stringCheckpoint(work);
|
|
5266
|
+
if (pending)
|
|
5267
|
+
await pending;
|
|
5268
|
+
const code = name.charCodeAt(index);
|
|
5269
|
+
if (!(code === 95 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || index > 0 && code >= 48 && code <= 57))
|
|
5270
|
+
return;
|
|
5271
|
+
}
|
|
5272
|
+
if (names.length >= this.budget.limits.maxExpansionFields)
|
|
5273
|
+
this.budget.fail("maxExpansionFields");
|
|
5274
|
+
if (scanned.bytes > this.budget.limits.maxExpansionBytes - bytes)
|
|
5275
|
+
this.budget.fail("maxExpansionBytes");
|
|
5276
|
+
bytes += scanned.bytes;
|
|
5277
|
+
allocation.reserveBytes(name.length * 2 + 64);
|
|
5278
|
+
seen.add(name);
|
|
5279
|
+
names.push(name);
|
|
5280
|
+
};
|
|
5281
|
+
for (const name in state.variables)
|
|
5282
|
+
if (Object.hasOwn(state.variables, name))
|
|
5283
|
+
await consider(name, state.variables[name] !== undefined);
|
|
5284
|
+
if (store)
|
|
5285
|
+
for (const [name, entry] of store.bindings)
|
|
5286
|
+
await consider(name, entry.binding.assigned);
|
|
5287
|
+
// Bash lists initialized names from dynamically enclosing local scopes,
|
|
5288
|
+
// even when the current local value shadows them with an unset value.
|
|
5289
|
+
for (const frame of state.locals)
|
|
5290
|
+
for (const [name, saved] of frame) {
|
|
5291
|
+
await consider(name, saved.value !== undefined || typedSavedVariables.get(saved)?.binding?.assigned === true);
|
|
5292
|
+
}
|
|
5293
|
+
await sortExpansionStrings(names, work);
|
|
5294
|
+
for (const name of names) {
|
|
5295
|
+
const pending = stringCheckpoint(work);
|
|
5296
|
+
if (pending)
|
|
5297
|
+
await pending;
|
|
5298
|
+
yield name;
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
finally {
|
|
5302
|
+
allocation.close();
|
|
5303
|
+
holding?.release();
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5207
5306
|
async valuePart(part, state, io, hereString = false) {
|
|
5307
|
+
if (part.kind === "variable" && part.prefixNames) {
|
|
5308
|
+
const ifs = state.variables.IFS ?? " ";
|
|
5309
|
+
const separator = io.nameExpansionContext === "document" || io.nameExpansionContext === "conditional" && part.prefixNames === "@"
|
|
5310
|
+
? " " : ifs.length ? String.fromCodePoint(ifs.codePointAt(0)) : "";
|
|
5311
|
+
const fragments = [];
|
|
5312
|
+
let bytes = 0;
|
|
5313
|
+
for await (const name of this.prefixNames(part.name, state)) {
|
|
5314
|
+
const join = fragments.length ? separator : "";
|
|
5315
|
+
bytes += Buffer.byteLength(name) + Buffer.byteLength(join);
|
|
5316
|
+
if (bytes > this.budget.limits.maxExpansionBytes)
|
|
5317
|
+
this.budget.fail("maxExpansionBytes");
|
|
5318
|
+
io[valueScope]?.reserve((name.length + join.length) * 2 + 32, 0);
|
|
5319
|
+
fragments.push(join, name);
|
|
5320
|
+
}
|
|
5321
|
+
io[valueScope]?.reserve(bytes * 2, 0);
|
|
5322
|
+
return fragments.join("");
|
|
5323
|
+
}
|
|
5208
5324
|
const binding = part.kind === "variable" ? arrayStore(state)?.get(part.name) : undefined;
|
|
5209
5325
|
const holding = binding ? requireArrays(state).owner.hold() : undefined;
|
|
5210
5326
|
const selector = getArraySelector(part);
|
|
@@ -5652,7 +5768,8 @@ export class Runtime {
|
|
|
5652
5768
|
}
|
|
5653
5769
|
return fields;
|
|
5654
5770
|
}
|
|
5655
|
-
const arrayOwned = word.parts.some(part => part.kind === "variable" && (getArraySelector(part) !== undefined || arrayStore(state)?.get(part.name) !== undefined));
|
|
5771
|
+
const arrayOwned = word.parts.some(part => part.kind === "variable" && !part.prefixNames && (getArraySelector(part) !== undefined || arrayStore(state)?.get(part.name) !== undefined));
|
|
5772
|
+
const prefixOwned = word.parts.some(part => part.kind === "variable" && part.prefixNames === "@");
|
|
5656
5773
|
const owner = arrayOwned ? requireArrays(state).owner : undefined;
|
|
5657
5774
|
const holding = owner?.hold();
|
|
5658
5775
|
const scratch = !owner && split && state.variables.IFS !== "" && word.parts.some(part => !part.quoted && part.kind !== "text")
|
|
@@ -5663,12 +5780,14 @@ export class Runtime {
|
|
|
5663
5780
|
owner?.reserve({ metadata: 128 + word.parts.length * 32, allocatedSlots: word.parts.length + 1, work: word.parts.length + 5 });
|
|
5664
5781
|
scratch?.reserve(word.parts.length * 32, 0);
|
|
5665
5782
|
const fields = [];
|
|
5783
|
+
let emptyNameGroups;
|
|
5784
|
+
let quoteGroup;
|
|
5666
5785
|
const addField = () => {
|
|
5667
5786
|
if (fields.length >= this.budget.limits.maxExpansionFields)
|
|
5668
5787
|
this.budget.fail("maxExpansionFields");
|
|
5669
5788
|
scratch?.reserve(32, 0);
|
|
5670
5789
|
owner?.reserve({ metadata: 32, allocatedSlots: 1, work: 3 });
|
|
5671
|
-
fields.push({ fragments: [], bytes: false, patterns: undefined, present: false });
|
|
5790
|
+
fields.push({ fragments: [], bytes: false, patterns: undefined, present: false, independentPresence: false });
|
|
5672
5791
|
};
|
|
5673
5792
|
addField();
|
|
5674
5793
|
let expansionBytes = 0;
|
|
@@ -5708,6 +5827,15 @@ export class Runtime {
|
|
|
5708
5827
|
regexAppend?.(text, !glob);
|
|
5709
5828
|
field.fragments.push(value);
|
|
5710
5829
|
field.present ||= present;
|
|
5830
|
+
if (present) {
|
|
5831
|
+
if (size > 0 || !quoteGroup)
|
|
5832
|
+
field.independentPresence = true;
|
|
5833
|
+
else {
|
|
5834
|
+
io[valueScope]?.reserve(32, 0);
|
|
5835
|
+
field.quoteGroups ??= [];
|
|
5836
|
+
field.quoteGroups.push(quoteGroup);
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5711
5839
|
};
|
|
5712
5840
|
const appendSplit = async (value) => {
|
|
5713
5841
|
this.budget.cpuCheckpoint();
|
|
@@ -5764,7 +5892,8 @@ export class Runtime {
|
|
|
5764
5892
|
const parts = word.parts.map((part) => ({ part, splitText: false, io }));
|
|
5765
5893
|
for (let index = 0; index < parts.length; index++) {
|
|
5766
5894
|
const { part, splitText, io: partIO } = parts[index];
|
|
5767
|
-
|
|
5895
|
+
quoteGroup = prefixNameQuoteGroups.get(part);
|
|
5896
|
+
const quotedPresence = part.quoted && !((arrayOwned || prefixOwned) && isQuoteMarker(part));
|
|
5768
5897
|
if (part.kind === "variable" && ["-", "+", ":-", ":+"].includes(part.operator ?? "") && /^[a-zA-Z_][a-zA-Z_0-9]*$/u.test(part.name)) {
|
|
5769
5898
|
const value = this.variable(state, part.name);
|
|
5770
5899
|
const missing = value === undefined || (part.operator.startsWith(":") && value === "");
|
|
@@ -5779,7 +5908,20 @@ export class Runtime {
|
|
|
5779
5908
|
}
|
|
5780
5909
|
}
|
|
5781
5910
|
const selector = getArraySelector(part);
|
|
5782
|
-
if (part.kind === "variable" &&
|
|
5911
|
+
if (part.kind === "variable" && part.prefixNames === "@" && split && (part.quoted || state.variables.IFS === "")) {
|
|
5912
|
+
let position = 0;
|
|
5913
|
+
for await (const name of this.prefixNames(part.name, state)) {
|
|
5914
|
+
if (position++)
|
|
5915
|
+
addField();
|
|
5916
|
+
append(name, !part.quoted, true);
|
|
5917
|
+
}
|
|
5918
|
+
if (position === 0 && quoteGroup) {
|
|
5919
|
+
io[valueScope]?.reserve(32, 0);
|
|
5920
|
+
emptyNameGroups ??= new Set();
|
|
5921
|
+
emptyNameGroups.add(quoteGroup);
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
else if (part.kind === "variable" && selector?.kind === "members" && !part.length && split && (!part.quoted || selector.separator === "@")) {
|
|
5783
5925
|
const members = await this.arrayMembers(part.name, state);
|
|
5784
5926
|
for (let position = 0; position < members.length; position++) {
|
|
5785
5927
|
if (position > 0)
|
|
@@ -5823,6 +5965,21 @@ export class Runtime {
|
|
|
5823
5965
|
for (const field of fields) {
|
|
5824
5966
|
if (!field.present && split)
|
|
5825
5967
|
continue;
|
|
5968
|
+
if (split && !field.independentPresence && field.quoteGroups) {
|
|
5969
|
+
const work = { remaining: this.budget.limits.maxExpansionBytes, signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
|
|
5970
|
+
let empty = true;
|
|
5971
|
+
for (const group of field.quoteGroups) {
|
|
5972
|
+
const pending = stringCheckpoint(work);
|
|
5973
|
+
if (pending)
|
|
5974
|
+
await pending;
|
|
5975
|
+
if (!emptyNameGroups?.has(group)) {
|
|
5976
|
+
empty = false;
|
|
5977
|
+
break;
|
|
5978
|
+
}
|
|
5979
|
+
}
|
|
5980
|
+
if (empty)
|
|
5981
|
+
continue;
|
|
5982
|
+
}
|
|
5826
5983
|
if (scratch && field.fragments.length > 1 && !field.bytes)
|
|
5827
5984
|
scratch.reserve(field.fragments.reduce((bytes, value) => bytes + shellValueText(value).length * 2, 0), 0);
|
|
5828
5985
|
const assembled = concatShellValues(field.fragments, io[valueScope]);
|
|
@@ -5969,9 +6126,213 @@ export class Runtime {
|
|
|
5969
6126
|
holding.release();
|
|
5970
6127
|
}
|
|
5971
6128
|
}
|
|
6129
|
+
async recursiveGlob(value, pattern, state) {
|
|
6130
|
+
// Entry/state limits are invocation-wide, including hidden and nonmatching
|
|
6131
|
+
// entries. They are separate from the number of emitted expansion fields.
|
|
6132
|
+
const failWalk = (message) => {
|
|
6133
|
+
const error = new FsError("EFBIG", { syscall: "glob", message });
|
|
6134
|
+
this.budget.controller.abort(error);
|
|
6135
|
+
throw error;
|
|
6136
|
+
};
|
|
6137
|
+
const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, this.budget.limits.maxExpansionBytes * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
|
|
6138
|
+
const scratch = this.budget.values.scope();
|
|
6139
|
+
work.allocation = scratch;
|
|
6140
|
+
try {
|
|
6141
|
+
const segments = [];
|
|
6142
|
+
let start = 0;
|
|
6143
|
+
for (let end = 0; end <= pattern.length; end++) {
|
|
6144
|
+
const pending = stringCheckpoint(work);
|
|
6145
|
+
if (pending)
|
|
6146
|
+
await pending;
|
|
6147
|
+
if (end !== pattern.length && pattern[end] !== "/")
|
|
6148
|
+
continue;
|
|
6149
|
+
if (end > start) {
|
|
6150
|
+
const bytes = (await scanString(pattern, work, start, end)).bytes;
|
|
6151
|
+
if (++this.budget.globstarStates > 100_000)
|
|
6152
|
+
failWalk("globstar traversal state limit exceeded");
|
|
6153
|
+
scratch.reserveBytes(bytes * 2 + 64);
|
|
6154
|
+
segments.push(pattern.slice(start, end));
|
|
6155
|
+
}
|
|
6156
|
+
start = end + 1;
|
|
6157
|
+
}
|
|
6158
|
+
const make = (parent, suffix, bytes, descend, depth) => {
|
|
6159
|
+
const separator = parent.path && parent.path !== "/" ? "/" : "";
|
|
6160
|
+
const size = parent.bytes + separator.length + bytes;
|
|
6161
|
+
if (size > this.budget.limits.maxExpansionBytes)
|
|
6162
|
+
this.budget.fail("maxExpansionBytes");
|
|
6163
|
+
if (++this.budget.globstarStates > 100_000)
|
|
6164
|
+
failWalk("globstar traversal state limit exceeded");
|
|
6165
|
+
if (depth > 128)
|
|
6166
|
+
failWalk("globstar directory depth limit exceeded");
|
|
6167
|
+
// Includes the candidate, queue/map/set slots and later segment-array
|
|
6168
|
+
// references. Reservations remain cumulative until this glob settles.
|
|
6169
|
+
scratch.reserveBytes(size * 2 + 128);
|
|
6170
|
+
return { path: parent.path + separator + suffix, bytes: size, descend, depth };
|
|
6171
|
+
};
|
|
6172
|
+
const empty = { path: "", bytes: 0, descend: true, depth: 0 };
|
|
6173
|
+
let candidates = [make(empty, pattern.startsWith("/") ? "/" : "", pattern.startsWith("/") ? 1 : 0, true, 0)];
|
|
6174
|
+
const ignored = (error) => {
|
|
6175
|
+
this.signal.throwIfAborted();
|
|
6176
|
+
return ["ENOENT", "ENOTDIR", "EACCES"].includes(errorCode(error) ?? "");
|
|
6177
|
+
};
|
|
6178
|
+
const read = async (candidate) => {
|
|
6179
|
+
const maxEntries = 100_000 - this.budget.globstarEntries;
|
|
6180
|
+
let entries;
|
|
6181
|
+
try {
|
|
6182
|
+
entries = await interruptible(this.fs.readdir(pathOf(state, candidate.path || "."), { signal: this.signal, maxEntries }), this.signal);
|
|
6183
|
+
}
|
|
6184
|
+
catch (error) {
|
|
6185
|
+
this.signal.throwIfAborted();
|
|
6186
|
+
if (errorCode(error) === "EFBIG")
|
|
6187
|
+
this.budget.controller.abort(error);
|
|
6188
|
+
throw error;
|
|
6189
|
+
}
|
|
6190
|
+
this.signal.throwIfAborted();
|
|
6191
|
+
if (entries.length > maxEntries)
|
|
6192
|
+
failWalk("globstar directory entry limit exceeded");
|
|
6193
|
+
this.budget.globstarEntries += entries.length;
|
|
6194
|
+
return entries;
|
|
6195
|
+
};
|
|
6196
|
+
let wildcardPrefix = false;
|
|
6197
|
+
for (let index = 0; index < segments.length; index++) {
|
|
6198
|
+
const segment = segments[index];
|
|
6199
|
+
const terminal = index === segments.length - 1;
|
|
6200
|
+
const next = new Map();
|
|
6201
|
+
const add = (candidate) => {
|
|
6202
|
+
const prior = next.get(candidate.path);
|
|
6203
|
+
if (!prior || candidate.descend && !prior.descend)
|
|
6204
|
+
next.set(candidate.path, candidate);
|
|
6205
|
+
};
|
|
6206
|
+
if (segment === "**") {
|
|
6207
|
+
const visited = new Set();
|
|
6208
|
+
for (const root of candidates) {
|
|
6209
|
+
this.signal.throwIfAborted();
|
|
6210
|
+
try {
|
|
6211
|
+
if ((await interruptible(this.fs.stat(pathOf(state, root.path || "."), { signal: this.signal }), this.signal)).type !== "directory")
|
|
6212
|
+
continue;
|
|
6213
|
+
}
|
|
6214
|
+
catch (error) {
|
|
6215
|
+
if (ignored(error))
|
|
6216
|
+
continue;
|
|
6217
|
+
throw error;
|
|
6218
|
+
}
|
|
6219
|
+
if (!terminal || root.path) {
|
|
6220
|
+
// A literal prefix retains its separator in the zero-depth
|
|
6221
|
+
// terminal match; expanded prefixes do not (GNU Bash 5.2).
|
|
6222
|
+
add(terminal && !wildcardPrefix && root.path !== "/" ? make(root, "", 0, root.descend, root.depth) : root);
|
|
6223
|
+
}
|
|
6224
|
+
const queue = root.descend ? [root] : [];
|
|
6225
|
+
while (queue.length) {
|
|
6226
|
+
const directory = queue.pop();
|
|
6227
|
+
if (visited.has(directory.path))
|
|
6228
|
+
continue;
|
|
6229
|
+
visited.add(directory.path);
|
|
6230
|
+
let entries;
|
|
6231
|
+
try {
|
|
6232
|
+
entries = await read(directory);
|
|
6233
|
+
}
|
|
6234
|
+
catch (error) {
|
|
6235
|
+
if (ignored(error))
|
|
6236
|
+
continue;
|
|
6237
|
+
throw error;
|
|
6238
|
+
}
|
|
6239
|
+
for (const entry of entries) {
|
|
6240
|
+
const pending = stringCheckpoint(work);
|
|
6241
|
+
if (pending)
|
|
6242
|
+
await pending;
|
|
6243
|
+
if (entry.name === "." || entry.name === ".." || !state.dotglob && entry.name.startsWith("."))
|
|
6244
|
+
continue;
|
|
6245
|
+
const bytes = (await scanString(entry.name, work)).bytes;
|
|
6246
|
+
const child = make(directory, entry.name, bytes, entry.type === "directory", directory.depth + (entry.type === "directory" ? 1 : 0));
|
|
6247
|
+
if (terminal || entry.type === "directory" || entry.type === "symlink" && root.path !== "")
|
|
6248
|
+
add(child);
|
|
6249
|
+
if (entry.type === "directory")
|
|
6250
|
+
queue.push(child);
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
wildcardPrefix = true;
|
|
6255
|
+
}
|
|
6256
|
+
else if (!/(?:^|[^\\])[*?[]/u.test(segment)) {
|
|
6257
|
+
const literal = segment.replace(/\\(.)/gu, "$1");
|
|
6258
|
+
const bytes = (await scanString(literal, work)).bytes;
|
|
6259
|
+
for (const candidate of candidates)
|
|
6260
|
+
add(make(candidate, literal, bytes, true, candidate.depth));
|
|
6261
|
+
}
|
|
6262
|
+
else {
|
|
6263
|
+
const matches = await compilePattern(segment, work);
|
|
6264
|
+
for (const candidate of candidates) {
|
|
6265
|
+
let entries;
|
|
6266
|
+
try {
|
|
6267
|
+
entries = await read(candidate);
|
|
6268
|
+
}
|
|
6269
|
+
catch (error) {
|
|
6270
|
+
if (ignored(error))
|
|
6271
|
+
continue;
|
|
6272
|
+
throw error;
|
|
6273
|
+
}
|
|
6274
|
+
for (const entry of entries) {
|
|
6275
|
+
const pending = stringCheckpoint(work);
|
|
6276
|
+
if (pending)
|
|
6277
|
+
await pending;
|
|
6278
|
+
if (entry.name !== "." && entry.name !== ".." && (state.dotglob || !entry.name.startsWith(".") || segment.startsWith(".")) && await matches(entry.name)) {
|
|
6279
|
+
add(make(candidate, entry.name, (await scanString(entry.name, work)).bytes, true, candidate.depth));
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
}
|
|
6283
|
+
wildcardPrefix = true;
|
|
6284
|
+
}
|
|
6285
|
+
candidates = [...next.values()];
|
|
6286
|
+
}
|
|
6287
|
+
const found = [];
|
|
6288
|
+
let outputBytes = 0;
|
|
6289
|
+
for (const candidate of candidates) {
|
|
6290
|
+
this.signal.throwIfAborted();
|
|
6291
|
+
try {
|
|
6292
|
+
const path = pathOf(state, candidate.path);
|
|
6293
|
+
const stat = await interruptible(value.endsWith("/") ? this.fs.stat(path, { signal: this.signal }) : this.fs.lstat(path, { signal: this.signal }), this.signal);
|
|
6294
|
+
if (value.endsWith("/") && stat.type !== "directory")
|
|
6295
|
+
continue;
|
|
6296
|
+
const slash = value.endsWith("/") && !candidate.path.endsWith("/") ? "/" : "";
|
|
6297
|
+
if (found.length >= this.budget.limits.maxExpansionFields)
|
|
6298
|
+
this.budget.fail("maxExpansionFields");
|
|
6299
|
+
outputBytes += candidate.bytes + slash.length;
|
|
6300
|
+
if (outputBytes > this.budget.limits.maxExpansionBytes)
|
|
6301
|
+
this.budget.fail("maxExpansionBytes");
|
|
6302
|
+
scratch.reserveBytes(candidate.bytes * 2 + slash.length + 8);
|
|
6303
|
+
found.push(candidate.path + slash);
|
|
6304
|
+
}
|
|
6305
|
+
catch (error) {
|
|
6306
|
+
if (!ignored(error))
|
|
6307
|
+
throw error;
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
// In-place heap sort preserves ordinary UTF-16 pathname order while
|
|
6311
|
+
// charging comparisons, including long common prefixes, and yielding.
|
|
6312
|
+
await sortExpansionStrings(found, work);
|
|
6313
|
+
return found.length ? found : [value];
|
|
6314
|
+
}
|
|
6315
|
+
finally {
|
|
6316
|
+
scratch.close();
|
|
6317
|
+
}
|
|
6318
|
+
}
|
|
5972
6319
|
async glob(value, pattern, state) {
|
|
5973
6320
|
if (!/(?:^|[^\\])[*?[]/u.test(pattern))
|
|
5974
6321
|
return [value];
|
|
6322
|
+
if (state.globstar) {
|
|
6323
|
+
const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, this.budget.limits.maxExpansionBytes * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
|
|
6324
|
+
let start = 0;
|
|
6325
|
+
for (let end = 0; end <= pattern.length; end++) {
|
|
6326
|
+
const pending = stringCheckpoint(work);
|
|
6327
|
+
if (pending)
|
|
6328
|
+
await pending;
|
|
6329
|
+
if (end !== pattern.length && pattern[end] !== "/")
|
|
6330
|
+
continue;
|
|
6331
|
+
if (end - start === 2 && pattern[start] === "*" && pattern[start + 1] === "*")
|
|
6332
|
+
return this.recursiveGlob(value, pattern, state);
|
|
6333
|
+
start = end + 1;
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
5975
6336
|
const absolute = pattern.startsWith("/");
|
|
5976
6337
|
const work = { remaining: Math.min(Number.MAX_SAFE_INTEGER, this.budget.limits.maxExpansionBytes * 4 + 1024), signal: this.signal, exhausted: () => this.budget.fail("maxExpansionBytes") };
|
|
5977
6338
|
let candidates = [absolute ? "/" : ""];
|