@sjawhar/pi-legion-envoy 5.17.2 → 5.17.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/legion.js +738 -97
  2. package/package.json +1 -1
package/dist/legion.js CHANGED
@@ -33473,7 +33473,7 @@ import { logger } from "@oh-my-pi/pi-utils";
33473
33473
  // package.json
33474
33474
  var package_default = {
33475
33475
  name: "@sjawhar/pi-legion-envoy",
33476
- version: "5.17.2",
33476
+ version: "5.17.4",
33477
33477
  type: "module",
33478
33478
  omp: {
33479
33479
  extensions: [
@@ -34776,6 +34776,8 @@ function createGoLegionTool(deps) {
34776
34776
  import {
34777
34777
  closeSync,
34778
34778
  existsSync,
34779
+ lstatSync,
34780
+ opendirSync,
34779
34781
  openSync,
34780
34782
  readFileSync as readFileSync3,
34781
34783
  readSync,
@@ -41435,9 +41437,15 @@ function isUnset(value) {
41435
41437
  }
41436
41438
  var NO_ELEMENTS = { kind: "literal", text: "", noElements: true };
41437
41439
  function stored(value) {
41438
- if (!value.some((piece) => piece.noElements === true))
41439
- return value;
41440
- return value.map((piece) => piece.noElements === true ? literal2("") : piece);
41440
+ const counted = value.map((piece) => {
41441
+ if (piece.unknownCount !== true)
41442
+ return piece;
41443
+ const { unknownCount: _, ...rest } = piece;
41444
+ return rest;
41445
+ });
41446
+ if (!counted.some((piece) => piece.noElements === true))
41447
+ return counted;
41448
+ return counted.map((piece) => piece.noElements === true ? literal2("") : piece);
41441
41449
  }
41442
41450
  var ANY_NAME = "\x00";
41443
41451
  var IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -41477,7 +41485,7 @@ function wrapRefusal(error48, site, label) {
41477
41485
  }
41478
41486
  var MAX_DEPTH = 8;
41479
41487
  var MAX_WALK_STEPS = 1e5;
41480
- var WALK_LIMIT = `the guard reached its walk limit of ${MAX_WALK_STEPS} nodes; run a smaller script`;
41488
+ var WALK_LIMIT = `the guard reached its walk limit of ${MAX_WALK_STEPS} steps (syntax nodes, pattern matching, and the destination entries a copy inspects); run a smaller script, or copy into a smaller destination`;
41481
41489
  var MAX_ALTERNATIVES = 64;
41482
41490
  var MAX_PATTERN_VALUE = 512;
41483
41491
  var PATTERN_WORK_PER_STEP = 1024;
@@ -41500,6 +41508,10 @@ var FILE_COMMANDS = new Set([
41500
41508
  "rm",
41501
41509
  "unlink",
41502
41510
  "mv",
41511
+ "cp",
41512
+ "dd",
41513
+ "install",
41514
+ "ln",
41503
41515
  "shred",
41504
41516
  "truncate",
41505
41517
  "chmod",
@@ -41507,6 +41519,51 @@ var FILE_COMMANDS = new Set([
41507
41519
  "chgrp"
41508
41520
  ]);
41509
41521
  var SIGNAL_COMMANDS = new Set(["kill", "pkill", "killall", "killall5"]);
41522
+ var FIND_VALUED_PREDICATES = new Set([
41523
+ "-amin",
41524
+ "-anewer",
41525
+ "-atime",
41526
+ "-cmin",
41527
+ "-cnewer",
41528
+ "-ctime",
41529
+ "-fls",
41530
+ "-fprint",
41531
+ "-fprint0",
41532
+ "-fprintf",
41533
+ "-fstype",
41534
+ "-gid",
41535
+ "-group",
41536
+ "-ilname",
41537
+ "-iname",
41538
+ "-inum",
41539
+ "-ipath",
41540
+ "-iregex",
41541
+ "-iwholename",
41542
+ "-links",
41543
+ "-lname",
41544
+ "-maxdepth",
41545
+ "-mindepth",
41546
+ "-mmin",
41547
+ "-mtime",
41548
+ "-name",
41549
+ "-newer",
41550
+ "-newerat",
41551
+ "-newerct",
41552
+ "-newermt",
41553
+ "-path",
41554
+ "-perm",
41555
+ "-printf",
41556
+ "-regex",
41557
+ "-regextype",
41558
+ "-samefile",
41559
+ "-size",
41560
+ "-type",
41561
+ "-uid",
41562
+ "-used",
41563
+ "-user",
41564
+ "-wholename",
41565
+ "-xtype"
41566
+ ]);
41510
41567
  var FUNCTION_OUTPUT_SILENT = {
41511
41568
  ":": true,
41512
41569
  break: true,
@@ -41759,8 +41816,10 @@ function resolveArrayIndex(index, st, ctx) {
41759
41816
  }
41760
41817
  function parameter(name, quoted, st, ctx) {
41761
41818
  if (name === "@" || name === "*") {
41762
- if (st.positional === undefined)
41763
- return [[unknown2(`\`$${name}\` (the positional parameters)`)]];
41819
+ if (st.positional === undefined) {
41820
+ const piece = unknown2(`\`$${name}\` (the positional parameters)`);
41821
+ return [[name === "*" && quoted ? piece : { ...piece, unknownCount: true }]];
41822
+ }
41764
41823
  return elements(st.positional, name === "*" && quoted);
41765
41824
  }
41766
41825
  if (/^[0-9]+$/.test(name)) {
@@ -41835,11 +41894,14 @@ function parameterExpansion(part, quoted, st, ctx) {
41835
41894
  if (part.index === "@" || part.index === "*") {
41836
41895
  if (array2 !== undefined) {
41837
41896
  const unknownElement = array2.get(UNKNOWN_ARRAY_INDEX);
41838
- if (unknownElement !== undefined)
41839
- return [[...unknownElement]];
41897
+ if (unknownElement !== undefined) {
41898
+ if (part.index === "*" && quoted)
41899
+ return [[...unknownElement]];
41900
+ return [unknownElement.map((piece) => ({ ...piece, unknownCount: true }))];
41901
+ }
41840
41902
  return elements([...array2.values()], part.index === "*" && quoted);
41841
41903
  }
41842
- return parameter(name, quoted, st, ctx);
41904
+ return parameter(name, quoted, st, ctx).map((alt) => !(part.index === "*" && quoted) && alt.some((piece) => piece.kind === "unknown") ? alt.map((piece) => ({ ...piece, unknownCount: true })) : alt);
41843
41905
  }
41844
41906
  if (part.index !== undefined) {
41845
41907
  const index = resolveArrayIndex(part.index, st, ctx);
@@ -42058,10 +42120,18 @@ function braceAlternatives(part) {
42058
42120
  return [[{ kind: "glob", text: part.text }]];
42059
42121
  return inner.split(",").map((alternative) => [literal2(alternative)]);
42060
42122
  }
42123
+ function tildeAt(text) {
42124
+ if (text.startsWith("~"))
42125
+ return 0;
42126
+ const equals = text.indexOf("=");
42127
+ if (equals <= 0 || !IDENTIFIER.test(text.slice(0, equals)))
42128
+ return;
42129
+ return text.charAt(equals + 1) === "~" ? equals + 1 : undefined;
42130
+ }
42061
42131
  function expandPart(part, first, st, ctx) {
42062
42132
  switch (part.type) {
42063
42133
  case "Literal":
42064
- return [unquotedLiteral(part.text, first ? 0 : undefined, st, ctx)];
42134
+ return [unquotedLiteral(part.text, first ? tildeAt(part.text) : undefined, st, ctx)];
42065
42135
  case "SingleQuoted":
42066
42136
  case "AnsiCQuoted":
42067
42137
  return [[literal2(part.value)]];
@@ -42117,7 +42187,7 @@ function textLength(pieces) {
42117
42187
  function expandWord(word, st, ctx) {
42118
42188
  let alternatives = [[]];
42119
42189
  if (word.parts === undefined)
42120
- alternatives = [unquotedLiteral(word.text, 0, st, ctx)];
42190
+ alternatives = [unquotedLiteral(word.text, tildeAt(word.text), st, ctx)];
42121
42191
  else {
42122
42192
  word.parts.forEach((part, index) => {
42123
42193
  alternatives = product(alternatives, expandPart(part, index === 0, st, ctx));
@@ -42143,6 +42213,9 @@ function args(words, st, ctx) {
42143
42213
  if (exp.some((piece) => piece.kind === "glob")) {
42144
42214
  return { text: word.text, exp, fields: "unknown" };
42145
42215
  }
42216
+ if (exp.some((piece) => piece.unknownCount === true)) {
42217
+ return { text: word.text, exp, fields: "unknown" };
42218
+ }
42146
42219
  if (unquoted && exp.some((piece) => piece.kind === "unknown" || /[ \t\n]/.test(piece.text))) {
42147
42220
  return { text: word.text, exp, fields: "unknown" };
42148
42221
  }
@@ -42166,6 +42239,46 @@ function literalText(exp) {
42166
42239
  return;
42167
42240
  return exp.map((piece) => piece.text).join("");
42168
42241
  }
42242
+ function readableWord(arg) {
42243
+ const whole = literalText(arg.exp);
42244
+ if (whole !== undefined)
42245
+ return { text: whole, whole: true };
42246
+ let text = "";
42247
+ for (const piece of arg.exp) {
42248
+ if (piece.kind !== "literal")
42249
+ break;
42250
+ text += piece.text;
42251
+ }
42252
+ return { text, whole: false };
42253
+ }
42254
+ function mayBeOption(arg, matches) {
42255
+ const { text, whole } = readableWord(arg);
42256
+ if (whole)
42257
+ return matches(text);
42258
+ return mayBeSomeOption(text);
42259
+ }
42260
+ function mayBeSomeOption(text) {
42261
+ return text === "" || /^--?[a-zA-Z-]*$/.test(text);
42262
+ }
42263
+ function valueInWord(arg, count) {
42264
+ const pieces = [];
42265
+ let dropped = 0;
42266
+ for (const piece of arg.exp) {
42267
+ if (dropped >= count) {
42268
+ pieces.push(piece);
42269
+ continue;
42270
+ }
42271
+ if (piece.kind !== "literal")
42272
+ return;
42273
+ if (piece.text.length + dropped <= count) {
42274
+ dropped += piece.text.length;
42275
+ continue;
42276
+ }
42277
+ pieces.push({ ...piece, text: piece.text.slice(count - dropped) });
42278
+ dropped = count;
42279
+ }
42280
+ return dropped < count || pieces.length === 0 ? undefined : pieces;
42281
+ }
42169
42282
  function visitParts(parts, st, ctx) {
42170
42283
  for (const part of parts ?? []) {
42171
42284
  switch (part.type) {
@@ -42600,6 +42713,15 @@ function spendPatternWork(work, ctx) {
42600
42713
  function outputChanged(before, after) {
42601
42714
  return before !== undefined && (after === undefined || JSON.stringify(before) !== JSON.stringify(after));
42602
42715
  }
42716
+ function loopValue(type, name, words) {
42717
+ const pids = words.every((word) => {
42718
+ const text = literalText(word.exp);
42719
+ return text === "" || word.exp.length > 0 && word.exp.every((piece) => piece.descendantPid);
42720
+ });
42721
+ if (pids)
42722
+ return [unknown2(`\`$${name}\`, a loop variable`, true)];
42723
+ return type === "For" && words.length === 1 && words[0]?.exp[0]?.kind === "literal" && words[0].exp[0].text !== "" && !words[0].exp[0].text.startsWith("-") && words[0]?.exp.some((piece) => piece.kind === "glob") ? words[0].exp : [unknown2(`\`$${name}\`, a loop variable`)];
42724
+ }
42603
42725
  function walkNode(node, st, ctx, pipeIn) {
42604
42726
  if (++ctx.steps.count > MAX_WALK_STEPS) {
42605
42727
  const site = siteOf(node, st);
@@ -42723,11 +42845,7 @@ function walkNode(node, st, ctx, pipeIn) {
42723
42845
  }
42724
42846
  } else {
42725
42847
  const body = clone2(st);
42726
- const signalSafe = words.every((word) => {
42727
- const text = literalText(word.exp);
42728
- return text === "" || word.exp.length > 0 && word.exp.every((piece) => piece.descendantPid);
42729
- });
42730
- assignScalar(body, name, signalSafe ? [unknown2(`\`$${name}\`, a loop variable`, true)] : [unknown2(`\`$${name}\`, a loop variable`)]);
42848
+ assignScalar(body, name, loopValue(node.type, name, words));
42731
42849
  if (node.type === "Select")
42732
42850
  assignScalar(body, "REPLY", [unknown2("`$REPLY`, read from input")]);
42733
42851
  uncertainly(ctx, () => walkList(node.body.commands, body, ctx));
@@ -43087,6 +43205,7 @@ function operands(list, valued, longValued = []) {
43087
43205
  return { options, operands: found };
43088
43206
  }
43089
43207
  function unwrap2(argv, st, ctx, site) {
43208
+ const alts = [];
43090
43209
  let list = argv;
43091
43210
  let state = st;
43092
43211
  for (let guard = 0;guard < 16; guard += 1) {
@@ -43095,13 +43214,19 @@ function unwrap2(argv, st, ctx, site) {
43095
43214
  break;
43096
43215
  const base = path4.basename(name);
43097
43216
  const rest = list.slice(1);
43098
- const skip = (valued, longValued = []) => {
43217
+ const skip = (valued, longValued = [], positional = false) => {
43099
43218
  let i = 0;
43100
43219
  for (;i < rest.length; i += 1) {
43101
- const text = literalText(rest[i]?.exp);
43220
+ const arg = rest[i];
43221
+ const text = literalText(arg.exp);
43102
43222
  if (text === "--")
43103
43223
  return rest.slice(i + 1);
43104
- if (text === undefined || !text.startsWith("-") || text === "-")
43224
+ if (text === undefined) {
43225
+ if (positional || !mayBeOption(arg, (option) => option.startsWith("-")))
43226
+ break;
43227
+ continue;
43228
+ }
43229
+ if (!text.startsWith("-") || text === "-")
43105
43230
  break;
43106
43231
  if (text.startsWith("--")) {
43107
43232
  if (!text.includes("=") && longValued.includes(text))
@@ -43115,8 +43240,10 @@ function unwrap2(argv, st, ctx, site) {
43115
43240
  }
43116
43241
  return rest.slice(i);
43117
43242
  };
43118
- if ((base === "busybox" || base === "toybox") && rest[0] !== undefined)
43243
+ if ((base === "busybox" || base === "toybox") && rest[0] !== undefined && literalText(rest[0].exp) !== undefined) {
43119
43244
  list = rest;
43245
+ continue;
43246
+ }
43120
43247
  if (base === "sudo" || base === "doas")
43121
43248
  list = skip("ughpCDrtUT", ["--user", "--group"]);
43122
43249
  else if (base === "nice")
@@ -43128,15 +43255,23 @@ function unwrap2(argv, st, ctx, site) {
43128
43255
  else if (base === "command") {
43129
43256
  const flags = rest.map((arg) => literalText(arg.exp));
43130
43257
  if (flags[0] === "-v" || flags[0] === "-V")
43131
- return { argv: [], st: state };
43258
+ return { argv: [], st: state, alts };
43132
43259
  list = skip("");
43133
43260
  } else if (base === "stdbuf")
43134
43261
  list = skip("ioe", ["--input", "--output", "--error"]);
43135
43262
  else if (base === "ionice")
43136
43263
  list = skip("cnt", ["--class", "--classdata"]);
43137
43264
  else if (base === "timeout") {
43138
- const after = skip("sk", ["--signal", "--kill-after"]);
43139
- list = after.slice(1);
43265
+ const stopped = skip("sk", ["--signal", "--kill-after"], true);
43266
+ list = stopped.slice(1);
43267
+ for (let at = rest.length - stopped.length;at < rest.length; at += 1) {
43268
+ if (literalText(rest[at]?.exp) !== undefined)
43269
+ break;
43270
+ alts.push(rest.slice(at + 1));
43271
+ }
43272
+ const read = skip("sk", ["--signal", "--kill-after"]);
43273
+ if (read.length !== stopped.length)
43274
+ alts.push(read.slice(1));
43140
43275
  } else if (base === "env") {
43141
43276
  let i = 0;
43142
43277
  const overlay = new Map;
@@ -43176,7 +43311,7 @@ function unwrap2(argv, st, ctx, site) {
43176
43311
  overlay.set(arg.exp[0].text.slice(0, arg.exp[0].text.indexOf("=")), [
43177
43312
  unknown2("an `env` assignment")
43178
43313
  ]);
43179
- } else
43314
+ } else if (text === undefined && mayBeOption(arg, (option) => option.startsWith("-"))) {} else
43180
43315
  break;
43181
43316
  }
43182
43317
  if (i !== -1)
@@ -43193,7 +43328,7 @@ function unwrap2(argv, st, ctx, site) {
43193
43328
  } else
43194
43329
  break;
43195
43330
  }
43196
- return { argv: list, st: state };
43331
+ return { argv: list, st: state, alts };
43197
43332
  }
43198
43333
  function runFunction(name, definition, args2, outer, ctx) {
43199
43334
  if (outer.runningFunctions.has(name))
@@ -43278,6 +43413,9 @@ function dispatch(invocation, outer, ctx) {
43278
43413
  const unwrapped = unwrap2(invocation.args, outer, ctx, invocation.site);
43279
43414
  const argv = unwrapped.argv;
43280
43415
  const st = unwrapped.st;
43416
+ for (const other of unwrapped.alts) {
43417
+ dispatch({ ...invocation, args: other }, clone2(outer), ctx);
43418
+ }
43281
43419
  const name = literalText(argv[0]?.exp);
43282
43420
  if (name === undefined)
43283
43421
  return;
@@ -43425,15 +43563,21 @@ function dispatch(invocation, outer, ctx) {
43425
43563
  const leading = head[0];
43426
43564
  const spaced = literalText(head) === "-v";
43427
43565
  const joined = !spaced && leading?.kind === "literal" && leading.text.startsWith("-v") && (leading.text.length > 2 || head.length > 1);
43428
- if (spaced || joined) {
43429
- const variable = spaced ? literalText(rest[1]?.exp) : literalText([
43566
+ const first = rest[0];
43567
+ const mayBeV = !spaced && !joined && first !== undefined && rest[1] !== undefined && mayBeOption(first, (text) => text.startsWith("-v"));
43568
+ if (spaced || joined || mayBeV) {
43569
+ const variable = spaced || mayBeV ? literalText(rest[1]?.exp) : literalText([
43430
43570
  { ...leading, text: leading?.text.slice(2) ?? "" },
43431
43571
  ...head.slice(1)
43432
43572
  ]);
43433
- const printed = rest.slice(spaced ? 2 : 1);
43573
+ const printed = rest.slice(spaced || mayBeV ? 2 : 1);
43434
43574
  const [format, value, ...others] = printed;
43435
43575
  let assigned;
43436
- if (literalText(format?.exp) === "%s" && value !== undefined && others.length === 0) {
43576
+ if (mayBeV) {
43577
+ assigned = [
43578
+ unknown2(`\`$${variable ?? "?"}\` (printf with an option word the guard cannot read, which may be \`-v\`)`)
43579
+ ];
43580
+ } else if (literalText(format?.exp) === "%s" && value !== undefined && others.length === 0) {
43437
43581
  assigned = value.exp;
43438
43582
  } else {
43439
43583
  const text = printfText(printed, MAX_VALUE_LENGTH);
@@ -43516,24 +43660,78 @@ function dispatch(invocation, outer, ctx) {
43516
43660
  return;
43517
43661
  case "rm":
43518
43662
  case "unlink":
43519
- checkTargets(base, "delete", operands(rest, "").operands, false, st, ctx, site);
43663
+ checkTargets(base, "delete", operands(rest, "").operands, ACTS_ON_LINK, st, ctx, site);
43520
43664
  return;
43521
43665
  case "mv": {
43522
- const found = operands(rest, "tS", ["--target-directory", "--suffix"]);
43523
- const targets = [...found.operands];
43524
- const at = rest.findIndex((arg) => {
43525
- const text = literalText(arg.exp);
43526
- return text === "-t" || text === "--target-directory";
43527
- });
43528
- if (at !== -1 && rest[at + 1] !== undefined)
43529
- targets.push(rest[at + 1]);
43666
+ const found = writeArguments(rest, "tS", MV_OPTIONS);
43667
+ if (found.flags.has("--help") || found.flags.has("--version"))
43668
+ return;
43669
+ const targets = [...found.operands, ...writeDestinations(found, site)];
43670
+ checkTargets("mv", "move", targets, ACTS_ON_LINK, st, ctx, site);
43671
+ return;
43672
+ }
43673
+ case "cp": {
43674
+ const found = writeArguments(rest, "tS", CP_OPTIONS);
43675
+ if (found.flags.has("--help") || found.flags.has("--version"))
43676
+ return;
43677
+ const unlinks = found.flags.has("b") || found.flags.has("--backup") || found.flags.has("--remove-destination") || found.uncertain;
43678
+ const destinations = writeDestinations(found, site);
43679
+ checkTargets("cp", "overwrite", destinations, unlinks ? WRITES_EITHER : WRITES_THROUGH_LINK, st, ctx, site);
43680
+ checkCopyContents(found, destinations, st, ctx, site);
43681
+ return;
43682
+ }
43683
+ case "install": {
43684
+ const found = writeArguments(rest, "mogtS", INSTALL_OPTIONS);
43685
+ if (found.flags.has("--help") || found.flags.has("--version"))
43686
+ return;
43687
+ const directories = found.flags.has("d") || found.flags.has("--directory");
43688
+ checkTargets("install", directories ? "create or change the mode of" : "overwrite", directories ? found.operands : writeDestinations(found, site), WRITES_EITHER, st, ctx, site);
43689
+ return;
43690
+ }
43691
+ case "dd": {
43692
+ const targets = [];
43530
43693
  for (const arg of rest) {
43531
- const text = literalText(arg.exp);
43532
- if (text?.startsWith("--target-directory=")) {
43533
- targets.push({ text: arg.text, exp: [literal2(text.slice(19))] });
43694
+ const { text, whole } = readableWord(arg);
43695
+ if (whole && longOption(text, ["--help", "--version"]) !== undefined)
43696
+ return;
43697
+ if (text.startsWith("of=")) {
43698
+ const carried = valueInWord(arg, 3);
43699
+ targets.push({
43700
+ text: arg.text,
43701
+ exp: carried ?? [unknown2("an `of=` operand the guard cannot read")]
43702
+ });
43703
+ continue;
43704
+ }
43705
+ if (!whole && "of=".startsWith(text)) {
43706
+ targets.push({
43707
+ text: arg.text,
43708
+ exp: [unknown2("a `dd` operand the guard cannot read, which may name its `of=` file")]
43709
+ });
43534
43710
  }
43535
43711
  }
43536
- checkTargets("mv", "move", targets, false, st, ctx, site);
43712
+ checkTargets("dd", "write", targets, WRITES_THROUGH_LINK, st, ctx, site);
43713
+ return;
43714
+ }
43715
+ case "ln": {
43716
+ const found = writeArguments(rest, "St", LN_OPTIONS);
43717
+ if (found.flags.has("--help") || found.flags.has("--version"))
43718
+ return;
43719
+ const sole = found.operands[0];
43720
+ const single = found.operands.length === 1 && found.directory === undefined && !found.uncertain && sole !== undefined && readableWord(sole).whole;
43721
+ const destinations = single ? [{ text: ".", exp: [literal2(".")] }] : writeDestinations(found, site);
43722
+ checkTargets("ln", "overwrite", destinations, WRITES_EITHER, st, ctx, site);
43723
+ const symbolic = found.flags.has("s") || found.flags.has("--symbolic");
43724
+ if (!symbolic) {
43725
+ const sources = found.operands.filter((arg) => !destinations.includes(arg));
43726
+ checkTargets("ln", "hard-link", sources, LINKS_TO, st, ctx, site);
43727
+ }
43728
+ return;
43729
+ }
43730
+ case "sed": {
43731
+ const writes = sedWrites(rest);
43732
+ const program = writes.inferred ? "sed, whose option word the guard cannot read and which may be `-i`," : "sed -i";
43733
+ checkTargets(program, "rewrite in place", writes.replaced, WRITES_LINK, st, ctx, site);
43734
+ checkTargets(program, "rewrite in place", writes.followed, WRITES_EITHER, st, ctx, site);
43537
43735
  return;
43538
43736
  }
43539
43737
  case "rsync": {
@@ -43555,41 +43753,48 @@ function dispatch(invocation, outer, ctx) {
43555
43753
  "--remote-option",
43556
43754
  "--rsync-path",
43557
43755
  "--temp-dir"
43558
- ]).operands.at(-1);
43756
+ ]).operands.filter(passesAnArgument).at(-1);
43559
43757
  if (destination !== undefined) {
43560
- checkTargets("rsync", "synchronize and delete into", [destination], true, st, ctx, site);
43758
+ checkTargets("rsync", "synchronize and delete into", [destination], ACTS_THROUGH_LINK, st, ctx, site);
43561
43759
  }
43562
43760
  return;
43563
43761
  }
43564
43762
  case "tar":
43565
43763
  case "unzip": {
43566
43764
  const extracting = base === "unzip" || rest.some((arg, index) => {
43567
- const text = literalText(arg.exp);
43568
- return text === "--extract" || text === "--get" || text?.startsWith("-") === true && !text.startsWith("--") && text.includes("x") || index === 0 && /^[A-Za-z]*x[A-Za-z]*$/.test(text ?? "");
43765
+ const { text, whole } = readableWord(arg);
43766
+ if (!whole && index === 0 && /^[A-Za-z]*$/.test(text))
43767
+ return true;
43768
+ return mayBeOption(arg, (mode) => mode === "--extract" || mode === "--get" || mode.startsWith("-") && !mode.startsWith("--") && mode.includes("x") || index === 0 && /^[A-Za-z]*x[A-Za-z]*$/.test(mode));
43569
43769
  });
43570
43770
  if (!extracting)
43571
43771
  return;
43572
43772
  const targetDirectories = [];
43573
43773
  const short = base === "tar" ? "-C" : "-d";
43574
43774
  const long = base === "tar" ? "--directory" : "--destination";
43775
+ const letter = short.slice(1);
43575
43776
  for (const [index, arg] of rest.entries()) {
43576
- const text = literalText(arg.exp);
43577
- if (text === short || text === long) {
43777
+ const { text, whole } = readableWord(arg);
43778
+ const clusterAt = text.startsWith("-") && !text.startsWith("--") ? text.indexOf(letter, 1) : -1;
43779
+ const carried = text.startsWith(`${long}=`) ? valueInWord(arg, long.length + 1) : clusterAt !== -1 && (!whole || text.length > clusterAt + 1) ? valueInWord(arg, clusterAt + 1) : undefined;
43780
+ if (carried !== undefined) {
43781
+ targetDirectories.push({ text: arg.text, exp: carried });
43782
+ continue;
43783
+ }
43784
+ if (clusterAt !== -1 || mayBeOption(arg, (option) => option === short || option === long)) {
43578
43785
  const target = rest[index + 1];
43579
43786
  if (target !== undefined)
43580
43787
  targetDirectories.push(target);
43581
- } else if (text?.startsWith(`${long}=`)) {
43582
- targetDirectories.push({ text: arg.text, exp: [literal2(text.slice(long.length + 1))] });
43583
43788
  }
43584
43789
  }
43585
- checkTargets(base, "extract and overwrite in", targetDirectories, true, st, ctx, site);
43790
+ checkTargets(base, "extract and overwrite in", targetDirectories, ACTS_THROUGH_LINK, st, ctx, site);
43586
43791
  return;
43587
43792
  }
43588
43793
  case "shred":
43589
- checkTargets("shred", "shred", operands(rest, "ns", ["--iterations", "--size", "--random-source"]).operands, false, st, ctx, site);
43794
+ checkTargets("shred", "shred", operands(rest, "ns", ["--iterations", "--size", "--random-source"]).operands, ACTS_ON_BOTH, st, ctx, site);
43590
43795
  return;
43591
43796
  case "truncate":
43592
- checkTargets("truncate", "truncate", operands(rest, "sr", ["--size", "--reference"]).operands, true, st, ctx, site);
43797
+ checkTargets("truncate", "truncate", operands(rest, "sr", ["--size", "--reference"]).operands, ACTS_THROUGH_LINK, st, ctx, site);
43593
43798
  return;
43594
43799
  case "chmod":
43595
43800
  case "chown":
@@ -43627,41 +43832,73 @@ function dispatch(invocation, outer, ctx) {
43627
43832
  case "killall":
43628
43833
  case "killall5":
43629
43834
  throw new Refusal(site.snippet, site.line, `${base} selects processes by name or pattern, which reaches other agents' processes on a ` + "shared machine; a pane signals only processes it started. Stop a process you started " + 'with the hub tool (`op: "stop"`), or `kill` the pid of a child you started from this pane');
43630
- case "fuser":
43631
- if (rest.some((arg) => /^-[a-zA-Z]*k/.test(literalText(arg.exp) ?? ""))) {
43835
+ case "fuser": {
43836
+ const kills = rest.some((arg) => mayBeOption(arg, (text) => /^-[a-zA-Z]*k/.test(text) || text === "--kill"));
43837
+ const readable2 = rest.every((arg) => literalText(arg.exp) !== undefined);
43838
+ const mayBeSeveral = rest.some((arg) => arg.fields === "unknown");
43839
+ if (kills && (readable2 || mayBeSeveral || rest.length > 1)) {
43632
43840
  throw new Refusal(site.snippet, site.line, "`fuser -k` kills every process using a file, other agents' included; a pane signals only " + "processes it started");
43633
43841
  }
43634
43842
  return;
43843
+ }
43635
43844
  case "tmux": {
43636
43845
  let socket;
43637
43846
  let socketIsPath = false;
43638
- for (let i = 0;i < rest.length; i += 1) {
43639
- const text = literalText(rest[i]?.exp);
43640
- if (text === "-L" || text === "-S" || text === "--socket") {
43641
- socket = rest[i + 1];
43642
- socketIsPath = text !== "-L";
43643
- i += 1;
43644
- } else if (text?.startsWith("--socket=")) {
43645
- socket = { text, exp: [literal2(text.slice(9))] };
43646
- socketIsPath = true;
43847
+ let current = { words: [], speculative: false };
43848
+ const segments = [current];
43849
+ for (const arg of rest) {
43850
+ const word = literalText(arg.exp);
43851
+ const trailing = word !== ";" && word !== undefined && word.endsWith(";");
43852
+ if (trailing)
43853
+ current.words.push({ text: arg.text, exp: [literal2(word.slice(0, -1))] });
43854
+ else if (word !== ";")
43855
+ current.words.push(arg);
43856
+ if (word === ";" || trailing) {
43857
+ current = { words: [], speculative: false };
43858
+ segments.push(current);
43859
+ } else if (word === undefined || arg.fields === "unknown") {
43860
+ current = { words: [], speculative: true };
43861
+ segments.push(current);
43647
43862
  }
43648
43863
  }
43649
43864
  const killCommands = ["kill-server", "kill-session", "kill-window", "kill-pane"];
43650
43865
  const respawnCommands = ["respawn-pane", "respawn-window"];
43651
- const respawning = rest.some((arg) => {
43652
- const text = literalText(arg.exp);
43653
- if (text === undefined)
43654
- return false;
43655
- const compact = text.replace("-", "");
43656
- return respawnCommands.filter((command) => command.replace("-", "").startsWith(compact)).length === 1;
43657
- }) && rest.some((arg) => literalText(arg.exp) === "-k");
43658
- const killing = rest.find((arg) => {
43659
- const text = literalText(arg.exp);
43660
- if (text === undefined)
43866
+ const names = (word, commands) => {
43867
+ if (word === undefined)
43661
43868
  return false;
43869
+ const { text, whole } = readableWord(word);
43662
43870
  const compact = text.replace("-", "");
43663
- return killCommands.filter((command) => command.replace("-", "").startsWith(compact)).length === 1 || respawning;
43664
- });
43871
+ const normalized = commands.map((command) => command.replace("-", ""));
43872
+ return whole ? completeName(compact, normalized) !== undefined : normalized.some((command) => command.startsWith(compact));
43873
+ };
43874
+ let killing;
43875
+ for (const segment of segments) {
43876
+ const words = segment.words;
43877
+ let subcommand;
43878
+ for (let i = 0;i < words.length; i += 1) {
43879
+ const arg = words[i];
43880
+ const { text, whole } = readableWord(arg);
43881
+ const attached = (option) => text.startsWith(option) && (!whole || text.length > option.length) ? valueInWord(arg, option.length) : undefined;
43882
+ const carried = attached("-L") ?? attached("-S") ?? attached("--socket=");
43883
+ if (carried !== undefined) {
43884
+ socket = { text: arg.text, exp: carried };
43885
+ socketIsPath = !text.startsWith("-L");
43886
+ } else if (text === "-L" || text === "-S" || text === "--socket") {
43887
+ socket = words[i + 1];
43888
+ socketIsPath = text !== "-L";
43889
+ i += 1;
43890
+ } else if (text === "-f" || text === "-T") {
43891
+ i += 1;
43892
+ } else if (subcommand === undefined && !text.startsWith("-")) {
43893
+ subcommand = arg;
43894
+ }
43895
+ }
43896
+ if (segment.speculative && (subcommand === undefined || readableWord(subcommand).text === ""))
43897
+ continue;
43898
+ const respawning = names(subcommand, respawnCommands) && words.some((arg) => mayBeOption(arg, (text) => text === "-k"));
43899
+ if (names(subcommand, killCommands) || respawning)
43900
+ killing ??= subcommand;
43901
+ }
43665
43902
  if (killing === undefined)
43666
43903
  return;
43667
43904
  let resolvedSocket;
@@ -43685,13 +43922,19 @@ function dispatch(invocation, outer, ctx) {
43685
43922
  if (resolvedSocket !== undefined && judgePath(resolvedSocket, st, ctx, { follow: true, overwrite: false }).ok) {
43686
43923
  return;
43687
43924
  }
43688
- throw new Refusal(site.snippet, site.line, `\`tmux ${literalText(killing.exp)}\` needs a socket path inside this pane's roots; the ` + "resolved socket can end panes and processes this pane did not start");
43925
+ throw new Refusal(site.snippet, site.line, `\`tmux ${killing.text}\` needs a socket path inside this pane's roots; the ` + "resolved socket can end panes and processes this pane did not start");
43926
+ }
43927
+ case "busybox":
43928
+ case "toybox": {
43929
+ if (rest.length === 0)
43930
+ return;
43931
+ throw new Refusal(site.snippet, site.line, `the guard cannot read the applet \`${rest[0].text}\` ${base} would run, and the ` + "applets it carries include `rm`, `sh` and `killall`, which reach paths and processes " + `this pane did not start; name the applet (\`${base} rm \u2026\`) so it can be checked`);
43689
43932
  }
43690
43933
  default:
43691
43934
  break;
43692
43935
  }
43693
- if (SHELLS.has(base) || base === "busybox" && SHELLS.has(literalText(rest[0]?.exp) ?? "")) {
43694
- runShell(base === "busybox" ? rest.slice(1) : rest, invocation, st, ctx);
43936
+ if (SHELLS.has(base)) {
43937
+ runShell(rest, invocation, st, ctx);
43695
43938
  return;
43696
43939
  }
43697
43940
  const interpreter = interpreterLanguage(base);
@@ -43721,8 +43964,9 @@ function changeDirectory(target, st, ctx) {
43721
43964
  function declaration(builtin, list, st, ctx, site) {
43722
43965
  const options = list.map((arg) => literalText(arg.exp) ?? "").filter((text) => /^[-+][a-zA-Z]+$/.test(text));
43723
43966
  const option = (letter) => options.some((text) => text.startsWith("-") && text.includes(letter));
43724
- if (builtin !== "export" && option("n")) {
43725
- throw new Refusal(site.snippet, site.line, "the guard does not follow a nameref (`declare -n`), whose writes land in the variable it names");
43967
+ const mayBeNameref = list.some((arg) => mayBeOption(arg, (text) => /^-[a-zA-Z]*n/.test(text)));
43968
+ if (builtin !== "export" && mayBeNameref) {
43969
+ throw new Refusal(site.snippet, site.line, option("n") ? "the guard does not follow a nameref (`declare -n`), whose writes land in the variable it names" : `an option word \`${builtin}\` carries that the guard cannot read may be \`-n\`, a nameref ` + "whose writes land in the variable it names and which the guard does not follow; write " + "the option as a literal");
43726
43970
  }
43727
43971
  if (option("f") || option("F"))
43728
43972
  return;
@@ -43869,11 +44113,50 @@ function nameAndValue(arg) {
43869
44113
  }
43870
44114
  return { name, value: undefined };
43871
44115
  }
43872
- function checkTargets(program, verb, targets, follow, st, ctx, site) {
44116
+ var ACTS_ON_LINK = {
44117
+ followed: false,
44118
+ unfollowed: true,
44119
+ overwrite: false
44120
+ };
44121
+ var ACTS_THROUGH_LINK = {
44122
+ followed: true,
44123
+ unfollowed: false,
44124
+ overwrite: false
44125
+ };
44126
+ var ACTS_ON_BOTH = {
44127
+ followed: true,
44128
+ unfollowed: true,
44129
+ overwrite: false
44130
+ };
44131
+ var WRITES_THROUGH_LINK = {
44132
+ followed: true,
44133
+ unfollowed: false,
44134
+ overwrite: true
44135
+ };
44136
+ var WRITES_LINK = {
44137
+ followed: false,
44138
+ unfollowed: true,
44139
+ overwrite: true
44140
+ };
44141
+ var WRITES_EITHER = {
44142
+ followed: true,
44143
+ unfollowed: true,
44144
+ overwrite: true
44145
+ };
44146
+ var LINKS_TO = {
44147
+ followed: true,
44148
+ unfollowed: false,
44149
+ overwrite: true
44150
+ };
44151
+ function checkTargets(program, verb, targets, reach, st, ctx, site) {
43873
44152
  for (const target of targets) {
43874
- const verdict = judgePath(target.exp, st, ctx, { follow, overwrite: false });
43875
- if (!verdict.ok) {
43876
- throw refusal(site, `${program} would ${verb} \`${target.text}\` (${verdict.resolution})`, ctx);
44153
+ for (const follow of [true, false]) {
44154
+ if (!(follow ? reach.followed : reach.unfollowed))
44155
+ continue;
44156
+ const verdict = judgePath(target.exp, st, ctx, { follow, overwrite: reach.overwrite });
44157
+ if (!verdict.ok) {
44158
+ throw refusal(site, `${program} would ${verb} \`${target.text}\` (${verdict.resolution})`, ctx);
44159
+ }
43877
44160
  }
43878
44161
  const text = literalText(target.exp);
43879
44162
  if (text !== undefined && (text.startsWith("/") || st.cwd !== undefined)) {
@@ -43881,6 +44164,339 @@ function checkTargets(program, verb, targets, follow, st, ctx, site) {
43881
44164
  }
43882
44165
  }
43883
44166
  }
44167
+ function completeName(prefix, names) {
44168
+ if (names.includes(prefix))
44169
+ return prefix;
44170
+ const reachable = names.filter((name) => name.startsWith(prefix));
44171
+ return reachable.length === 1 ? reachable[0] : undefined;
44172
+ }
44173
+ function longOption(text, names) {
44174
+ return completeName(text.split("=", 1)[0], names);
44175
+ }
44176
+ var GNU_INFORMATION = { "--help": false, "--version": false };
44177
+ var MV_OPTIONS = {
44178
+ "--backup": false,
44179
+ "--context": false,
44180
+ "--debug": false,
44181
+ "--force": false,
44182
+ "--interactive": false,
44183
+ "--no-clobber": false,
44184
+ "--no-copy": false,
44185
+ "--no-target-directory": false,
44186
+ "--strip-trailing-slashes": false,
44187
+ "--suffix": true,
44188
+ "--target-directory": true,
44189
+ "--update": false,
44190
+ "--verbose": false,
44191
+ ...GNU_INFORMATION
44192
+ };
44193
+ var CP_OPTIONS = {
44194
+ "--archive": false,
44195
+ "--attributes-only": false,
44196
+ "--backup": false,
44197
+ "--context": false,
44198
+ "--copy-contents": false,
44199
+ "--debug": false,
44200
+ "--dereference": false,
44201
+ "--force": false,
44202
+ "--interactive": false,
44203
+ "--link": false,
44204
+ "--no-clobber": false,
44205
+ "--no-dereference": false,
44206
+ "--no-preserve": true,
44207
+ "--no-target-directory": false,
44208
+ "--one-file-system": false,
44209
+ "--parents": false,
44210
+ "--preserve": false,
44211
+ "--recursive": false,
44212
+ "--reflink": false,
44213
+ "--remove-destination": false,
44214
+ "--sparse": true,
44215
+ "--strip-trailing-slashes": false,
44216
+ "--suffix": true,
44217
+ "--symbolic-link": false,
44218
+ "--target-directory": true,
44219
+ "--update": false,
44220
+ "--verbose": false,
44221
+ ...GNU_INFORMATION
44222
+ };
44223
+ var INSTALL_OPTIONS = {
44224
+ "--backup": false,
44225
+ "--compare": false,
44226
+ "--context": false,
44227
+ "--debug": false,
44228
+ "--directory": false,
44229
+ "--group": true,
44230
+ "--mode": true,
44231
+ "--no-target-directory": false,
44232
+ "--owner": true,
44233
+ "--preserve-context": false,
44234
+ "--preserve-timestamps": false,
44235
+ "--strip": false,
44236
+ "--strip-program": true,
44237
+ "--suffix": true,
44238
+ "--target-directory": true,
44239
+ "--verbose": false,
44240
+ ...GNU_INFORMATION
44241
+ };
44242
+ var LN_OPTIONS = {
44243
+ "--backup": false,
44244
+ "--directory": false,
44245
+ "--force": false,
44246
+ "--interactive": false,
44247
+ "--logical": false,
44248
+ "--no-dereference": false,
44249
+ "--no-target-directory": false,
44250
+ "--physical": false,
44251
+ "--relative": false,
44252
+ "--suffix": true,
44253
+ "--symbolic": false,
44254
+ "--target-directory": true,
44255
+ "--verbose": false,
44256
+ ...GNU_INFORMATION
44257
+ };
44258
+ function passesAnArgument(arg) {
44259
+ return arg.fields !== "none";
44260
+ }
44261
+ function writeArguments(rest, valued, long) {
44262
+ const names = Object.keys(long);
44263
+ const operands2 = [];
44264
+ const flags = new Set;
44265
+ let directory;
44266
+ let uncertain;
44267
+ for (let i = 0;i < rest.length; i += 1) {
44268
+ const arg = rest[i];
44269
+ const { text, whole } = readableWord(arg);
44270
+ if (whole && text === "--") {
44271
+ operands2.push(...rest.slice(i + 1).filter(passesAnArgument));
44272
+ break;
44273
+ }
44274
+ if (!whole || !text.startsWith("-") || text === "-") {
44275
+ if (!whole && mayBeSomeOption(text))
44276
+ uncertain = arg;
44277
+ if (passesAnArgument(arg))
44278
+ operands2.push(arg);
44279
+ continue;
44280
+ }
44281
+ let takesValue = false;
44282
+ let offset;
44283
+ if (text.startsWith("--")) {
44284
+ const option = longOption(text, names);
44285
+ if (option === undefined)
44286
+ continue;
44287
+ flags.add(option);
44288
+ takesValue = long[option] === true && !text.includes("=");
44289
+ if (option === "--target-directory")
44290
+ offset = text.indexOf("=") + 1 || text.length;
44291
+ } else {
44292
+ for (let index = 1;index < text.length; index += 1) {
44293
+ const letter = text.charAt(index);
44294
+ flags.add(letter);
44295
+ if (!valued.includes(letter))
44296
+ continue;
44297
+ takesValue = index === text.length - 1;
44298
+ if (letter === "t")
44299
+ offset = index + 1;
44300
+ break;
44301
+ }
44302
+ }
44303
+ const next = takesValue ? rest[i + 1] : undefined;
44304
+ if (takesValue)
44305
+ i += 1;
44306
+ if (offset === undefined)
44307
+ continue;
44308
+ const value = valueInWord(arg, offset);
44309
+ directory = value === undefined ? next : { text: arg.text, exp: value };
44310
+ }
44311
+ return { operands: operands2, flags, directory, uncertain };
44312
+ }
44313
+ function writeDestinations(found, site) {
44314
+ if (found.operands.length > 1 && (found.flags.has("T") || found.flags.has("--no-target-directory"))) {
44315
+ return [found.operands.at(-1)];
44316
+ }
44317
+ if (found.directory !== undefined && !found.uncertain)
44318
+ return [found.directory];
44319
+ if (found.operands.length < 2 || found.uncertain) {
44320
+ const word = found.uncertain?.text ?? found.operands.at(-1)?.text ?? "missing destination";
44321
+ throw new Refusal(site.snippet, site.line, `the guard cannot read a write destination from \`${word}\`; put \`--\` before operands, or use \`-T\` to name the last operand as the destination`);
44322
+ }
44323
+ return [found.operands.at(-1)];
44324
+ }
44325
+ function checkCopyContents(found, destinations, st, ctx, site) {
44326
+ const recursive = found.uncertain || ["r", "R", "a", "--recursive", "--archive"].some((flag) => found.flags.has(flag));
44327
+ const direct = found.flags.has("T") || found.flags.has("--no-target-directory");
44328
+ for (const destination of destinations) {
44329
+ const text = literalText(destination.exp);
44330
+ if (text === undefined || !path4.isAbsolute(text) && st.cwd === undefined)
44331
+ continue;
44332
+ const dir = realExisting(path4.resolve(st.cwd ?? "/", text));
44333
+ if (!statSync(dir, { throwIfNoEntry: false })?.isDirectory())
44334
+ continue;
44335
+ for (const source of found.operands) {
44336
+ if (destinations.includes(source))
44337
+ continue;
44338
+ const name = literalText(source.exp);
44339
+ const from = name === undefined ? undefined : path4.resolve(st.cwd ?? "/", name);
44340
+ const knownTree = from !== undefined && statSync(from, { throwIfNoEntry: false })?.isDirectory();
44341
+ const root = direct || name === undefined ? dir : path4.join(dir, found.flags.has("--parents") ? name : path4.basename(name));
44342
+ const pending = [root];
44343
+ while (pending.length > 0) {
44344
+ const target = pending.pop();
44345
+ checkTargets("cp", "overwrite", [{ text: name === undefined ? source.text : target, exp: [literal2(target)] }], WRITES_THROUGH_LINK, st, ctx, site);
44346
+ if (!recursive && !(name === undefined && target === root))
44347
+ continue;
44348
+ if (!lstatSync(target, { throwIfNoEntry: false })?.isDirectory())
44349
+ continue;
44350
+ const entries = opendirSync(target);
44351
+ try {
44352
+ for (let entry = entries.readSync();entry !== null; entry = entries.readSync()) {
44353
+ if (++ctx.steps.count > MAX_WALK_STEPS) {
44354
+ throw new Refusal(site.snippet, site.line, WALK_LIMIT);
44355
+ }
44356
+ const child = path4.join(target, entry.name);
44357
+ if (knownTree && !existsSync(path4.join(from, path4.relative(root, child))))
44358
+ continue;
44359
+ pending.push(child);
44360
+ }
44361
+ } finally {
44362
+ entries.closeSync();
44363
+ }
44364
+ }
44365
+ }
44366
+ }
44367
+ }
44368
+ var SED_OPTIONS = {
44369
+ "--debug": false,
44370
+ "--expression": true,
44371
+ "--file": true,
44372
+ "--follow-symlinks": false,
44373
+ "--in-place": false,
44374
+ "--line-length": true,
44375
+ "--null-data": false,
44376
+ "--posix": false,
44377
+ "--quiet": false,
44378
+ "--regexp-extended": false,
44379
+ "--sandbox": false,
44380
+ "--separate": false,
44381
+ "--silent": false,
44382
+ "--unbuffered": false,
44383
+ ...GNU_INFORMATION
44384
+ };
44385
+ function sedWrites(list) {
44386
+ const names = Object.keys(SED_OPTIONS);
44387
+ let inPlace = false;
44388
+ let follow = false;
44389
+ let expression = false;
44390
+ let splitOption = false;
44391
+ let ended = false;
44392
+ let reopened = false;
44393
+ let takesNext = false;
44394
+ const unread = [];
44395
+ const operands2 = [];
44396
+ for (let i = 0;i < list.length; i += 1) {
44397
+ const arg = list[i];
44398
+ const { text, whole } = readableWord(arg);
44399
+ const afterUnread = takesNext;
44400
+ takesNext = false;
44401
+ const several = arg.fields === "unknown";
44402
+ if (ended) {
44403
+ operands2.push({ arg, option: false, several });
44404
+ continue;
44405
+ }
44406
+ if (whole && text === "--") {
44407
+ if (reopened)
44408
+ operands2.push({ arg, option: false, several: false });
44409
+ if (afterUnread)
44410
+ reopened = true;
44411
+ else
44412
+ ended = true;
44413
+ continue;
44414
+ }
44415
+ if (several) {
44416
+ splitOption = true;
44417
+ takesNext = true;
44418
+ operands2.push({ arg, option: true, several });
44419
+ continue;
44420
+ }
44421
+ if (!text.startsWith("-") || whole && text === "-") {
44422
+ const option = !whole && text === "";
44423
+ if (option) {
44424
+ unread.push(arg);
44425
+ takesNext = true;
44426
+ }
44427
+ operands2.push({ arg, option, several });
44428
+ continue;
44429
+ }
44430
+ if (reopened)
44431
+ operands2.push({ arg, option: false, several });
44432
+ if (text.startsWith("--")) {
44433
+ const option = longOption(text, names);
44434
+ if (whole && !reopened && (option === "--help" || option === "--version")) {
44435
+ return { replaced: [], followed: [], inPlace: false, inferred: false };
44436
+ }
44437
+ if (option === "--in-place")
44438
+ inPlace = true;
44439
+ if (option === "--follow-symlinks")
44440
+ follow = true;
44441
+ if (option === "--expression" || option === "--file")
44442
+ expression = true;
44443
+ if (option === undefined && !whole)
44444
+ unread.push(arg);
44445
+ if ((option === undefined ? !whole : SED_OPTIONS[option]) && !text.includes("=")) {
44446
+ if (whole && !reopened)
44447
+ i += 1;
44448
+ else
44449
+ takesNext = true;
44450
+ }
44451
+ continue;
44452
+ }
44453
+ let settled = false;
44454
+ for (const [offset, letter] of [...text.slice(1)].entries()) {
44455
+ if (letter === "i") {
44456
+ inPlace = true;
44457
+ settled = true;
44458
+ break;
44459
+ }
44460
+ if ("efl".includes(letter)) {
44461
+ if (letter !== "l")
44462
+ expression = true;
44463
+ if (offset === text.length - 2) {
44464
+ if (whole && !reopened)
44465
+ i += 1;
44466
+ else
44467
+ takesNext = true;
44468
+ }
44469
+ settled = true;
44470
+ break;
44471
+ }
44472
+ }
44473
+ if (!settled && !whole) {
44474
+ unread.push(arg);
44475
+ takesNext = true;
44476
+ }
44477
+ }
44478
+ const unsettled = (settled) => settled ? 0 : 1;
44479
+ const replaced = [];
44480
+ const followed = [];
44481
+ let before = false;
44482
+ for (const operand of operands2) {
44483
+ const maybe = operand.option && !operand.several;
44484
+ const others = unread.length - (maybe ? 1 : 0);
44485
+ const needed = unsettled(inPlace || splitOption) + unsettled(expression || splitOption || operand.several || before);
44486
+ if (others >= needed + unsettled(follow || splitOption))
44487
+ followed.push(operand.arg);
44488
+ else if (others >= needed)
44489
+ replaced.push(operand.arg);
44490
+ if (!maybe)
44491
+ before = true;
44492
+ }
44493
+ return {
44494
+ replaced,
44495
+ followed,
44496
+ inPlace: inPlace || splitOption || unread.length > 0,
44497
+ inferred: !inPlace
44498
+ };
44499
+ }
43884
44500
  function checkRecursiveMode(program, list, st, ctx, site) {
43885
44501
  const flags = program === "chmod" ? "cfvR" : "cfvRhHLP";
43886
44502
  const found = [];
@@ -43907,11 +44523,20 @@ function checkRecursiveMode(program, list, st, ctx, site) {
43907
44523
  }
43908
44524
  found.push(arg);
43909
44525
  }
43910
- if (!recursive)
43911
- return;
43912
- const targets = reference ? found : found.slice(1);
43913
44526
  const verb = program === "chmod" ? "recursively change the mode of" : "recursively change the owner of";
43914
- checkTargets(`${program} -R`, verb, targets, true, st, ctx, site);
44527
+ if (recursive) {
44528
+ checkTargets(`${program} -R`, verb, reference ? found : found.slice(1), ACTS_THROUGH_LINK, st, ctx, site);
44529
+ return;
44530
+ }
44531
+ for (const [index, arg] of found.entries()) {
44532
+ if (!mayBeOption(arg, (text) => /^-[A-Za-z]*R/.test(text)))
44533
+ continue;
44534
+ const others = arg.fields === "unknown" ? found : found.filter((_, at) => at !== index);
44535
+ const targets = reference || arg.fields === "unknown" ? others : others.slice(1);
44536
+ if (targets.length === 0)
44537
+ continue;
44538
+ checkTargets(`${program} -R`, verb, targets, ACTS_THROUGH_LINK, st, ctx, site);
44539
+ }
43915
44540
  }
43916
44541
  function checkFind(list, st, ctx, site) {
43917
44542
  let i = 0;
@@ -43926,17 +44551,27 @@ function checkFind(list, st, ctx, site) {
43926
44551
  break;
43927
44552
  }
43928
44553
  const starts = [];
44554
+ let rootsBeforePredicate;
43929
44555
  for (;i < list.length; i += 1) {
43930
- const text = literalText(list[i]?.exp);
44556
+ const arg = list[i];
44557
+ const text = literalText(arg.exp);
43931
44558
  if (text !== undefined && (text.startsWith("-") || text === "(" || text === "!" || text === ")"))
43932
44559
  break;
43933
- starts.push(list[i]);
44560
+ if (rootsBeforePredicate === undefined && mayBeOption(arg, (option) => option.startsWith("-"))) {
44561
+ rootsBeforePredicate = [...starts];
44562
+ }
44563
+ starts.push(arg);
43934
44564
  }
43935
44565
  const targets = starts.length > 0 ? starts : [{ text: ".", exp: [literal2(".")] }];
43936
44566
  const expression = list.slice(i).map((arg) => literalText(arg.exp));
43937
44567
  let action;
43938
44568
  let shell;
44569
+ let consumedByPredicate = false;
43939
44570
  for (const [index, word] of expression.entries()) {
44571
+ const isValue = consumedByPredicate;
44572
+ consumedByPredicate = word !== undefined && FIND_VALUED_PREDICATES.has(word);
44573
+ if (word === undefined && !isValue)
44574
+ action ??= "find with a predicate the guard cannot read";
43940
44575
  if (word === "-delete")
43941
44576
  action = "find -delete";
43942
44577
  if (word !== "-exec" && word !== "-execdir" && word !== "-ok" && word !== "-okdir")
@@ -43946,16 +44581,22 @@ function checkFind(list, st, ctx, site) {
43946
44581
  continue;
43947
44582
  const inner = unwrap2(list.slice(i + index + 1, i + end), st, ctx, site);
43948
44583
  const program = path4.basename(literalText(inner.argv[0]?.exp) ?? "");
43949
- if (FILE_COMMANDS.has(program) || program === "xargs")
44584
+ if (FILE_COMMANDS.has(program) || program === "xargs" || program === "sed" && sedWrites(inner.argv.slice(1)).inPlace) {
43950
44585
  action = `find ${word} ${program}`;
44586
+ }
43951
44587
  if (SHELLS.has(program)) {
43952
44588
  action = `find ${word} ${program}`;
43953
44589
  shell = inner.argv;
43954
44590
  }
43955
44591
  }
43956
- if (action === undefined)
44592
+ if (action === undefined) {
44593
+ if (rootsBeforePredicate === undefined)
44594
+ return;
44595
+ const under = rootsBeforePredicate.length > 0 ? rootsBeforePredicate : [{ text: ".", exp: [literal2(".")] }];
44596
+ checkTargets("find with a root the guard cannot read, which may be a predicate", "delete or change what it finds under", under, ACTS_ON_LINK, st, ctx, site);
43957
44597
  return;
43958
- checkTargets(action, "delete or change what it finds under", targets, false, st, ctx, site);
44598
+ }
44599
+ checkTargets(action, "delete or change what it finds under", targets, ACTS_ON_LINK, st, ctx, site);
43959
44600
  if (shell === undefined)
43960
44601
  return;
43961
44602
  const found = targets.length === 1 ? targets[0]?.exp ?? [] : [unknown2("a path `find` found")];
@@ -43993,7 +44634,7 @@ function checkXargs(list, st, ctx, site) {
43993
44634
  };
43994
44635
  });
43995
44636
  const program = path4.basename(literalText(argv[0]?.exp) ?? "");
43996
- if (FILE_COMMANDS.has(program) || SIGNAL_COMMANDS.has(program)) {
44637
+ if (FILE_COMMANDS.has(program) || SIGNAL_COMMANDS.has(program) || program === "sed" && sedWrites(argv.slice(1)).inPlace) {
43997
44638
  throw new Refusal(site.snippet, site.line, `\`xargs ${program}\` takes its targets from standard input, which the guard cannot see; name ` + `the paths or pids on the command line (or use \`find <dir> -delete\`) so it can check them`);
43998
44639
  }
43999
44640
  const overlay = new Map;
@@ -44231,7 +44872,7 @@ function runInterpreter(base, language, list, invocation, st, ctx) {
44231
44872
  for (;i < words.length; i += 1) {
44232
44873
  const word = words[i];
44233
44874
  if (word === undefined)
44234
- return;
44875
+ continue;
44235
44876
  if (codeFlags.includes(word)) {
44236
44877
  runCode(language, list[i + 1], invocation, st, ctx, site);
44237
44878
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "5.17.2",
3
+ "version": "5.17.4",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [