From ed2babafdadd859830274772fcb201acecbe6fab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Feb 2026 22:18:58 +0100 Subject: [PATCH 1/5] 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; +} From e77f971677c3d9d9495e1f1fc73bfed3bda6cba7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Feb 2026 22:19:19 +0100 Subject: [PATCH 2/5] Use proper argument serialization for Windows Replace the hacky implementation for executing simple commands on Windows with the proper serialization mechanism. --- app/src/adb/adb.c | 32 -------------------------------- app/src/sys/win/process.c | 22 +++++----------------- 2 files changed, 5 insertions(+), 49 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 52ed4592..e388d862 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -331,56 +331,24 @@ sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, bool sc_adb_push(struct sc_intr *intr, const char *serial, const char *local, const char *remote, unsigned flags) { -#ifdef _WIN32 - // Windows will parse the string, so the paths must be quoted - // (see sys/win/command.c) - local = sc_str_quote(local); - if (!local) { - return SC_PROCESS_NONE; - } - remote = sc_str_quote(remote); - if (!remote) { - free((void *) local); - return SC_PROCESS_NONE; - } -#endif - assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "push", local, remote); sc_pid pid = sc_adb_execute(argv, flags); -#ifdef _WIN32 - free((void *) remote); - free((void *) local); -#endif - return process_check_success_intr(intr, pid, "adb push", flags); } bool sc_adb_install(struct sc_intr *intr, const char *serial, const char *local, unsigned flags) { -#ifdef _WIN32 - // Windows will parse the string, so the local name must be quoted - // (see sys/win/command.c) - local = sc_str_quote(local); - if (!local) { - return SC_PROCESS_NONE; - } -#endif - assert(serial); const char *const argv[] = SC_ADB_COMMAND("-s", serial, "install", "-r", local); sc_pid pid = sc_adb_execute(argv, flags); -#ifdef _WIN32 - free((void *) local); -#endif - return process_check_success_intr(intr, pid, "adb install", flags); } diff --git a/app/src/sys/win/process.c b/app/src/sys/win/process.c index 38197f2f..c121bef2 100644 --- a/app/src/sys/win/process.c +++ b/app/src/sys/win/process.c @@ -5,25 +5,12 @@ #include #include +#include "util/command.h" #include "util/log.h" -#include "util/strbuf.h" +#include "util/str.h" #define CMD_MAX_LEN 8192 -static bool -build_cmd(char *cmd, size_t len, const char *const argv[]) { - // Windows command-line parsing is WTF: - // - // only make it work for this very specific program - // (don't handle escaping nor quotes) - size_t ret = sc_str_join(cmd, argv, ' ', len); - if (ret >= len) { - LOGE("Command too long (%" SC_PRIsizet " chars)", len - 1); - return false; - } - return true; -} - enum sc_process_result sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags, HANDLE *pin, HANDLE *pout, HANDLE *perr) { @@ -138,8 +125,9 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags, si.lpAttributeList = lpAttributeList; } - char *cmd = malloc(CMD_MAX_LEN); - if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) { + assert(argv && *argv); + char *cmd = sc_command_serialize_windows(argv); + if (!cmd) { LOG_OOM(); goto error_free_attribute_list; } From 96733d8da28ae84f4c4ec1c5d4cddbb0ca9822f4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Feb 2026 22:19:29 +0100 Subject: [PATCH 3/5] Remove sc_str_quote() It is no longer used. --- app/src/util/str.c | 15 --------------- app/src/util/str.h | 8 -------- app/tests/test_str.c | 11 ----------- 3 files changed, 34 deletions(-) diff --git a/app/src/util/str.c b/app/src/util/str.c index 83d19c4d..c4f04b55 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -49,21 +49,6 @@ truncated: return n; } -char * -sc_str_quote(const char *src) { - size_t len = strlen(src); - char *quoted = malloc(len + 3); - if (!quoted) { - LOG_OOM(); - return NULL; - } - memcpy("ed[1], src, len); - quoted[0] = '"'; - quoted[len + 1] = '"'; - quoted[len + 2] = '\0'; - return quoted; -} - char * sc_str_concat(const char *start, const char *end) { assert(start); diff --git a/app/src/util/str.h b/app/src/util/str.h index b386b48d..b48e6334 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -32,14 +32,6 @@ sc_strncpy(char *dest, const char *src, size_t n); size_t sc_str_join(char *dst, const char *const tokens[], char sep, size_t n); -/** - * Quote a string - * - * Return a new allocated string, surrounded with quotes (`"`). - */ -char * -sc_str_quote(const char *src); - /** * Concat two strings * diff --git a/app/tests/test_str.c b/app/tests/test_str.c index 4a906d92..49cf12d5 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -131,16 +131,6 @@ static void test_join_truncated_after_sep(void) { assert(!strcmp("abc de ", s)); } -static void test_quote(void) { - const char *s = "abcde"; - char *out = sc_str_quote(s); - - // add '"' at the beginning and the end - assert(!strcmp("\"abcde\"", out)); - - free(out); -} - static void test_concat(void) { const char *s = "2024:11"; char *out = sc_str_concat("my-prefix:", s); @@ -398,7 +388,6 @@ int main(int argc, char *argv[]) { test_join_truncated_in_token(); test_join_truncated_before_sep(); test_join_truncated_after_sep(); - test_quote(); test_concat(); test_utf8_truncate(); test_parse_integer(); From 86e614c9a50be68d1c9146e18d3b733f65c69778 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Feb 2026 22:51:31 +0100 Subject: [PATCH 4/5] Improve adb devices parsing `adb devices -l` prints one device per line, containing, separated by spaces: - the device serial, - the device state, - a list of key:value pairs. However, the device serial itself may contain spaces, making a simple split ambiguous. To avoid ambiguity, parse the string backwards: - first, parse all the trailing key-value pairs (containing ':'), - then, the preceding token (not containing ':') is the device state, - finally, the remaining leading token is the device serial. --- app/src/adb/adb_parser.c | 121 ++++++++++++++++++++---------------- app/tests/test_adb_parser.c | 20 ++++++ 2 files changed, 88 insertions(+), 53 deletions(-) diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index 90a1b30b..d123584d 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -8,6 +8,31 @@ #include "util/log.h" #include "util/str.h" +static size_t +rstrip_len(const char *s, size_t len) { + size_t i = len; + + // Ignore trailing whitespaces + while (i > 0 && (s[i-1] == ' ' || s[i-1] == '\t')) { + --i; + } + + return i; +} + +static void +locate_last_token(const char *s, size_t len, size_t *start, size_t *end) { + size_t i = rstrip_len(s, len); + *end = i; // excluded + + // The token contains non-whitespace chars + while (i > 0 && (s[i-1] != ' ' && s[i-1] != '\t')) { + --i; + } + + *start = i; // included +} + static bool sc_adb_parse_device(char *line, struct sc_adb_device *device) { // One device line looks like: @@ -25,64 +50,54 @@ sc_adb_parse_device(char *line, struct sc_adb_device *device) { return false; } - char *s = line; // cursor in the line + size_t len = strlen(line); - // After the serial: - // - "adb devices" writes a single '\t' - // - "adb devices -l" writes multiple spaces - // For flexibility, accept both. - size_t serial_len = strcspn(s, " \t"); + size_t start; + size_t end; + + // The serial (the first token) may contain spaces, which are also token + // separators. To avoid ambiguity, parse the string backwards: + // - first, parse all the trailing key-value pairs (containing ':'), + // - then, the preceding token (not containing ':') is the device state, + // - finally, the remaining leading token is the device serial. + // + // Refs: + // - + // - + const char *state; + const char *model = NULL; + for (;;) { + locate_last_token(line, len, &start, &end); + if (start == end) { + // No more tokens, unexpected + return false; + } + + const char *token = &line[start]; + line[end] = '\0'; + + if (!strncmp("model:", token, sizeof("model:") - 1)) { + model = &token[sizeof("model:") - 1]; + // We only need the model + } else if (!strchr(token, ':')) { + // The first non-key:value token, it's the device state + state = token; + break; + } + + // Remove the trailing parts already handled + len = start; + } + + assert(state); + + size_t serial_len = rstrip_len(line, start); if (!serial_len) { // empty serial return false; } - bool eol = s[serial_len] == '\0'; - if (eol) { - // serial alone is unexpected - return false; - } - s[serial_len] = '\0'; - char *serial = s; - s += serial_len + 1; - // After the serial, there might be several spaces - s += strspn(s, " \t"); // consume all separators - - size_t state_len = strcspn(s, " "); - if (!state_len) { - // empty state - return false; - } - eol = s[state_len] == '\0'; - s[state_len] = '\0'; - char *state = s; - - char *model = NULL; - if (!eol) { - s += state_len + 1; - - // Iterate over all properties "key:value key:value ..." - for (;;) { - size_t token_len = strcspn(s, " "); - if (!token_len) { - break; - } - eol = s[token_len] == '\0'; - s[token_len] = '\0'; - char *token = s; - - if (!strncmp("model:", token, sizeof("model:") - 1)) { - model = &token[sizeof("model:") - 1]; - // We only need the model - break; - } - - if (eol) { - break; - } else { - s+= token_len + 1; - } - } - } + char *serial = line; + line[serial_len] = '\0'; device->serial = strdup(serial); if (!device->serial) { diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index 362b254f..d275e7ce 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -163,6 +163,25 @@ static void test_adb_devices_spaces(void) { sc_adb_devices_destroy(&vec); } +static void test_adb_devices_serial_with_spaces(void) { + char output[] = + "List of devices attached\n" + "adb-AEORQMSAOWNQ-N7Ger8 (2)._adb-tls-connect._tcp device " + "product:blazer model:Pixel_10_Pro device:blazer transport_id:3\n"; + + struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER; + bool ok = sc_adb_parse_devices(output, &vec); + assert(ok); + assert(vec.size == 1); + + struct sc_adb_device *device = &vec.data[0]; + assert(!strcmp("adb-AEORQMSAOWNQ-N7Ger8 (2)._adb-tls-connect._tcp", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("Pixel_10_Pro", device->model)); + + sc_adb_devices_destroy(&vec); +} + static void test_get_ip_single_line(void) { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34\r\r\n"; @@ -265,6 +284,7 @@ int main(int argc, char *argv[]) { test_adb_devices_without_header(); test_adb_devices_corrupted(); test_adb_devices_spaces(); + test_adb_devices_serial_with_spaces(); test_get_ip_single_line(); test_get_ip_single_line_without_eol(); From 1350f19f62e90c0b3208a94029d00a8c3d173347 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 29 Dec 2025 18:40:52 +0100 Subject: [PATCH 5/5] Detect TCP devices provided by mDNS Their serial is not in the form ip:port, but rather a complex string containing "adb-tls-connect". Fixes #6248 --- app/src/adb/adb_device.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/adb/adb_device.c b/app/src/adb/adb_device.c index 5ea8eb44..2dc78c9f 100644 --- a/app/src/adb/adb_device.c +++ b/app/src/adb/adb_device.c @@ -39,5 +39,11 @@ sc_adb_device_get_type(const char *serial) { return SC_ADB_DEVICE_TYPE_TCPIP; } + // TCP/IP devices provided by mDNS contain "adb-tls-connect" + // + if (strstr(serial, "adb-tls-connect")) { + return SC_ADB_DEVICE_TYPE_TCPIP; + } + return SC_ADB_DEVICE_TYPE_USB; }