Fix parsing of command lines with nested quotes and empty parameters

This commit is contained in:
baldurk
2020-06-29 19:08:39 +01:00
parent 113c6f35ae
commit f238428d81
+425 -124
View File
@@ -402,20 +402,120 @@ void Process::ApplyEnvironmentModification()
modifications.clear();
}
static void CleanupStringArray(char **arr, char **invalid)
static void CleanupStringArray(char **arr)
{
if(arr != invalid)
{
char **arr_delete = arr;
char **arr_delete = arr;
while(*arr)
while(*arr)
{
delete[] * arr;
arr++;
}
delete[] arr_delete;
}
static rdcarray<rdcstr> ParseCommandLine(const rdcstr &appName, const char *cmdLine)
{
// argv[0] is the application name, by convention
rdcarray<rdcstr> argv = {appName};
const char *c = cmdLine;
// parse command line into argv[], similar to how bash would
if(cmdLine)
{
rdcstr a;
bool haveArg = false;
bool dquot = false, squot = false; // are we inside ''s or ""s
while(*c)
{
delete[] * arr;
arr++;
if(!dquot && !squot && (*c == ' ' || *c == '\t'))
{
if(!a.empty() || haveArg)
{
// if we've fetched some number of non-space characters
argv.push_back(a);
}
a = "";
haveArg = false;
}
// if we're not quoting at all and see a quote, enter that quote mode
else if(!dquot && !squot && *c == '"')
{
dquot = true;
haveArg = true;
}
else if(!dquot && !squot && *c == '\'')
{
squot = true;
haveArg = true;
}
// exit quoting if we see the matching quote (we skip over escapes separately)
else if(dquot && *c == '"')
{
dquot = false;
}
else if(squot && *c == '\'')
{
squot = false;
}
else if(squot)
{
// single quotes don't escape, just copy literally until we leave single quote mode
a.push_back(*c);
}
else if(dquot)
{
// handle escaping
if(*c == '\\')
{
c++;
if(*c)
{
a.push_back(*c);
}
else
{
RDCERR("Malformed command line:\n%s", cmdLine);
return {};
}
}
else
{
a.push_back(*c);
}
}
else
{
a.push_back(*c);
}
c++;
}
delete[] arr_delete;
if(!a.empty() || haveArg)
{
// if we've fetched some number of non-space characters
argv.push_back(a);
}
if(squot || dquot)
{
RDCERR("Malformed command line\n%s", cmdLine);
return {};
}
}
RDCLOG("$ ./a.out %s", cmdLine);
for(size_t i = 0; i < argv.size(); i++)
{
RDCLOG("%d: '%s'", i, argv[i].c_str());
}
return argv;
}
static pid_t RunProcess(const char *app, const char *workingDir, const char *cmdLine, char **envp,
@@ -447,122 +547,15 @@ static pid_t RunProcess(const char *app, const char *workingDir, const char *cmd
appName = shellExpand(appName);
workDir = shellExpand(workDir);
// it is safe to use app directly as execve never modifies argv
char *emptyargv[] = {(char *)appName.c_str(), NULL};
char **argv = emptyargv;
rdcarray<rdcstr> argvList = ParseCommandLine(appName, cmdLine);
const char *c = cmdLine;
if(argvList.empty())
return 0;
// parse command line into argv[], similar to how bash would
if(cmdLine)
{
int argc = 1;
// get a rough upper bound on the number of arguments
while(*c)
{
if(*c == ' ' || *c == '\t')
argc++;
c++;
}
argv = new char *[argc + 2];
memset(argv, 0, (argc + 2) * sizeof(char *));
c = cmdLine;
rdcstr a;
argc = 0; // current argument we're fetching
// argv[0] is the application name, by convention
size_t len = appName.length() + 1;
argv[argc] = new char[len];
strcpy(argv[argc], appName.c_str());
argc++;
bool dquot = false, squot = false; // are we inside ''s or ""s
while(*c)
{
if(!dquot && !squot && (*c == ' ' || *c == '\t'))
{
if(!a.empty())
{
// if we've fetched some number of non-space characters
argv[argc] = new char[a.length() + 1];
memcpy(argv[argc], a.c_str(), a.length() + 1);
argc++;
}
a = "";
}
else if(!dquot && *c == '"')
{
dquot = true;
}
else if(!squot && *c == '\'')
{
squot = true;
}
else if(dquot && *c == '"')
{
dquot = false;
}
else if(squot && *c == '\'')
{
squot = false;
}
else if(squot)
{
// single quotes don't escape, just copy literally until we leave single quote mode
a.push_back(*c);
}
else if(dquot)
{
// handle escaping
if(*c == '\\')
{
c++;
if(*c)
{
a.push_back(*c);
}
else
{
CleanupStringArray(argv, emptyargv);
RDCERR("Malformed command line:\n%s", cmdLine);
return 0;
}
}
else
{
a.push_back(*c);
}
}
else
{
a.push_back(*c);
}
c++;
}
if(!a.empty())
{
// if we've fetched some number of non-space characters
argv[argc] = new char[a.length() + 1];
memcpy(argv[argc], a.c_str(), a.length() + 1);
argc++;
}
if(squot || dquot)
{
CleanupStringArray(argv, emptyargv);
RDCERR("Malformed command line\n%s", cmdLine);
return 0;
}
}
char **argv = new char *[argvList.size() + 1];
for(size_t i = 0; i < argvList.size(); i++)
argv[i] = argvList[i].data();
argv[argvList.size()] = NULL;
const rdcstr appPath(GetAbsoluteAppPathFromName(appName));
@@ -634,7 +627,7 @@ static pid_t RunProcess(const char *app, const char *workingDir, const char *cmd
close(stderrPipe[1]);
}
CleanupStringArray(argv, emptyargv);
delete[] argv;
return childPid;
}
@@ -862,7 +855,7 @@ rdcpair<ReplayStatus, uint32_t> Process::LaunchAndInjectIntoProcess(
}
}
CleanupStringArray(envp, NULL);
CleanupStringArray(envp);
return {ret == 0 ? ReplayStatus::InjectionFailed : ReplayStatus::Succeeded, (uint32_t)ret};
}
@@ -924,6 +917,314 @@ void Process::Shutdown()
#include "catch/catch.hpp"
TEST_CASE("Test command line parsing", "[osspecific]")
{
rdcarray<rdcstr> args;
SECTION("NULL command line")
{
args = ParseCommandLine("app", NULL);
REQUIRE(args.size() == 1);
CHECK(args[0] == "app");
}
SECTION("empty command line")
{
args = ParseCommandLine("app", "");
REQUIRE(args.size() == 1);
CHECK(args[0] == "app");
args = ParseCommandLine("app", " ");
REQUIRE(args.size() == 1);
CHECK(args[0] == "app");
args = ParseCommandLine("app", " \t \t ");
REQUIRE(args.size() == 1);
CHECK(args[0] == "app");
}
SECTION("whitespace command line")
{
args = ParseCommandLine("app", "' '");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == " ");
args = ParseCommandLine("app", " ' '");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == " ");
args = ParseCommandLine("app", " ' ' ");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == " ");
args = ParseCommandLine("app", " \" \" ");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == " ");
}
SECTION("a single parameter")
{
args = ParseCommandLine("app", "--foo");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "--foo");
args = ParseCommandLine("app", "--bar");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "--bar");
args = ParseCommandLine("app", "/a/path/to/somewhere");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "/a/path/to/somewhere");
}
SECTION("multiple parameters")
{
args = ParseCommandLine("app", "--foo --bar ");
REQUIRE(args.size() == 3);
CHECK(args[0] == "app");
CHECK(args[1] == "--foo");
CHECK(args[2] == "--bar");
args = ParseCommandLine("app", " --qux \t --asdf");
REQUIRE(args.size() == 3);
CHECK(args[0] == "app");
CHECK(args[1] == "--qux");
CHECK(args[2] == "--asdf");
args = ParseCommandLine("app", "--path /a/path/to/somewhere --many --param a b c d ");
REQUIRE(args.size() == 9);
CHECK(args[0] == "app");
CHECK(args[1] == "--path");
CHECK(args[2] == "/a/path/to/somewhere");
CHECK(args[3] == "--many");
CHECK(args[4] == "--param");
CHECK(args[5] == "a");
CHECK(args[6] == "b");
CHECK(args[7] == "c");
CHECK(args[8] == "d");
}
SECTION("parameters with single quotes")
{
args = ParseCommandLine("app", "'single quoted single parameter'");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "single quoted single parameter");
args = ParseCommandLine("app", " 'single quoted single parameter' ");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "single quoted single parameter");
args = ParseCommandLine("app", " 'single quoted \t\tsingle parameter' ");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "single quoted \t\tsingle parameter");
args = ParseCommandLine("app", " --thing='single quoted single parameter' ");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "--thing=single quoted single parameter");
args = ParseCommandLine("app", " 'quoted string with \"double quotes inside\" it' ");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "quoted string with \"double quotes inside\" it");
args =
ParseCommandLine("app", " --multiple --params 'single quoted parameter' --with --quotes ");
REQUIRE(args.size() == 6);
CHECK(args[0] == "app");
CHECK(args[1] == "--multiple");
CHECK(args[2] == "--params");
CHECK(args[3] == "single quoted parameter");
CHECK(args[4] == "--with");
CHECK(args[5] == "--quotes");
args = ParseCommandLine("app", "--explicit '' --empty");
REQUIRE(args.size() == 4);
CHECK(args[0] == "app");
CHECK(args[1] == "--explicit");
CHECK(args[2] == "");
CHECK(args[3] == "--empty");
args = ParseCommandLine("app", "--explicit ' ' --spaces");
REQUIRE(args.size() == 4);
CHECK(args[0] == "app");
CHECK(args[1] == "--explicit");
CHECK(args[2] == " ");
CHECK(args[3] == "--spaces");
args = ParseCommandLine("app", "--explicit ''");
REQUIRE(args.size() == 3);
CHECK(args[0] == "app");
CHECK(args[1] == "--explicit");
CHECK(args[2] == "");
args = ParseCommandLine("app", "--explicit ' '");
REQUIRE(args.size() == 3);
CHECK(args[0] == "app");
CHECK(args[1] == "--explicit");
CHECK(args[2] == " ");
}
SECTION("parameters with double quotes")
{
args = ParseCommandLine("app", "\"double quoted single parameter\"");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "double quoted single parameter");
args = ParseCommandLine("app", " \"double quoted single parameter\" ");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "double quoted single parameter");
args = ParseCommandLine("app", " \"double quoted \t\tsingle parameter\" ");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "double quoted \t\tsingle parameter");
args = ParseCommandLine("app", " --thing=\"double quoted single parameter\" ");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "--thing=double quoted single parameter");
args = ParseCommandLine("app", " \"quoted string with \\\"double quotes inside\\\" it\" ");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "quoted string with \"double quotes inside\" it");
args = ParseCommandLine("app", " \"string's contents has a quoted quote\" ");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "string's contents has a quoted quote");
args =
ParseCommandLine("app", " --multiple --params 'double quoted parameter' --with --quotes ");
REQUIRE(args.size() == 6);
CHECK(args[0] == "app");
CHECK(args[1] == "--multiple");
CHECK(args[2] == "--params");
CHECK(args[3] == "double quoted parameter");
CHECK(args[4] == "--with");
CHECK(args[5] == "--quotes");
args = ParseCommandLine("app", "--explicit \"\" --empty");
REQUIRE(args.size() == 4);
CHECK(args[0] == "app");
CHECK(args[1] == "--explicit");
CHECK(args[2] == "");
CHECK(args[3] == "--empty");
args = ParseCommandLine("app", "--explicit \" \" --spaces");
REQUIRE(args.size() == 4);
CHECK(args[0] == "app");
CHECK(args[1] == "--explicit");
CHECK(args[2] == " ");
CHECK(args[3] == "--spaces");
args = ParseCommandLine("app", "--explicit \"\"");
REQUIRE(args.size() == 3);
CHECK(args[0] == "app");
CHECK(args[1] == "--explicit");
CHECK(args[2] == "");
args = ParseCommandLine("app", "--explicit \" \"");
REQUIRE(args.size() == 3);
CHECK(args[0] == "app");
CHECK(args[1] == "--explicit");
CHECK(args[2] == " ");
}
SECTION("concatenated quotes")
{
args = ParseCommandLine("app", "'foo''bar''blah'");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "foobarblah");
args = ParseCommandLine("app", "\"foo\"\"bar\"\"blah\"");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "foobarblah");
args = ParseCommandLine("app", "\"foo\"'bar'\"blah\"");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "foobarblah");
args = ParseCommandLine("app", "\"foo\"'bar'\"blah\"");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "foobarblah");
args = ParseCommandLine("app", "foo'bar'blah");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "foobarblah");
args = ParseCommandLine("app", "foo\"bar\"blah");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "foobarblah");
args = ParseCommandLine("app", "\"string with spaces\"' and other string'");
REQUIRE(args.size() == 2);
CHECK(args[0] == "app");
CHECK(args[1] == "string with spaces and other string");
}
}
TEST_CASE("Test PID Node list handling", "[osspecific]")
{
PIDNode *a = new PIDNode;