/* * Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "utils/escape.h" #include #include #ifdef _MSC_VER #define __builtin_unreachable() (__assume(0)) #endif static QString contextualBackslashEscaping(const QString &arg, const QSet &needsEscaping, bool escapeFinal = true) { QString result; for (auto it = arg.begin(); ; ++it) { int nBackSlash = 0; while (it != arg.end() && *it == '\\') { ++it; ++nBackSlash; } if (it == arg.end()) { if (escapeFinal) { // escape all backslashes, but leave following character unescaped // (terminating double quote for CreateProcess, or LF or space in makefile) result.append(QString('\\').repeated(nBackSlash * 2)); } else { // leave all backslashes unescaped, and add a space to protect LF result.append(QString('\\').repeated(nBackSlash)); if (nBackSlash > 0) result.push_back(' '); } break; } else if (needsEscaping.contains(*it)) { // escape all backslashes and the following character result.append(QString('\\').repeated(nBackSlash * 2 + 1)); result.push_back(*it); } else { // backslashes aren't special here result.append(QString('\\').repeated(nBackSlash)); result.push_back(*it); } } return result; } QString escapeArgumentImplBourneAgainShellPretty(const QString &arg, bool isFirstArg) { // ref. https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html if (arg.isEmpty()) return R"("")"; /* POSIX say the following reserved words (may) have special meaning: * !, {, }, case, do, done, elif, else, esac, fi, for, if, in, then, until, while, * [[, ]], function, select, * only if used as the _first word_ of a command (or somewhere we dot not care). */ const static QSet reservedWord{ "!", "{", "}", "case", "do", "done", "elif", "else", "esac", "fi", "for", "if", "in", "then", "until", "while", "[[", "]]", "function", "select", }; if (isFirstArg && reservedWord.contains(arg)) return QString(R"("%1")").arg(arg); /* POSIX say “shall quote”: * '|', '&', ';', '<', '>', '(', ')', '$', '`', '\\', '"', '\'', ' ', '\t', '\n'; * and “may need to be quoted”: * '*', '?', '[', '#', '~', '=', '%'. * among which “may need to be quoted” there are 4 kinds: * - wildcards '*', '?', '[' are “danger anywhere” (handle it as if “shall quote”); * - comment '#', home '~', is “danger at first char in any word”; * - (environment) variable '=' is “danger at any char in first word”; * - foreground '%' is “danger at first char in first word”. * although not mentioned by POSIX, bash’s brace expansion '{', '}' are also “danger anywhere”. */ static QRegularExpression doubleQuotingDangerChars(R"([`$\\"])"); static QRegularExpression otherDangerChars(R"([|&;<>() \t\n*?\[\{\}])"); bool isDoubleQuotingDanger = arg.contains(doubleQuotingDangerChars); bool isSingleQuotingDanger = arg.contains('\''); bool isDangerAnyChar = isDoubleQuotingDanger || isSingleQuotingDanger || arg.contains(otherDangerChars); bool isDangerFirstChar = (arg[0] == '#') || (arg[0] == '~'); if (isFirstArg) { isDangerAnyChar = isDangerAnyChar || arg.contains('='); isDangerFirstChar = isDangerFirstChar || (arg[0] == '%'); } // a “safe” string if (!isDangerAnyChar && !isDangerFirstChar) return arg; // prefer more-common double quoting if (!isDoubleQuotingDanger) return QString(R"("%1")").arg(arg); // and then check the opportunity of single quoting if (!isSingleQuotingDanger) return QString("'%1'").arg(arg); // escaping is necessary // use double quoting since it’s less tricky QString result = "\""; for (auto ch : arg) { if (ch == '$' || ch == '`' || ch == '\\' || ch == '"') result.push_back('\\'); result.push_back(ch); } result.push_back('"'); return result; } QString escapeArgumentImplBourneAgainShellFast(QString arg) { /* 1. replace each single quote with `'\''`, which contains * - a single quote to close quoting, * - an escaped single quote representing the single quote itself, and * - a single quote to open quoting again. */ arg.replace('\'', R"('\'')"); /* 2. enclose the string with a pair of single quotes. */ return '\'' + arg + '\''; } QString escapeArgumentImplWindowsCreateProcess(const QString &arg, bool forceQuote) { // See https://stackoverflow.com/questions/31838469/how-do-i-convert-argv-to-lpcommandline-parameter-of-createprocess , // and https://learn.microsoft.com/en-gb/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way . static QRegularExpression needQuoting(R"([ \t\n\v"])"); if (!arg.isEmpty() && !forceQuote && !arg.contains(needQuoting)) return arg; return '"' + contextualBackslashEscaping(arg, {'"'}) + '"'; } QString escapeArgumentImplWindowsCommandPrompt(const QString &arg) { static QRegularExpression metaChars(R"([()%!^"<>&|])"); bool containsMeta = arg.contains(metaChars); if (containsMeta) { QString quoted = escapeArgumentImplWindowsCreateProcess(arg, false); quoted.replace('^', "^^"); // handle itself first quoted.replace('(', "^("); quoted.replace(')', "^)"); quoted.replace('%', "^%"); quoted.replace('!', "^!"); quoted.replace('"', "^\""); quoted.replace('<', "^<"); quoted.replace('>', "^>"); quoted.replace('&', "^&"); quoted.replace('|', "^|"); return quoted; } else return escapeArgumentImplWindowsCreateProcess(arg, false); } QString escapeArgument(const QString &arg, bool isFirstArg, EscapeArgumentRule rule) { switch (rule) { case EscapeArgumentRule::BourneAgainShellPretty: return escapeArgumentImplBourneAgainShellPretty(arg, isFirstArg); case EscapeArgumentRule::BourneAgainShellFast: return escapeArgumentImplBourneAgainShellFast(arg); case EscapeArgumentRule::WindowsCreateProcess: return escapeArgumentImplWindowsCreateProcess(arg, false); case EscapeArgumentRule::WindowsCreateProcessForceQuote: return escapeArgumentImplWindowsCreateProcess(arg, true); case EscapeArgumentRule::WindowsCommandPrompt: return escapeArgumentImplWindowsCommandPrompt(arg); default: __builtin_unreachable(); } } EscapeArgumentRule platformShellEscapeArgumentRule() { #ifdef Q_OS_WIN return EscapeArgumentRule::WindowsCommandPrompt; #else return EscapeArgumentRule::BourneAgainShellPretty; #endif } QString escapeArgumentForPlatformShell(const QString &arg, bool isFirstArg) { return escapeArgument(arg, isFirstArg, platformShellEscapeArgumentRule()); } QString escapeCommandForPlatformShell(const QString &prog, const QStringList &args) { QStringList escapedArgs{escapeArgumentForPlatformShell(prog, true)}; for (int i = 0; i < args.size(); ++i) escapedArgs << escapeArgumentForPlatformShell(args[i], false); return escapedArgs.join(' '); } EscapeArgumentRule makefileRecipeEscapeArgumentRule() { #ifdef Q_OS_WIN /* Lord knows why. standard CreateProcess or CMD escaping: child.exe -c main'.c -o main'.o yielding: 0: [child.exe] 1: [-c] 2: [main.c -o main.o] that's not what we want. however, if CMD escaping a malformed argument child.exe -c main'.c -o main'.o ^"mal \^" ^& calc^" yielding: 0: [child.exe] 1: [-c] 2: [main'.c] 3: [-o] 4: [main'.o] 5: [mal " & calc] it works?!?!?! force-quoted CreateProcess escaping seems work on most cases. */ return EscapeArgumentRule::WindowsCreateProcessForceQuote; #else return EscapeArgumentRule::BourneAgainShellPretty; #endif } QString escapeArgumentForMakefileVariableValue(const QString &arg, bool isFirstArg) { static QSet needsMfEscaping = {'#'}; QString recipeEscaped = escapeArgument(arg, isFirstArg, makefileRecipeEscapeArgumentRule()); QString mfEscaped = contextualBackslashEscaping(recipeEscaped, needsMfEscaping); return mfEscaped.replace('$', "$$"); } QString escapeArgumentsForMakefileVariableValue(const QStringList &args) { QStringList escapedArgs; for (int i = 0; i < args.size(); ++i) escapedArgs << escapeArgumentForMakefileVariableValue(args[i], false); return escapedArgs.join(' '); } QString escapeFilenameForMakefileInclude(const QString &filename) { static QSet needsEscaping{'#', ' '}; QString result = contextualBackslashEscaping(filename, needsEscaping); return result.replace('$', "$$"); } QString escapeFilenameForMakefileTarget(const QString &filename) { static QSet needsEscaping{'#', ' ', ':', '%'}; QString result = contextualBackslashEscaping(filename, needsEscaping); return result.replace('$', "$$"); } QString escapeFilenameForMakefilePrerequisite(const QString &filename) { static QSet needsEscaping{'#', ' ', ':', '?', '*'}; QString result = contextualBackslashEscaping(filename, needsEscaping, false); return result.replace('$', "$$"); } QString escapeFilenamesForMakefilePrerequisite(const QStringList &filenames) { QStringList escapedFilenames; for (int i = 0; i < filenames.size(); ++i) escapedFilenames << escapeFilenameForMakefilePrerequisite(filenames[i]); return escapedFilenames.join(' '); } QString escapeArgumentForMakefileRecipe(const QString &arg, bool isFirstArg) { QString shellEscaped = escapeArgument(arg, isFirstArg, makefileRecipeEscapeArgumentRule()); return shellEscaped.replace('$', "$$"); } QString escapeArgumentForInputField(const QString &arg, bool isFirstArg) { return escapeArgument(arg, isFirstArg, EscapeArgumentRule::BourneAgainShellPretty); } QString escapeArgumentsForInputField(const QStringList &args) { QStringList escapedArgs; for (int i = 0; i < args.size(); ++i) escapedArgs << escapeArgumentForInputField(args[i], false); return escapedArgs.join(' '); }