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 values until the device state,
   identified using a list of well-known values;
 - finally, treat the remaining leading token as the device serial.

Fixes #3537 <https://github.com/Genymobile/scrcpy/issues/3537>
Refs #6248 <https://github.com/Genymobile/scrcpy/issues/6248>
PR #6664 <https://github.com/Genymobile/scrcpy/pull/6664>
This commit is contained in:
Romain Vimont
2026-02-09 22:51:31 +01:00
parent e39d7a06e4
commit 82e102e036
2 changed files with 127 additions and 53 deletions

View File

@@ -8,6 +8,49 @@
#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
is_device_state(const char *s) {
// <https://android.googlesource.com/platform/packages/modules/adb/+/1cf2f017d312f73b3dc53bda85ef2610e35a80e9/adb.cpp#144>
// "device", "unauthorized" and "offline" are the most common states, so
// check them first.
return !strcmp(s, "device")
|| !strcmp(s, "unauthorized")
|| !strcmp(s, "offline")
|| !strcmp(s, "bootloader")
|| !strcmp(s, "host")
|| !strcmp(s, "recovery")
|| !strcmp(s, "rescue")
|| !strcmp(s, "sideload")
|| !strcmp(s, "authorizing")
|| !strcmp(s, "connecting")
|| !strcmp(s, "detached");
}
static bool
sc_adb_parse_device(char *line, struct sc_adb_device *device) {
// One device line looks like:
@@ -25,64 +68,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 values until the device state,
// identified using a list of well-known values;
// - finally, treat the remaining leading token as the device serial.
//
// Refs:
// - <https://github.com/Genymobile/scrcpy/issues/6248>
// - <https://github.com/Genymobile/scrcpy/issues/3537>
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 (is_device_state(token)) {
state = token;
// The device state is the item immediately after the device serial
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) {

View File

@@ -163,6 +163,45 @@ 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-0123456789AB-CDdefg (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-0123456789AB-CDdefg (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_adb_devices_with_devpath_without_colon(void) {
char output[] =
"List of devices attached\n"
"12345678 device 3-1 product:manet model:23117RK66C "
"device:manet transport_id:2\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("12345678", device->serial));
assert(!strcmp("device", device->state));
assert(!strcmp("23117RK66C", 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 +304,8 @@ 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_adb_devices_with_devpath_without_colon();
test_get_ip_single_line();
test_get_ip_single_line_without_eol();