diff --git a/CMakeLists.txt b/CMakeLists.txt index b9758155b..b128734a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,9 +38,9 @@ if(APPLE) option(CMAKE_FRAMEWORK "Build macOS/iOS Framework version of the library" OFF) endif() elseif(NOT WIN32) + option(HIDAPI_WITH_LIBUSB "Build LIBUSB-based implementation of HIDAPI" ON) if(CMAKE_SYSTEM_NAME MATCHES "Linux") option(HIDAPI_WITH_HIDRAW "Build HIDRAW-based implementation of HIDAPI" ON) - option(HIDAPI_WITH_LIBUSB "Build LIBUSB-based implementation of HIDAPI" ON) endif() if(CMAKE_SYSTEM_NAME MATCHES "NetBSD") option(HIDAPI_WITH_NETBSD "Build NetBSD/UHID implementation of HIDAPI" ON) @@ -73,12 +73,14 @@ endif() if(WIN32) option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" ${IS_DEBUG_BUILD}) elseif(CMAKE_SYSTEM_NAME MATCHES "Linux" OR APPLE) - # Linux and macOS have virtual-device based tests (uhid / raw-gadget / - # IOHIDUserDevice). Off by default: they need a virtual device at runtime - # and otherwise self-skip. + # Linux and macOS include virtual-device based tests (uhid / raw-gadget / + # IOHIDUserDevice). Keep the full test suite opt-in because those tests need + # a virtual device at runtime and otherwise self-skip. option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" OFF) +elseif(HIDAPI_WITH_LIBUSB) + option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" ${IS_DEBUG_BUILD}) else() - set(HIDAPI_WITH_TESTS OFF) + option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" OFF) endif() if(HIDAPI_WITH_TESTS) diff --git a/libusb/CMakeLists.txt b/libusb/CMakeLists.txt index e0be178da..da0f6b391 100644 --- a/libusb/CMakeLists.txt +++ b/libusb/CMakeLists.txt @@ -2,6 +2,7 @@ list(APPEND HIDAPI_PUBLIC_HEADERS "hidapi_libusb.h") add_library(hidapi_libusb ${HIDAPI_PUBLIC_HEADERS} + hidapi_libusb_report_descriptor.h hid.c ) if(HIDAPI_BUILD_AS_CXX) @@ -110,3 +111,7 @@ hidapi_configure_pc("${PROJECT_ROOT}/pc/hidapi-backend-specific.pc.in" LIBRARY_NAME "hidapi-libusb" DESCRIPTION "C Library for USB HID device access from Linux, Mac OS X, FreeBSD, and Windows. This is the libusb implementation." ) + +if(HIDAPI_WITH_TESTS) + add_subdirectory(test) +endif() diff --git a/libusb/hid.c b/libusb/hid.c index 74f59cf5e..b19c76440 100644 --- a/libusb/hid.c +++ b/libusb/hid.c @@ -32,6 +32,7 @@ #include #include #include +#include /* Unix */ #include @@ -49,6 +50,7 @@ #endif #include "hidapi_libusb.h" +#include "hidapi_libusb_report_descriptor.h" #ifndef HIDAPI_THREAD_MODEL_INCLUDE #define HIDAPI_THREAD_MODEL_INCLUDE "hidapi_thread_pthread.h" @@ -112,6 +114,10 @@ struct hid_device_ { int interface; uint16_t report_descriptor_size; + uint8_t *report_descriptor; + size_t report_descriptor_length; + /* Includes report number. */ + size_t max_input_report_size; /* Endpoint information */ int input_endpoint; @@ -131,6 +137,7 @@ struct hid_device_ { hidapi_thread_state thread_state; int shutdown_thread; int transfer_loop_finished; + int read_thread_init_error; struct libusb_transfer *transfer; /* List of received input reports. */ @@ -161,6 +168,7 @@ static hidapi_error_ctx last_global_error; uint16_t get_usb_code_for_current_locale(void); static int return_data(hid_device *dev, unsigned char *data, size_t length); +static int hid_get_report_descriptor_libusb(libusb_device_handle *handle, int interface_num, uint16_t expected_report_descriptor_size, unsigned char *buf, size_t buf_size); static hid_device *new_hid_device(void) { @@ -191,6 +199,7 @@ static void free_hid_device(hid_device *dev) hid_free_enumeration(dev->device_info); free_hidapi_error(&dev->error); free(dev->last_read_error_str); + free(dev->report_descriptor); /* Free the device itself */ free(dev); @@ -1173,12 +1182,34 @@ static void *read_thread(void *param) { int res; hid_device *dev = (hid_device *) param; - uint8_t *buf; - const size_t length = dev->input_ep_max_packet_size; + uint8_t *buf = NULL; + size_t length = (size_t)dev->input_ep_max_packet_size; + + /* Never shrink below the endpoint packet size: some devices pad short + reports to a complete USB packet. */ + if (dev->max_input_report_size > length) + length = dev->max_input_report_size; + + if (length == 0 || length > INT_MAX) { + LOG("Invalid input report buffer length: %zu\n", length); + dev->read_thread_init_error = LIBUSB_ERROR_INVALID_PARAM; + goto notify_main_thread; + } /* Set up the transfer object. */ buf = (uint8_t*) malloc(length); + if (!buf) { + dev->read_thread_init_error = LIBUSB_ERROR_NO_MEM; + goto notify_main_thread; + } + dev->transfer = libusb_alloc_transfer(0); + if (!dev->transfer) { + free(buf); + dev->read_thread_init_error = LIBUSB_ERROR_NO_MEM; + goto notify_main_thread; + } + libusb_fill_interrupt_transfer(dev->transfer, dev->device_handle, dev->input_endpoint, @@ -1193,13 +1224,18 @@ static void *read_thread(void *param) res = libusb_submit_transfer(dev->transfer); if(res < 0) { LOG("libusb_submit_transfer failed: %d %s. Stopping read_thread from running\n", res, libusb_error_name(res)); + dev->read_thread_init_error = res; dev->shutdown_thread = 1; dev->transfer_loop_finished = 1; } +notify_main_thread: /* Notify the main thread that the read thread is up and running. */ hidapi_thread_barrier_wait(&dev->thread_state); + if (dev->read_thread_init_error) + return NULL; + /* Handle all the events. */ while (!dev->shutdown_thread) { res = libusb_handle_events(usb_context); @@ -1314,6 +1350,17 @@ static int hidapi_initialize_device(hid_device *dev, const struct libusb_interfa int i =0; int res = 0; struct libusb_device_descriptor desc; + + /* hid_open_path() can retry another alternate setting after initialization + fails. Clear all state owned by the previous attempt before doing so. */ + free(dev->report_descriptor); + dev->report_descriptor = NULL; + dev->report_descriptor_length = 0; + dev->max_input_report_size = 0; + dev->shutdown_thread = 0; + dev->transfer_loop_finished = 0; + dev->read_thread_init_error = 0; + libusb_get_device_descriptor(libusb_get_device(dev->device_handle), &desc); #ifdef DETACH_KERNEL_DRIVER @@ -1367,6 +1414,25 @@ static int hidapi_initialize_device(hid_device *dev, const struct libusb_interfa dev->report_descriptor_size = get_report_descriptor_size_from_interface_descriptors(intf_desc); + if (intf_desc->bInterfaceClass == LIBUSB_CLASS_HID) { + unsigned char report_descriptor[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; + int desc_size = hid_get_report_descriptor_libusb(dev->device_handle, dev->interface, dev->report_descriptor_size, report_descriptor, sizeof(report_descriptor)); + + if (desc_size > 0) { + ssize_t max_input_report_size; + + dev->report_descriptor = (uint8_t *)malloc((size_t)desc_size); + if (dev->report_descriptor) { + memcpy(dev->report_descriptor, report_descriptor, (size_t)desc_size); + dev->report_descriptor_length = (size_t)desc_size; + } + + max_input_report_size = get_max_report_size(report_descriptor, (size_t)desc_size, REPORT_DESCR_INPUT); + if (max_input_report_size > 0) + dev->max_input_report_size = (size_t)max_input_report_size; + } + } + dev->input_endpoint = 0; dev->input_ep_max_packet_size = 0; dev->output_endpoint = 0; @@ -1407,6 +1473,26 @@ static int hidapi_initialize_device(hid_device *dev, const struct libusb_interfa /* Wait here for the read thread to be initialized. */ hidapi_thread_barrier_wait(&dev->thread_state); + if (dev->read_thread_init_error) { + hidapi_thread_join(&dev->thread_state); + if (dev->transfer) { + free(dev->transfer->buffer); + dev->transfer->buffer = NULL; + libusb_free_transfer(dev->transfer); + dev->transfer = NULL; + } + + libusb_release_interface(dev->device_handle, dev->interface); +#ifdef DETACH_KERNEL_DRIVER + if (dev->is_driver_detached) { + res = libusb_attach_kernel_driver(dev->device_handle, dev->interface); + if (res < 0) + LOG("Failed to reattach the driver to kernel: (%d) %s\n", res, libusb_error_name(res)); + dev->is_driver_detached = 0; + } +#endif + return 0; + } return 1; } @@ -2079,7 +2165,12 @@ int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char register_libusb_error(&dev->error, LIBUSB_SUCCESS, NULL); - res = hid_get_report_descriptor_libusb(dev->device_handle, dev->interface, dev->report_descriptor_size, buf, buf_size); + if (dev->report_descriptor) { + res = dev->report_descriptor_length < buf_size ? (int)dev->report_descriptor_length : (int)buf_size; + memcpy(buf, dev->report_descriptor, (size_t)res); + } else { + res = hid_get_report_descriptor_libusb(dev->device_handle, dev->interface, dev->report_descriptor_size, buf, buf_size); + } if (res < 0) { register_libusb_error(&dev->error, res, "hid_get_report_descriptor"); diff --git a/libusb/hidapi_libusb_report_descriptor.h b/libusb/hidapi_libusb_report_descriptor.h new file mode 100644 index 000000000..fd6d41564 --- /dev/null +++ b/libusb/hidapi_libusb_report_descriptor.h @@ -0,0 +1,162 @@ +#ifndef HIDAPI_LIBUSB_REPORT_DESCRIPTOR_H +#define HIDAPI_LIBUSB_REPORT_DESCRIPTOR_H + +#include +#include +#include + +enum report_descr_type { + REPORT_DESCR_INPUT = 0x80, + REPORT_DESCR_OUTPUT = 0x90, + REPORT_DESCR_FEATURE = 0xB0, +}; + +struct report_global_state { + uint32_t report_size; + uint32_t report_count; + uint8_t report_id; + int report_size_set; + int report_count_set; +}; + +#define REPORT_GLOBAL_STACK_SIZE 16 +/* Bound descriptor-controlled allocations to a conservative 16-bit report + range while leaving room for practical libusb interrupt transfers. */ +#define HIDAPI_LIBUSB_MAX_REPORT_SIZE ((size_t)UINT16_MAX) + +static uint32_t get_report_item_data(const uint8_t *report_descriptor, size_t item_offset, size_t data_len) +{ + uint32_t value = 0; + + for (size_t i = 0; i < data_len; i++) + value |= (uint32_t)report_descriptor[item_offset + 1 + i] << (8 * i); + + return value; +} + +/* Retrieves the largest report size (in bytes) from the passed-in report + descriptor. Returns the size on success, 0 when the descriptor contains no + reports of the requested type, and -1 for a malformed descriptor. */ +static ssize_t get_max_report_size(const uint8_t *report_descriptor, size_t descriptor_size, enum report_descr_type report_type) +{ + struct report_global_state state = {0, 0, 0, 0, 0}; + struct report_global_state state_stack[REPORT_GLOBAL_STACK_SIZE]; + size_t report_bits[256] = {0}; + size_t state_stack_size = 0; + size_t offset = 0; + int report_found = 0; + int report_ids_used = 0; + + if (!report_descriptor && descriptor_size > 0) + return -1; + + while (offset < descriptor_size) { + const uint8_t key = report_descriptor[offset]; + size_t data_len; + size_t key_size; + + if (key == 0xfe) { + /* Long Item: prefix, data size, long-item tag, then data. */ + if (descriptor_size - offset < 3) + return -1; + data_len = report_descriptor[offset + 1]; + key_size = 3; + } else { + const uint8_t size_code = key & 0x3; + data_len = size_code == 3 ? 4 : size_code; + key_size = 1; + } + + if (data_len > descriptor_size - offset - key_size) + return -1; + + if (key != 0xfe) { + const uint8_t key_cmd = key & 0xfc; + const uint32_t value = get_report_item_data(report_descriptor, offset, data_len); + + switch (key_cmd) { + case 0x74: /* Report Size */ + state.report_size = value; + state.report_size_set = 1; + break; + case 0x84: /* Report ID */ + if (data_len == 0 || value == 0 || value > UINT8_MAX) + return -1; + state.report_id = (uint8_t)value; + report_ids_used = 1; + break; + case 0x94: /* Report Count */ + state.report_count = value; + state.report_count_set = 1; + break; + case 0xa4: /* Push */ + if (data_len != 0 || state_stack_size == REPORT_GLOBAL_STACK_SIZE) + return -1; + state_stack[state_stack_size++] = state; + break; + case 0xb4: /* Pop */ + if (data_len != 0 || state_stack_size == 0) + return -1; + state = state_stack[--state_stack_size]; + break; + default: + if (key_cmd == (uint8_t)report_type) { + size_t item_bits; + + if (!state.report_count_set || !state.report_size_set) + return -1; + if (state.report_count != 0 && state.report_size > SIZE_MAX / state.report_count) + return -1; + + item_bits = (size_t)state.report_count * state.report_size; + if (report_bits[state.report_id] > SIZE_MAX - item_bits) + return -1; + + report_bits[state.report_id] += item_bits; + report_found = 1; + } + break; + } + } + + offset += key_size + data_len; + } + + if (state_stack_size != 0) + return -1; + if (!report_found) + return 0; + + if (report_ids_used) { + size_t max_bits = 0; + size_t max_bytes; + + /* Report ID 0 is reserved when Report ID items are used. */ + if (report_bits[0] != 0) + return -1; + + for (size_t report_id = 1; report_id < 256; report_id++) { + if (report_bits[report_id] > max_bits) + max_bits = report_bits[report_id]; + } + + if (max_bits > SIZE_MAX - 7) + return -1; + max_bytes = (max_bits + 7) / 8; + if (max_bytes >= (size_t)PTRDIFF_MAX || + max_bytes >= HIDAPI_LIBUSB_MAX_REPORT_SIZE) + return -1; + return (ssize_t)(max_bytes + 1); + } + + if (report_bits[0] > (size_t)PTRDIFF_MAX - 7) + return -1; + { + const size_t report_size = (report_bits[0] + 7) / 8; + if (report_size > HIDAPI_LIBUSB_MAX_REPORT_SIZE) + return -1; + return (ssize_t)report_size; + } +} + +#endif diff --git a/libusb/test/CMakeLists.txt b/libusb/test/CMakeLists.txt new file mode 100644 index 000000000..436fc0ded --- /dev/null +++ b/libusb/test/CMakeLists.txt @@ -0,0 +1,59 @@ +add_executable(max_input_report_size_test max_input_report_size_test.c) +set_target_properties(max_input_report_size_test + PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED TRUE +) + +target_link_libraries(max_input_report_size_test PRIVATE hidapi_include) + +# Each test case requires 2 files: +# .pp_data - textual representation of HIDP_PREPARSED_DATA; +# _real.rpt_desc - the original report descriptor used to create a test case. +set(HID_REPORT_DESCRIPTOR_TEST_CASES + 046D_C52F_0001_000C + 046D_C52F_0001_FF00 + 046D_C52F_0002_0001 + 046D_C52F_0002_FF00 + 17CC_1130_0000_FF01 + 046D_0A37_0001_000C + 046A_0011_0006_0001 + 046D_C077_0002_0001 + 046D_C283_0004_0001 + 046D_B010_0006_0001 + 046D_B010_0002_FF00 + 046D_B010_0002_0001 + 046D_B010_0001_FF00 + 046D_B010_0001_000C + 046D_C534_0001_000C + 046D_C534_0001_FF00 + 046D_C534_0002_0001 + 046D_C534_0002_FF00 + 046D_C534_0006_0001 + 046D_C534_0080_0001 + 047F_C056_0001_000C + 047F_C056_0003_FFA0 + 047F_C056_0005_000B + 045E_02FF_0005_0001 + 1532_00A3_0002_0001 +) + +add_test(NAME LibUsbHidReportDescriptorParserTest + COMMAND max_input_report_size_test --self-test +) + +foreach(TEST_CASE ${HID_REPORT_DESCRIPTOR_TEST_CASES}) + set(TEST_PP_DATA "${CMAKE_CURRENT_LIST_DIR}/../../windows/test/data/${TEST_CASE}.pp_data") + if(NOT EXISTS "${TEST_PP_DATA}") + message(FATAL_ERROR "Missing '${TEST_PP_DATA}' file for '${TEST_CASE}' test case") + endif() + set(TEST_REPORT_DESCRIPTOR "${CMAKE_CURRENT_LIST_DIR}/../../windows/test/data/${TEST_CASE}_real.rpt_desc") + if(NOT EXISTS "${TEST_REPORT_DESCRIPTOR}") + message(FATAL_ERROR "Missing '${TEST_REPORT_DESCRIPTOR}' file for '${TEST_CASE}' test case") + endif() + + add_test(NAME "LibUsbHidMaxInputReportSizeTest_${TEST_CASE}" + COMMAND max_input_report_size_test "${TEST_PP_DATA}" "${TEST_REPORT_DESCRIPTOR}" + WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" + ) +endforeach() diff --git a/libusb/test/max_input_report_size_test.c b/libusb/test/max_input_report_size_test.c new file mode 100644 index 000000000..c7fc26a46 --- /dev/null +++ b/libusb/test/max_input_report_size_test.c @@ -0,0 +1,414 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "hidapi.h" +#include "../hidapi_libusb_report_descriptor.h" + +struct max_report_sizes { + size_t input; + size_t output; + size_t feature; +}; + +static int parse_expected_report_sizes(const char *filename, struct max_report_sizes *sizes) +{ + FILE *file = fopen(filename, "r"); + int found_input = 0; + int found_output = 0; + int found_feature = 0; + int has_report_id = 0; + char line[256]; + + if (!file) { + fprintf(stderr, "ERROR: Couldn't open file '%s' for reading: %s\n", filename, strerror(errno)); + return -1; + } + + while (fgets(line, sizeof(line), file)) { + unsigned int value; + + if (sscanf(line, "pp_data->caps_info[0]->ReportByteLength = %u", &value) == 1) { + sizes->input = value; + found_input = 1; + } else if (sscanf(line, "pp_data->caps_info[1]->ReportByteLength = %u", &value) == 1) { + sizes->output = value; + found_output = 1; + } else if (sscanf(line, "pp_data->caps_info[2]->ReportByteLength = %u", &value) == 1) { + sizes->feature = value; + found_feature = 1; + } else if (sscanf(line, "pp_data->cap[%*u]->ReportID = 0x%x", &value) == 1 && value != 0) { + has_report_id = 1; + } + } + + fclose(file); + + if (!found_input || !found_output || !found_feature) { + fprintf(stderr, "Missing report-size fields in '%s'\n", filename); + return -1; + } + + /* Windows includes a report-ID byte in each ReportByteLength even when + report IDs are not used. The libusb result only includes an actual ID. */ + if (!has_report_id) { + if (sizes->input) + sizes->input--; + if (sizes->output) + sizes->output--; + if (sizes->feature) + sizes->feature--; + } + + return 0; +} + +static int append_byte(unsigned char *data, size_t data_size, size_t *data_length, unsigned int value, const char *filename) +{ + if (value > 0xff) { + fprintf(stderr, "Invalid byte value 0x%x in '%s'\n", value, filename); + return -1; + } + if (*data_length >= data_size) { + fprintf(stderr, "Report descriptor in '%s' exceeds %zu bytes\n", filename, data_size); + return -1; + } + + data[(*data_length)++] = (unsigned char)value; + return 0; +} + +static int parse_hid_decode_record(char *line, unsigned char *data, size_t data_size, size_t *data_length, const char *filename) +{ + char *token = strtok(line + 2, " \t\r\n"); + char *end; + unsigned long expected_length; + + if (!token) + return -1; + + expected_length = strtoul(token, &end, 10); + if (*end != '\0' || expected_length > data_size) + return -1; + + while ((token = strtok(NULL, " \t\r\n")) != NULL) { + unsigned long value; + + if (strlen(token) != 2 || !isxdigit((unsigned char)token[0]) || !isxdigit((unsigned char)token[1])) + return -1; + + value = strtoul(token, &end, 16); + if (*end != '\0' || append_byte(data, data_size, data_length, (unsigned int)value, filename) < 0) + return -1; + } + + if (*data_length != expected_length) { + fprintf(stderr, "HID decode record in '%s' declares %lu bytes but contains %zu\n", + filename, expected_length, *data_length); + return -1; + } + + return 0; +} + +static int parse_c_hex_bytes(char *line, unsigned char *data, size_t data_size, size_t *data_length, const char *filename) +{ + char *comment = strstr(line, "//"); + char *cursor = line; + int found = 0; + + if (comment) + *comment = '\0'; + + while (isspace((unsigned char)*cursor)) + cursor++; + if (cursor[0] != '0' || cursor[1] != 'x') + return 0; + + while (*cursor) { + char *end; + unsigned long value; + + if (cursor[0] != '0' || cursor[1] != 'x') { + fprintf(stderr, "Malformed C hex byte list in '%s'\n", filename); + return -1; + } + value = strtoul(cursor + 2, &end, 16); + if (end == cursor + 2 || end - (cursor + 2) > 2 || + append_byte(data, data_size, data_length, (unsigned int)value, filename) < 0) + return -1; + found = 1; + cursor = end; + + while (isspace((unsigned char)*cursor)) + cursor++; + if (*cursor == ',') { + cursor++; + while (isspace((unsigned char)*cursor)) + cursor++; + } else if (*cursor != '\0') { + fprintf(stderr, "Malformed C hex byte list in '%s'\n", filename); + return -1; + } + } + + return found; +} + +static int parse_trailing_hex_bytes(char *line, unsigned char *data, size_t data_size, size_t *data_length, const char *filename) +{ + char *tokens[128]; + size_t token_count = 0; + size_t first_hex_token; + char *token; + + for (token = strtok(line, " \t\r\n"); token && token_count < 128; token = strtok(NULL, " \t\r\n")) + tokens[token_count++] = token; + + first_hex_token = token_count; + while (first_hex_token > 0) { + const char *candidate = tokens[first_hex_token - 1]; + if (strlen(candidate) != 2 || + !isxdigit((unsigned char)candidate[0]) || + !isxdigit((unsigned char)candidate[1])) + break; + first_hex_token--; + } + + for (size_t i = first_hex_token; i < token_count; i++) { + unsigned int value = (unsigned int)strtoul(tokens[i], NULL, 16); + if (append_byte(data, data_size, data_length, value, filename) < 0) + return -1; + } + + return first_hex_token < token_count; +} + +static bool read_report_descriptor(const char *filename, unsigned char *data, size_t data_size, size_t *data_length) +{ + char line[HID_API_MAX_REPORT_DESCRIPTOR_SIZE * 4]; + FILE *file = fopen(filename, "r"); + int found_c_hex = 0; + + if (!file) { + fprintf(stderr, "ERROR: Couldn't open file '%s' for reading: %s\n", filename, strerror(errno)); + return false; + } + *data_length = 0; + + /* hid-decode output includes a canonical raw descriptor on its R: line. + Prefer it over the preceding commented disassembly. */ + while (fgets(line, sizeof(line), file)) { + char *cursor = line; + while (isspace((unsigned char)*cursor)) + cursor++; + if (cursor[0] == 'R' && cursor[1] == ':') { + const int result = parse_hid_decode_record(cursor, data, data_size, data_length, filename); + fclose(file); + return result == 0; + } + } + + /* Several fixtures contain both a raw tool dump and a normalized + usbdescreqparser rendering. Prefer the normalized 0xNN form when it is + present so the same descriptor is not parsed twice. */ + rewind(file); + while (fgets(line, sizeof(line), file)) { + char line_copy[sizeof(line)]; + char *cursor = line; + int result; + + while (isspace((unsigned char)*cursor)) + cursor++; + if (*cursor == '#' || (*cursor == '/' && cursor[1] == '/')) + continue; + + memcpy(line_copy, line, sizeof(line_copy)); + line_copy[sizeof(line_copy) - 1] = '\0'; + result = parse_c_hex_bytes(line_copy, data, data_size, data_length, filename); + if (result < 0) { + fclose(file); + return false; + } + if (result > 0) + found_c_hex = 1; + } + if (found_c_hex) { + fclose(file); + return true; + } + + rewind(file); + while (fgets(line, sizeof(line), file)) { + char line_copy[sizeof(line)]; + char *cursor = line; + + while (isspace((unsigned char)*cursor)) + cursor++; + if (*cursor == '#' || (*cursor == '/' && cursor[1] == '/')) + continue; + + memcpy(line_copy, line, sizeof(line_copy)); + line_copy[sizeof(line_copy) - 1] = '\0'; + if (parse_trailing_hex_bytes(line_copy, data, data_size, data_length, filename) < 0) { + fclose(file); + return false; + } + } + + fclose(file); + if (*data_length == 0) { + fprintf(stderr, "No report-descriptor bytes found in '%s'\n", filename); + return false; + } + return true; +} + +static int test_report_descriptor_parser(void) +{ + char single_digit_hex[] = " 0x5, 0x0a, // valid C-style byte list"; + char stray_hex_text[] = "description mentions 0x05 but is not a byte list"; + unsigned char fixture_bytes[2]; + size_t fixture_byte_count = 0; + static const uint8_t missing_report_size[] = {0x95, 0x01, 0x81, 0x00}; + static const uint8_t truncated_item[] = {0x75}; + static const uint8_t output_only[] = {0x75, 0x08, 0x95, 0x01, 0x91, 0x00}; + static const uint8_t repeated_report_id[] = { + 0x85, 0x01, 0x75, 0x08, 0x95, 0x01, 0x81, 0x00, + 0x85, 0x02, 0x95, 0x01, 0x81, 0x00, + 0x85, 0x01, 0x95, 0x02, 0x81, 0x00, + }; + static const uint8_t push_pop[] = { + 0x75, 0x08, 0x95, 0x01, 0xa4, + 0x75, 0x10, 0x95, 0x02, 0x81, 0x00, + 0xb4, 0x81, 0x00, + }; + static const uint8_t long_item[] = { + 0x75, 0x08, 0xfe, 0x02, 0x99, 0xaa, 0xbb, + 0x95, 0x03, 0x81, 0x00, + }; + static const uint8_t four_byte_globals[] = { + 0x77, 0x08, 0x00, 0x00, 0x00, + 0x97, 0x02, 0x00, 0x00, 0x00, + 0x81, 0x00, + }; + static const uint8_t two_byte_report_id[] = { + 0x86, 0x01, 0x00, 0x75, 0x08, 0x95, 0x01, 0x81, 0x00, + }; + static const uint8_t mixed_report_id_zero[] = { + 0x75, 0x08, 0x95, 0x01, 0x81, 0x00, + 0x85, 0x01, 0x81, 0x00, + }; + static const uint8_t oversized_report[] = { + 0x75, 0x20, 0x97, 0xff, 0xff, 0xff, 0x7f, 0x81, 0x00, + }; + static const uint8_t maximum_report[] = { + 0x75, 0x08, 0x97, 0xff, 0xff, 0x00, 0x00, 0x81, 0x00, + }; + static const uint8_t over_maximum_report[] = { + 0x75, 0x08, 0x97, 0x00, 0x00, 0x01, 0x00, 0x81, 0x00, + }; + static const uint8_t accumulated_overflow[] = { + 0x77, 0xff, 0xff, 0xff, 0xff, + 0x97, 0xff, 0xff, 0xff, 0xff, + 0x81, 0x00, 0x81, 0x00, + }; + + if (parse_c_hex_bytes(single_digit_hex, fixture_bytes, sizeof(fixture_bytes), &fixture_byte_count, "self-test") != 1 || + fixture_byte_count != 2 || fixture_bytes[0] != 0x05 || fixture_bytes[1] != 0x0a || + parse_c_hex_bytes(stray_hex_text, fixture_bytes, sizeof(fixture_bytes), &fixture_byte_count, "self-test") != 0) { + fprintf(stderr, "C-style report descriptor fixture parsing failed\n"); + return -1; + } + if (get_max_report_size(missing_report_size, sizeof(missing_report_size), REPORT_DESCR_INPUT) != -1 || + get_max_report_size(truncated_item, sizeof(truncated_item), REPORT_DESCR_INPUT) != -1) { + fprintf(stderr, "Malformed report descriptor was not rejected\n"); + return -1; + } + if (get_max_report_size(output_only, sizeof(output_only), REPORT_DESCR_INPUT) != 0) { + fprintf(stderr, "Missing input report was not reported as zero-sized\n"); + return -1; + } + if (get_max_report_size(repeated_report_id, sizeof(repeated_report_id), REPORT_DESCR_INPUT) != 4) { + fprintf(stderr, "Repeated report ID fields were not accumulated correctly\n"); + return -1; + } + if (get_max_report_size(push_pop, sizeof(push_pop), REPORT_DESCR_INPUT) != 5 || + get_max_report_size(long_item, sizeof(long_item), REPORT_DESCR_INPUT) != 3 || + get_max_report_size(four_byte_globals, sizeof(four_byte_globals), REPORT_DESCR_INPUT) != 2 || + get_max_report_size(two_byte_report_id, sizeof(two_byte_report_id), REPORT_DESCR_INPUT) != 2 || + get_max_report_size(maximum_report, sizeof(maximum_report), REPORT_DESCR_INPUT) != (ssize_t)HIDAPI_LIBUSB_MAX_REPORT_SIZE) { + fprintf(stderr, "Valid global-item encodings were not parsed correctly\n"); + return -1; + } + if (get_max_report_size(mixed_report_id_zero, sizeof(mixed_report_id_zero), REPORT_DESCR_INPUT) != -1 || + get_max_report_size(oversized_report, sizeof(oversized_report), REPORT_DESCR_INPUT) != -1 || + get_max_report_size(over_maximum_report, sizeof(over_maximum_report), REPORT_DESCR_INPUT) != -1 || + get_max_report_size(accumulated_overflow, sizeof(accumulated_overflow), REPORT_DESCR_INPUT) != -1) { + fprintf(stderr, "Invalid or unsafe report descriptor was not rejected\n"); + return -1; + } + + return 0; +} + +int main(int argc, char *argv[]) +{ + unsigned char report_descriptor[HID_API_MAX_REPORT_DESCRIPTOR_SIZE]; + size_t report_descriptor_size = 0; + struct max_report_sizes expected = {0}; + struct max_report_sizes computed; + ssize_t input_size; + ssize_t output_size; + ssize_t feature_size; + int ret = EXIT_SUCCESS; + + if (argc == 2 && strcmp(argv[1], "--self-test") == 0) + return test_report_descriptor_parser() == 0 ? EXIT_SUCCESS : EXIT_FAILURE; + + if (argc != 3) { + fprintf(stderr, "Expected 2 arguments ('<>.pp_data' and '<>_real.rpt_desc'), got: %d\n", argc - 1); + return EXIT_FAILURE; + } + + printf("Checking: '%s' / '%s'\n", argv[1], argv[2]); + + if (!read_report_descriptor(argv[2], report_descriptor, sizeof(report_descriptor), &report_descriptor_size)) + return EXIT_FAILURE; + if (parse_expected_report_sizes(argv[1], &expected) < 0) + return EXIT_FAILURE; + + input_size = get_max_report_size(report_descriptor, report_descriptor_size, REPORT_DESCR_INPUT); + output_size = get_max_report_size(report_descriptor, report_descriptor_size, REPORT_DESCR_OUTPUT); + feature_size = get_max_report_size(report_descriptor, report_descriptor_size, REPORT_DESCR_FEATURE); + if (input_size < 0 || output_size < 0 || feature_size < 0) { + fprintf(stderr, "Failed to parse report descriptor '%s'\n", argv[2]); + return EXIT_FAILURE; + } + + computed.input = (size_t)input_size; + computed.output = (size_t)output_size; + computed.feature = (size_t)feature_size; + + if (expected.input != computed.input) { + fprintf(stderr, "Failed to compute input size. Got %zu, expected %zu\n", computed.input, expected.input); + ret = EXIT_FAILURE; + } + if (expected.output != computed.output) { + fprintf(stderr, "Failed to compute output size. Got %zu, expected %zu\n", computed.output, expected.output); + ret = EXIT_FAILURE; + } + if (expected.feature != computed.feature) { + fprintf(stderr, "Failed to compute feature size. Got %zu, expected %zu\n", computed.feature, expected.feature); + ret = EXIT_FAILURE; + } + + if (ret == EXIT_SUCCESS) + printf("Computed report sizes: %zu, %zu, %zu\n", computed.input, computed.output, computed.feature); + + return ret; +}