From 4389de1c239c524f2a3aed2c985893dd3de74824 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 5 Feb 2022 22:32:40 +0100 Subject: [PATCH] Add adb devices parser Add a parser of `adb device -l` output, to extract a list of devices with their serial, state and model. PR #3005 --- app/meson.build | 2 + app/src/adb/adb_device.c | 26 ++++++ app/src/adb/adb_device.h | 33 ++++++++ app/src/adb/adb_parser.c | 159 +++++++++++++++++++++++++++++++++++- app/src/adb/adb_parser.h | 13 +++ app/tests/test_adb_parser.c | 159 ++++++++++++++++++++++++++++++++++++ 6 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 app/src/adb/adb_device.c create mode 100644 app/src/adb/adb_device.h diff --git a/app/meson.build b/app/meson.build index b2c0bc55..a9b39b1e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -1,6 +1,7 @@ src = [ 'src/main.c', 'src/adb/adb.c', + 'src/adb/adb_device.c', 'src/adb/adb_parser.c', 'src/adb/adb_tunnel.c', 'src/cli.c', @@ -221,6 +222,7 @@ if get_option('buildtype') == 'debug' tests = [ ['test_adb_parser', [ 'tests/test_adb_parser.c', + 'src/adb/adb_device.c', 'src/adb/adb_parser.c', 'src/util/str.c', 'src/util/strbuf.c', diff --git a/app/src/adb/adb_device.c b/app/src/adb/adb_device.c new file mode 100644 index 00000000..b6ff16a7 --- /dev/null +++ b/app/src/adb/adb_device.c @@ -0,0 +1,26 @@ +#include "adb_device.h" + +#include + +void +sc_adb_device_destroy(struct sc_adb_device *device) { + free(device->serial); + free(device->state); + free(device->model); +} + +void +sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src) { + *dst = *src; + src->serial = NULL; + src->state = NULL; + src->model = NULL; +} + +void +sc_adb_devices_destroy_all(struct sc_adb_device *devices, size_t count) { + for (size_t i = 0; i < count; ++i) { + sc_adb_device_destroy(&devices[i]); + } +} + diff --git a/app/src/adb/adb_device.h b/app/src/adb/adb_device.h new file mode 100644 index 00000000..11b46c0c --- /dev/null +++ b/app/src/adb/adb_device.h @@ -0,0 +1,33 @@ +#ifndef SC_ADB_DEVICE_H +#define SC_ADB_DEVICE_H + +#include "common.h" + +#include +#include + +struct sc_adb_device { + char *serial; + char *state; + char *model; +}; + +void +sc_adb_device_destroy(struct sc_adb_device *device); + +/** + * Move src to dst + * + * After this call, the content of src is undefined, except that + * sc_adb_device_destroy() can be called. + * + * This is useful to take a device from a list that will be destroyed, without + * making unnecessary copies. + */ +void +sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src); + +void +sc_adb_devices_destroy_all(struct sc_adb_device *devices, size_t count); + +#endif diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index 2a0fd8da..d41e1bcd 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -1,11 +1,168 @@ #include "adb_parser.h" #include +#include #include #include "util/log.h" #include "util/str.h" +bool +sc_adb_parse_device(char *line, struct sc_adb_device *device) { + // One device line looks like: + // "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + // "device:MyDevice transport_id:1" + + if (line[0] == '*') { + // Garbage lines printed by adb daemon while starting start with a '*' + return false; + } + + if (!strncmp("adb server", line, sizeof("adb server") - 1)) { + // Ignore lines starting with "adb server": + // adb server version (41) doesn't match this client (39); killing... + return false; + } + + char *s = line; // cursor in the 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"); + 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; + } + } + } + + device->serial = strdup(serial); + if (!device->serial) { + return false; + } + + device->state = strdup(state); + if (!device->state) { + free(device->serial); + return false; + } + + if (model) { + device->model = strdup(model); + if (!device->model) { + LOG_OOM(); + // model is optional, do not fail + } + } else { + device->model = NULL; + } + + return true; +} + +ssize_t +sc_adb_parse_devices(char *str, struct sc_adb_device *devices, + size_t devices_len) { + size_t dev_count = 0; + +#define HEADER "List of devices attached" +#define HEADER_LEN (sizeof(HEADER) - 1) + bool header_found = false; + + size_t idx_line = 0; + while (str[idx_line] != '\0') { + char *line = &str[idx_line]; + size_t len = strcspn(line, "\n"); + + // The next line starts after the '\n' (replaced by `\0`) + idx_line += len; + + if (str[idx_line] != '\0') { + // The next line starts after the '\n' + ++idx_line; + } + + if (!header_found) { + if (!strncmp(line, HEADER, HEADER_LEN)) { + header_found = true; + } + // Skip everything until the header, there might be garbage lines + // related to daemon starting before + continue; + } + + // The line, but without any trailing '\r' + size_t line_len = sc_str_remove_trailing_cr(line, len); + line[line_len] = '\0'; + + bool ok = sc_adb_parse_device(line, &devices[dev_count]); + if (!ok) { + continue; + } + + ++dev_count; + + assert(dev_count <= devices_len); + if (dev_count == devices_len) { + // Max number of devices reached + break; + } + } + + if (!header_found) { + return -1; + } + + return dev_count; +} + static char * sc_adb_parse_device_ip_from_line(char *line) { // One line from "ip route" looks like: @@ -64,7 +221,7 @@ sc_adb_parse_device_ip_from_output(char *str) { if (str[idx_line] != '\0') { // The next line starts after the '\n' - idx_line += 1; + ++idx_line; } } diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h index 7d116713..65493a2e 100644 --- a/app/src/adb/adb_parser.h +++ b/app/src/adb/adb_parser.h @@ -5,6 +5,19 @@ #include +#include "adb_device.h" + +/** + * Parse the available devices from the output of `adb devices` + * + * The parameter must be a NUL-terminated string. + * + * Warning: this function modifies the buffer for optimization purposes. + */ +ssize_t +sc_adb_parse_devices(char *str, struct sc_adb_device *devices, + size_t devices_len); + /** * Parse the ip from the output of `adb shell ip route` * diff --git a/app/tests/test_adb_parser.c b/app/tests/test_adb_parser.c index 749ce433..990418c0 100644 --- a/app/tests/test_adb_parser.c +++ b/app/tests/test_adb_parser.c @@ -2,8 +2,158 @@ #include +#include "adb/adb_device.h" #include "adb/adb_parser.h" +static void test_adb_devices() { + char output[] = + "List of devices attached\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\n" + "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " + "device:MyWifiDevice trandport_id:2\n"; + + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 2); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + device = &devices[1]; + assert(!strcmp("192.168.1.1:5555", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyWifiModel", device->model)); + + sc_adb_devices_destroy_all(devices, count); +} + +static void test_adb_devices_cr() { + char output[] = + "List of devices attached\r\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\r\n" + "192.168.1.1:5555 device product:MyWifiProduct model:MyWifiModel " + "device:MyWifiDevice trandport_id:2\r\n"; + + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 2); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + device = &devices[1]; + assert(!strcmp("192.168.1.1:5555", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyWifiModel", device->model)); + + sc_adb_devices_destroy_all(devices, count); +} + +static void test_adb_devices_daemon_start() { + char output[] = + "* daemon not running; starting now at tcp:5037\n" + "* daemon started successfully\n" + "List of devices attached\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\n"; + + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 1); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + sc_adb_device_destroy(device); +} + +static void test_adb_devices_daemon_start_mixed() { + char output[] = + "List of devices attached\n" + "adb server version (41) doesn't match this client (39); killing...\n" + "* daemon started successfully *\n" + "0123456789abcdef unauthorized usb:1-1\n" + "87654321 device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice\n"; + + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 2); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("unauthorized", device->state)); + fprintf(stderr, "==== [%s]\n", device->model); + assert(!device->model); + + device = &devices[1]; + assert(!strcmp("87654321", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + sc_adb_devices_destroy_all(devices, count); +} + +static void test_adb_devices_without_eol() { + char output[] = + "List of devices attached\n" + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1"; + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 1); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("device", device->state)); + assert(!strcmp("MyModel", device->model)); + + sc_adb_device_destroy(device); +} + +static void test_adb_devices_without_header() { + char output[] = + "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " + "device:MyDevice transport_id:1\n"; + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == -1); +} + +static void test_adb_devices_corrupted() { + char output[] = + "List of devices attached\n" + "corrupted_garbage\n"; + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 0); +} + +static void test_adb_devices_spaces() { + char output[] = + "List of devices attached\n" + "0123456789abcdef unauthorized usb:1-4 transport_id:3\n"; + + struct sc_adb_device devices[16]; + ssize_t count = sc_adb_parse_devices(output, devices, ARRAY_LEN(devices)); + assert(count == 1); + + struct sc_adb_device *device = &devices[0]; + assert(!strcmp("0123456789abcdef", device->serial)); + assert(!strcmp("unauthorized", device->state)); + assert(!device->model); + + sc_adb_device_destroy(device); +} + static void test_get_ip_single_line() { char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src " "192.168.12.34\r\r\n"; @@ -86,6 +236,15 @@ int main(int argc, char *argv[]) { (void) argc; (void) argv; + test_adb_devices(); + test_adb_devices_cr(); + test_adb_devices_daemon_start(); + test_adb_devices_daemon_start_mixed(); + test_adb_devices_without_eol(); + test_adb_devices_without_header(); + test_adb_devices_corrupted(); + test_adb_devices_spaces(); + test_get_ip_single_line(); test_get_ip_single_line_without_eol(); test_get_ip_single_line_with_trailing_space();