306 lines
11 KiB
C++
306 lines
11 KiB
C++
|
/*
|
|||
|
* 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 <https://www.gnu.org/licenses/>.
|
|||
|
*/
|
|||
|
|
|||
|
#include "utils/escape.h"
|
|||
|
|
|||
|
#include <QSet>
|
|||
|
#include <QRegularExpression>
|
|||
|
|
|||
|
#ifdef _MSC_VER
|
|||
|
#define __builtin_unreachable() (__assume(0))
|
|||
|
#endif
|
|||
|
|
|||
|
static QString contextualBackslashEscaping(const QString &arg, const QSet<QChar> &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<QString> 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<QChar> 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<QChar> needsEscaping{'#', ' '};
|
|||
|
QString result = contextualBackslashEscaping(filename, needsEscaping);
|
|||
|
return result.replace('$', "$$");
|
|||
|
}
|
|||
|
|
|||
|
QString escapeFilenameForMakefileTarget(const QString &filename)
|
|||
|
{
|
|||
|
static QSet<QChar> needsEscaping{'#', ' ', ':', '%'};
|
|||
|
QString result = contextualBackslashEscaping(filename, needsEscaping);
|
|||
|
return result.replace('$', "$$");
|
|||
|
}
|
|||
|
|
|||
|
QString escapeFilenameForMakefilePrerequisite(const QString &filename)
|
|||
|
{
|
|||
|
static QSet<QChar> 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(' ');
|
|||
|
}
|