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;}
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;
}