From ed2babafdadd859830274772fcb201acecbe6fab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Feb 2026 22:18:58 +0100 Subject: [PATCH] Add utility to serialize windows arguments Add a function to convert an argv array into a single escaped string to be passed to CreateProcess() on Windows. Refs Refs --- app/meson.build | 7 +++ app/src/sys/win/process.c | 3 +- app/src/util/command.c | 99 ++++++++++++++++++++++++++++++++ app/src/util/command.h | 17 ++++++ app/tests/test_command_windows.c | 61 ++++++++++++++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 app/src/util/command.c create mode 100644 app/src/util/command.h create mode 100644 app/tests/test_command_windows.c diff --git a/app/meson.build b/app/meson.build index c730ce07..35c2e3ea 100644 --- a/app/meson.build +++ b/app/meson.build @@ -75,6 +75,7 @@ conf.set('_GNU_SOURCE', true) if host_machine.system() == 'windows' windows = import('windows') src += [ + 'src/util/command.c', 'src/sys/win/file.c', 'src/sys/win/process.c', windows.compile_resources('scrcpy-windows.rc'), @@ -238,6 +239,12 @@ if get_option('buildtype') == 'debug' 'src/util/strbuf.c', 'src/util/term.c', ]], + ['test_command_windows', [ + 'tests/test_command_windows.c', + 'src/util/command.c', + 'src/util/str.c', + 'src/util/strbuf.c', + ]], ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 6ae33d86..38197f2f 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -3,9 +3,10 @@ #include #include +#include #include "util/log.h" -#include "util/str.h" +#include "util/strbuf.h" #define CMD_MAX_LEN 8192 diff --git a/app/src/util/command.c b/app/src/util/command.c new file mode 100644 index 00000000..efd0a8fa --- /dev/null +++ b/app/src/util/command.c @@ -0,0 +1,99 @@ +#include "command.h" + +#include + +#include "util/strbuf.h" + +char * +sc_command_serialize_windows(const char *const argv[]) { + // + // + + struct sc_strbuf buf; + bool ok = sc_strbuf_init(&buf, 1024); + if (!ok) { + return NULL; + } + +#define BUF_PUSH(C) \ + do { \ + ok = sc_strbuf_append_char(&buf, C); \ + if (!ok) { \ + goto end; \ + } \ + } while (0) + + while (*argv) { + const char *arg = *argv; + + BUF_PUSH('"'); + + // + // + // """ + // If an even number of backslashes is followed by a double quote mark, + // then one backslash (\) is placed in the argv array for every pair of + // backslashes (\\), and the double quote mark (") is interpreted as a + // string delimiter. + // + // If an odd number of backslashes is followed by a double quote mark, + // then one backslash (\) is placed in the argv array for every pair of + // backslashes (\\). The double quote mark is interpreted as an escape + // sequence by the remaining backslash, causing a literal double quote + // mark (") to be placed in argv. + // """ + // + // To produce correct escaping according to what the parser will do, we + // must count the number of successive backslashes. + unsigned backslashes = 0; + + for (const char *c = arg; *c; c++) { + switch (*c) { + case '"': + while (backslashes) { + // Double all backslashes before a quote + BUF_PUSH('\\'); + BUF_PUSH('\\'); + --backslashes; + } + BUF_PUSH('\\'); + BUF_PUSH('"'); + backslashes = 0; + break; + case '\\': + ++backslashes; + break; + default: + while (backslashes) { + // Put all backslashes as literals + BUF_PUSH('\\'); + --backslashes; + } + BUF_PUSH(*c); + break; + } + } + + while (backslashes) { + // Double all backslashes before a quote + BUF_PUSH('\\'); + BUF_PUSH('\\'); + --backslashes; + } + + BUF_PUSH('"'); + + ++argv; + + // Argument separator + if (*argv) { + BUF_PUSH(' '); + } + } + + return buf.s; + +end: + free(buf.s); + return NULL; +} diff --git a/app/src/util/command.h b/app/src/util/command.h new file mode 100644 index 00000000..18497311 --- /dev/null +++ b/app/src/util/command.h @@ -0,0 +1,17 @@ +#ifndef SC_COMMAND_H +#define SC_COMMAND_H + +#include "common.h" + +/** + * Serialize an argv array for Windows + * + * Convert a NULL-terminated argument array into a single escaped string + * suitable for massing to CreateProcess() on Windows. + * + * The returned value must be freed by the caller. + */ +char * +sc_command_serialize_windows(const char *const argv[]); + +#endif diff --git a/app/tests/test_command_windows.c b/app/tests/test_command_windows.c new file mode 100644 index 00000000..e041250c --- /dev/null +++ b/app/tests/test_command_windows.c @@ -0,0 +1,61 @@ +#include "common.h" + +#include +#include + +#include "util/command.h" + +static void test_command_with_spaces(void) { + const char *const argv[] = { + "C:\\Program Files\\scrcpy\\adb", + "-s", + "serial with spaces", + "push", + "E:\\some folder\\scrcpy-server", + "/data/local/tmp/scrcpy-server.jar", + NULL, + }; + char *cmd = sc_command_serialize_windows(argv); + const char *expected = "\"C:\\Program Files\\scrcpy\\adb\" " + "\"-s\" " + "\"serial with spaces\" " + "\"push\" " + "\"E:\\some folder\\scrcpy-server\" " + "\"/data/local/tmp/scrcpy-server.jar\""; + + assert(!strcmp(expected, cmd)); + free(cmd); +} + +static void test_command_with_backslashes(void) { + const char *const argv[] = { + "a\\\\ b\\", + "def \\", + "gh\"i\" \\\\", + "jkl\\\\", + "mno\\", + "p\\\"qr", + NULL, + }; + + char *cmd = sc_command_serialize_windows(argv); + const char *expected = "\"a\\\\ b\\\\\" " + "\"def \\\\\" " + "\"gh\\\"i\\\" \\\\\\\\\" " + "\"jkl\\\\\\\\\" " + "\"mno\\\\\" " + "\"p\\\\\\\"qr\""; + + assert(!strcmp(expected, cmd)); + free(cmd); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_command_with_spaces(); + test_command_with_backslashes(); + + return 0; +}