App-Ariza.git | runner/tests/ | test_cmdline.c
/* The argv[0] boundary, the quoting, and the child command line.
*
* This is the file that says what the runner is FOR. Each tail case
* below is an argument that a .cmd trampoline damages on the way to the
* app — '^' is cmd's escape character, '%x%' and '!x!' are its
* expansions, and a quoted argument is re-split by `%*` — and the
* expectation is always the same: the bytes after argv[0], unchanged. */
#include <stdlib.h>
#include "support.h"
struct tail_case {
const char *cmdline;
const char *tail;
const char *why;
};
/* The table is deliberately written as raw command lines, the way
* GetCommandLineW hands them over, rather than as argv arrays: the whole
* point is that the runner never turns one into the other. */
static const struct tail_case TAILS[] = {
{ "exampleapp.exe", "",
"no arguments at all" },
{ "exampleapp.exe foo bar", "foo bar",
"the ordinary case" },
{ "exampleapp.exe foo bar ", "foo bar ",
"spacing between and after arguments is the caller's, not ours" },
{ " exampleapp.exe foo", "foo",
"a command line may begin with whitespace" },
{ "exampleapp.exe\tfoo\tbar", "foo\tbar",
"a tab separates argv[0] as readily as a space" },
{ "\"C:\\Program Files\\App\\exampleapp.exe\" foo", "foo",
"a quoted argv[0] with spaces in it — the common Explorer case" },
{ "\"C:\\Program Files\\App\\exampleapp.exe\"", "",
"quoted, and no arguments" },
{ "\"C:\\dir\\exampleapp.exe\" --flag \"a b\"", "--flag \"a b\"",
"quotes in the tail are the caller's and stay exactly where they are" },
{ "\"C:\\dir\\prog\".exe foo", ".exe foo",
"a quoted argv[0] ends at the closing quote, and the runtime would"
" read '.exe' as the next argument — so we hand over the same" },
{ "\"C:\\dir\\unterminated foo bar", "",
"an unterminated quote swallows the line, exactly as the runtime"
" has it" },
{ "exampleapp.exe ^", "^",
"a caret: cmd's escape character, and nothing to us" },
{ "exampleapp.exe a^^b ^\"q^\"", "a^^b ^\"q^\"",
"carets in quantity, which is what a batch file cannot survive" },
{ "exampleapp.exe %PATH% %%x %", "%PATH% %%x %",
"percent signs are not expanded here; the shell already did or"
" did not" },
{ "exampleapp.exe !x! !!", "!x! !!",
"delayed-expansion syntax, likewise" },
{ "exampleapp.exe \"say \\\"hi\\\"\"", "\"say \\\"hi\\\"\"",
"an escaped quote inside a quoted argument survives intact" },
{ "exampleapp.exe C:\\dir\\", "C:\\dir\\",
"a trailing backslash" },
{ "exampleapp.exe \"C:\\dir\\\\\"", "\"C:\\dir\\\\\"",
"backslashes before a closing quote, which is the case every"
" hand-rolled re-quoter gets wrong" },
{ "exampleapp.exe --name=Caffè --emoji=☕", "--name=Caffè --emoji=☕",
"non-ASCII arguments pass through as units, not bytes to reinterpret" },
{ "", "",
"an empty command line" },
{ " ", "",
"whitespace only" },
};
static void test_tail(void)
{
size_t i;
for (i = 0; i < sizeof(TAILS) / sizeof(TAILS[0]); i++) {
const arz_char *line = t_u8(TAILS[i].cmdline);
const arz_char *got = arz_cmdline_tail(line);
if (!t_eq(got, TAILS[i].tail)) {
fprintf(stderr, "FAIL tail[%lu]: %s\n",
(unsigned long)i, TAILS[i].why);
fprintf(stderr, " cmdline: %s\n", TAILS[i].cmdline);
fprintf(stderr, " wanted: %s\n", TAILS[i].tail);
t_fails++;
}
t_checks++;
/* The tail is a pointer INTO the command line, never a copy:
* there is no re-encoding step for anything to be lost in. */
T_CHECK(got >= line && got <= line + arz_len(line));
}
T_CHECK(arz_cmdline_tail(NULL) == NULL);
}
struct quote_case {
const char *arg;
const char *quoted;
};
/* The runner quotes exactly two things — the interpreter and the
* script — and both are paths it built itself. The rules are still the
* C runtime's, because the child parses them back with the C runtime. */
static const struct quote_case QUOTES[] = {
{ "plain", "\"plain\"" },
{ "", "\"\"" },
{ "C:\\b\\rakudo\\bin\\raku.exe", "\"C:\\b\\rakudo\\bin\\raku.exe\"" },
{ "C:\\Program Files\\My App\\raku.exe",
"\"C:\\Program Files\\My App\\raku.exe\"" },
/* A trailing backslash sits immediately before the closing quote,
* where an undoubled one would escape the quote instead. */
{ "C:\\dir\\", "\"C:\\dir\\\\\"" },
{ "C:\\dir\\\\", "\"C:\\dir\\\\\\\\\"" },
{ "a\"b", "\"a\\\"b\"" },
{ "a\\\"b", "\"a\\\\\\\"b\"" },
{ "a\\b", "\"a\\b\"" },
{ "Caffè ☕", "\"Caffè ☕\"" },
};
static void test_quote(void)
{
size_t i;
for (i = 0; i < sizeof(QUOTES) / sizeof(QUOTES[0]); i++)
T_EQ_OWNED(arz_quote_arg(t_u8(QUOTES[i].arg)),
QUOTES[i].quoted);
T_EQ_OWNED(arz_quote_arg(NULL), "\"\"");
}
static void test_child_cmdline(void)
{
T_EQ_OWNED(arz_child_cmdline(t_s("C:\\b\\rakudo\\bin\\raku.exe"),
t_s("C:\\b\\site\\bin\\a.raku"), t_s("--flag \"a b\" ^%x%")),
"\"C:\\b\\rakudo\\bin\\raku.exe\" \"C:\\b\\site\\bin\\a.raku\""
" --flag \"a b\" ^%x%");
/* No trailing space when there are no arguments: a command line
* ending in one is harmless but shows up in every error message and
* every process listing. */
T_EQ_OWNED(arz_child_cmdline(t_s("C:\\b\\rakudo\\bin\\raku.exe"),
t_s("C:\\b\\site\\bin\\a.raku"), t_s("")),
"\"C:\\b\\rakudo\\bin\\raku.exe\" \"C:\\b\\site\\bin\\a.raku\"");
T_EQ_OWNED(arz_child_cmdline(t_s("C:\\b\\rakudo\\bin\\raku.exe"),
t_s("C:\\b\\site\\bin\\a.raku"), NULL),
"\"C:\\b\\rakudo\\bin\\raku.exe\" \"C:\\b\\site\\bin\\a.raku\"");
/* End to end: a raw command line in, the child's command line out,
* with the tail carried across untouched. */
T_EQ_OWNED(arz_child_cmdline(t_s("C:\\b\\rakudo\\bin\\raku.exe"),
t_s("C:\\b\\site\\bin\\a.raku"),
arz_cmdline_tail(t_u8(
"\"C:\\Program Files\\b\\bin\\a.exe\" --note=\"50%% ^ done\" !x!"))),
"\"C:\\b\\rakudo\\bin\\raku.exe\" \"C:\\b\\site\\bin\\a.raku\""
" --note=\"50%% ^ done\" !x!");
}
int main(void)
{
test_tail();
test_quote();
test_child_cmdline();
return t_done("cmdline");
}