169 lines · 5.1 KB · TypeScript
RawDownload
type CommandWrapper = "env" | "sudo"; const COMMAND_WRAPPER_OPTIONS_WITH_VALUE: Record<CommandWrapper, ReadonlySet<string>> = {  env: new Set(["-C", "--chdir", "-S", "--split-string", "-u", "--unset"]),  sudo: new Set(["-C", "--close-from", "-D", "--chdir", "-g", "--group", "-u", "--user"]),}; const COMMAND_WRAPPER_FLAGS: Record<CommandWrapper, ReadonlySet<string>> = {  env: new Set(["-0", "--null", "-i", "--ignore-environment", "--debug", "-v"]),  sudo: new Set(["-A", "--askpass", "-b", "--background", "-E", "-H", "-i", "-n", "-S"]),}; function tokenizeShellCommand(command: string): string[] | null {  const input = command.trim();  const tokens: string[] = [];  let current = "";  let quote: '"' | "'" | null = null;  let escaping = false;  let substitutionDepth = 0;  let tokenStarted = false;   for (let index = 0; index < input.length; index += 1) {    const character = input[index]!;    if (escaping) {      current += character;      escaping = false;      tokenStarted = true;      continue;    }    if (character === "\\" && quote !== "'") {      const nextCharacter = input[index + 1];      const isWindowsDrivePath = quote === null && /^[A-Za-z]:/.test(current);      if (        (quote === '"' || isWindowsDrivePath) &&        nextCharacter !== undefined &&        nextCharacter !== '"' &&        nextCharacter !== "\\" &&        nextCharacter !== "$" &&        nextCharacter !== "`" &&        nextCharacter !== "\n"      ) {        current += character;        tokenStarted = true;        continue;      }      escaping = true;      tokenStarted = true;      continue;    }    if (quote !== null) {      if (character === quote) {        quote = null;      } else {        current += character;      }      tokenStarted = true;      continue;    }    if (character === "$" && input[index + 1] === "(") {      current += "$(";      substitutionDepth += 1;      tokenStarted = true;      index += 1;      continue;    }    if (character === ")" && substitutionDepth > 0) {      current += character;      substitutionDepth -= 1;      tokenStarted = true;      continue;    }    if (character === '"' || character === "'") {      quote = character;      tokenStarted = true;      continue;    }    if (/\s/u.test(character)) {      if (substitutionDepth > 0) {        current += character;        tokenStarted = true;        continue;      }      if (tokenStarted) {        tokens.push(current);        current = "";        tokenStarted = false;      }      continue;    }    current += character;    tokenStarted = true;  }   if (quote !== null || escaping || substitutionDepth > 0) return null;  if (tokenStarted) tokens.push(current);  return tokens;} export function commandProgramName(command: string, depth = 0): string | null {  if (depth >= 8) return null;  const tokens = tokenizeShellCommand(command);  if (tokens === null) return null;  let index = 0;  let wrapper: CommandWrapper | null = null;   while (index < tokens.length) {    const token = tokens[index];    if (!token) return null;    if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) {      index += 1;      continue;    }    const tokenProgram = token.split(/[\\/]/).at(-1);    if (tokenProgram === "env" || tokenProgram === "sudo") {      wrapper = tokenProgram;      index += 1;      continue;    }    if (wrapper !== null && token === "--") {      wrapper = null;      index += 1;      continue;    }    if (wrapper !== null && token.startsWith("-")) {      if (wrapper === "env" && (token === "-S" || token === "--split-string")) {        const splitCommand = tokens[index + 1];        return splitCommand ? commandProgramName(splitCommand, depth + 1) : null;      }      if (wrapper === "env" && token.startsWith("--split-string=")) {        return commandProgramName(token.slice("--split-string=".length), depth + 1);      }      if (COMMAND_WRAPPER_OPTIONS_WITH_VALUE[wrapper].has(token)) {        if (tokens[index + 1] === undefined) return null;        index += 2;        continue;      }      if (COMMAND_WRAPPER_FLAGS[wrapper].has(token)) {        index += 1;        continue;      }      const equalsIndex = token.indexOf("=");      if (token.startsWith("--") && equalsIndex > 2) {        if (!COMMAND_WRAPPER_OPTIONS_WITH_VALUE[wrapper].has(token.slice(0, equalsIndex))) {          return null;        }        index += 1;        continue;      }      if (/^-[A-Za-z].+/.test(token) && !token.startsWith("--")) {        let consumesNextToken = false;        for (const [optionIndex, option] of token.slice(1).split("").entries()) {          const shortOption = `-${option}`;          if (COMMAND_WRAPPER_OPTIONS_WITH_VALUE[wrapper].has(shortOption)) {            consumesNextToken = optionIndex === token.length - 2;            break;          }          if (!COMMAND_WRAPPER_FLAGS[wrapper].has(shortOption)) return null;        }        if (consumesNextToken && tokens[index + 1] === undefined) return null;        index += consumesNextToken ? 2 : 1;        continue;      }      return null;    }    return token.split(/[\\/]/).at(-1) || null;  }   return null;}