App-Ariza.git | runner/src/ | cmdline.c
/* Command lines: where the caller's arguments start, and how the two
* arguments the runner supplies are quoted.
*
* This file is the point of the whole exercise. A .cmd launcher ends in
* `"%ARIZA_RAKU%" "%BUNDLE_ROOT%\<target>" %*`, and `%*` is not the
* caller's arguments — it is the caller's arguments after cmd.exe has
* had a second go at them. A '^' is an escape character to cmd, '%x%'
* and '!x!' are expansions, and a quoted argument containing either
* arrives at the app as something the user did not type. The batch file
* cannot avoid it: by the time `%*` is substituted, the damage is in the
* substitution.
*
* A compiled launcher can avoid it completely, and does, by never
* parsing the tail at all: it finds where argv[0] ends, and copies every
* unit after that into the child's command line unchanged. The child's
* C runtime then parses exactly the bytes the user's shell produced,
* which is what it would have done had the user run raku.exe directly. */
#include <stdlib.h>
#include "ariza_runner.h"
static int is_space(arz_char c)
{
return c == ARZ_T(' ') || c == ARZ_T('\t');
}
const arz_char *arz_cmdline_tail(const arz_char *cmdline)
{
const arz_char *p = cmdline;
if (p == NULL)
return NULL;
while (is_space(*p))
p++;
if (*p == ARZ_T('"')) {
/* The C runtime's argv[0] rule, which is not the rule for any
* other argument: everything between the opening quote and the
* next one is the program name, and a backslash inside it is a
* backslash. It can afford to be that simple because argv[0]
* has to name a file, and a Windows path cannot contain a
* quote. An unterminated quote swallows the rest of the line,
* exactly as the runtime would have it. */
p++;
while (*p != ARZ_T('"') && *p != ARZ_T('\0'))
p++;
if (*p == ARZ_T('"'))
p++;
} else {
while (*p != ARZ_T('\0') && !is_space(*p))
p++;
}
while (is_space(*p))
p++;
return p;
}
arz_char *arz_quote_arg(const arz_char *arg)
{
arz_char *out;
size_t worst;
size_t at = 0;
size_t i = 0;
size_t n;
if (arg == NULL)
arg = ARZ_T("");
n = arz_len(arg);
/* Every unit can at worst double (a backslash run before a quote)
* and a quote adds one more, plus the two wrapping quotes. */
worst = n * 2 + 3;
out = malloc((worst + 1) * sizeof(*out));
if (out == NULL)
return NULL;
out[at++] = ARZ_T('"');
while (i < n) {
size_t slashes = 0;
size_t k;
while (i < n && arg[i] == ARZ_T('\\')) {
slashes++;
i++;
}
if (i == n) {
/* Trailing backslashes sit immediately before the
* closing quote, so they have to be doubled or the
* quote is escaped instead of closing. */
for (k = 0; k < slashes * 2; k++)
out[at++] = ARZ_T('\\');
break;
}
if (arg[i] == ARZ_T('"')) {
for (k = 0; k < slashes * 2 + 1; k++)
out[at++] = ARZ_T('\\');
} else {
for (k = 0; k < slashes; k++)
out[at++] = ARZ_T('\\');
}
out[at++] = arg[i++];
}
out[at++] = ARZ_T('"');
out[at] = ARZ_T('\0');
return out;
}
arz_char *arz_child_cmdline(const arz_char *exe, const arz_char *script,
const arz_char *tail)
{
arz_char *qexe = arz_quote_arg(exe);
arz_char *qscript = arz_quote_arg(script);
arz_char *head = NULL;
arz_char *out = NULL;
if (qexe == NULL || qscript == NULL)
goto done;
head = arz_cat(qexe, ARZ_T(" "), qscript);
if (head == NULL)
goto done;
if (tail == NULL || tail[0] == ARZ_T('\0'))
out = arz_dup(head);
else
out = arz_cat(head, ARZ_T(" "), tail);
done:
free(qexe);
free(qscript);
free(head);
return out;
}