@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.
@@ -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
- if (!(assignment.kind === "compound" && assignment.append && writes === 0)) {
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.dotglob ? "s" : "u"} dotglob\n` : `dotglob \t${state.dotglob ? "on" : "off"}\n`);
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
- if ((!set || state.dotglob) && (!unset || !state.dotglob))
4494
- await emit();
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 (only dotglob is supported)`);
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.dotglob = set;
4546
+ state[name] = set;
4507
4547
  else {
4508
- await emit();
4509
- if (!state.dotglob)
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
- const quotedPresence = part.quoted && !(arrayOwned && isQuoteMarker(part));
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" && selector?.kind === "members" && !part.length && split && (!part.quoted || selector.separator === "@")) {
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 ? "/" : ""];