// SPDX-License-Identifier: GPL-3.0-or-later // Host test for the keyboard report reader used by the Bluetooth keyboard. // c++ -std=c++17 -Wall -Wextra -I src test/test_hid_report_map.cpp -o /tmp/hid && /tmp/hid #include #include #include #include "helpers/input/HidReportMap.h" // USB HID 1.11, appendix E.6: the boot keyboard layout, no report ids. static const uint8_t kSpecKeyboard[] = { 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00, 0xC0, }; // The usual Bluetooth keyboard: keyboard on id 1, media keys on id 2. static const uint8_t kBleKeyboard[] = { 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x85, 0x01, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00, 0xC0, 0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x02, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x10, 0x09, 0xB5, 0x09, 0xB6, 0x09, 0xB7, 0x09, 0xCD, 0x09, 0xE2, 0x09, 0xE9, 0x09, 0xEA, 0x0A, 0x23, 0x02, 0x0A, 0x94, 0x01, 0x0A, 0x92, 0x01, 0x0A, 0x2A, 0x02, 0x0A, 0x21, 0x02, 0x0A, 0x26, 0x02, 0x0A, 0x24, 0x02, 0x0A, 0x83, 0x01, 0x0A, 0x8A, 0x01, 0x81, 0x02, 0xC0, }; // Full-key rollover: modifiers, LEDs, then one bit per usage 0x00-0xEF (id 6). static const uint8_t kNkroKeyboard[] = { 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x85, 0x06, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x95, 0x08, 0x75, 0x01, 0x81, 0x02, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x95, 0x05, 0x75, 0x01, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x03, 0x05, 0x07, 0x19, 0x00, 0x29, 0xEF, 0x15, 0x00, 0x25, 0x01, 0x95, 0xF0, 0x75, 0x01, 0x81, 0x02, 0xC0, }; // Keyboard (id 1, no LEDs) next to a touchpad whose report is also 8 bytes (id 3). static const uint8_t kKeyboardAndTouchpad[] = { 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x85, 0x01, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65, 0x81, 0x00, 0xC0, 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x85, 0x03, 0x09, 0x01, 0xA1, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x05, 0x15, 0x00, 0x25, 0x01, 0x95, 0x05, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, 0x03, 0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x16, 0x01, 0x80, 0x26, 0xFF, 0x7F, 0x75, 0x10, 0x95, 0x02, 0x81, 0x06, 0x09, 0x38, 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x01, 0x81, 0x06, 0x05, 0x0C, 0x0A, 0x38, 0x02, 0x95, 0x01, 0x81, 0x06, 0x95, 0x02, 0x81, 0x01, 0xC0, 0xC0, }; // Oddities seen in the wild: modifiers as a list of single usages, and a key // array whose "Logical Maximum (255)" was written as the signed byte 0xFF. static const uint8_t kQuirkyKeyboard[] = { 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x05, 0x07, 0x09, 0xE0, 0x09, 0xE1, 0x09, 0xE2, 0x09, 0xE3, 0x09, 0xE4, 0x09, 0xE5, 0x09, 0xE6, 0x09, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x75, 0x08, 0x95, 0x01, 0x81, 0x03, 0x15, 0x00, 0x25, 0xFF, 0x19, 0x00, 0x29, 0xFF, 0x75, 0x08, 0x95, 0x05, 0x81, 0x00, 0xC0, }; // A real keyboard (Sologic, 2026-09): five key slots and a vendor byte in // report 2, media keys as a 16-bit array in report 3. static const uint8_t kSologicKeyboard[] = { 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x85, 0x02, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, 0x75, 0x08, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x05, 0x07, 0x19, 0x00, 0x29, 0xFF, 0x81, 0x00, 0x05, 0xFF, 0x09, 0x03, 0x75, 0x08, 0x95, 0x01, 0x81, 0x02, 0x95, 0x05, 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x03, 0xC0, 0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x03, 0x75, 0x10, 0x95, 0x01, 0x15, 0x00, 0x26, 0x80, 0x03, 0x19, 0x00, 0x2A, 0x80, 0x03, 0x81, 0x00, 0xC0, }; // Only media keys: no keyboard report at all. static const uint8_t kMediaOnly[] = { 0x05, 0x0C, 0x09, 0x01, 0xA1, 0x01, 0x85, 0x02, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x02, 0x09, 0xB5, 0x09, 0xB6, 0x81, 0x02, 0x95, 0x06, 0x81, 0x01, 0xC0, }; static void expectKeys(const uint8_t keys[6], const uint8_t expect[6]) { for (int i = 0; i < 6; i++) { if (keys[i] != expect[i]) { printf("key %d: got 0x%02X, want 0x%02X\n", i, keys[i], expect[i]); assert(false); } } } static void testSpecKeyboard() { HidReportMapInfo info; assert(hidParseReportMap(kSpecKeyboard, sizeof kSpecKeyboard, &info)); assert(info.kbd_count == 1); const HidKeyboardReport& k = info.kbd[0]; assert(k.report_id == 0); assert(k.mod_bit == 0); assert(k.keys_bit == 16 && k.keys_count == 6 && k.keys_size == 8); assert(k.bitmap_bit < 0); assert(k.bits == 64); assert(info.have_leds && info.leds.report_id == 0); assert(info.leds.num_bit == 0 && info.leds.caps_bit == 1 && info.leds.bits == 8); const uint8_t shift_a[8] = {0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00}; uint8_t mod = 0xFF, keys[6]; assert(hidDecodeKeyboard(k, shift_a, sizeof shift_a, &mod, keys)); assert(mod == 0x02); const uint8_t want[6] = {0x04}; expectKeys(keys, want); const uint8_t rollover[8] = {0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}; assert(!hidDecodeKeyboard(k, rollover, sizeof rollover, &mod, keys)); const uint8_t shorter[3] = {0x00, 0x00, 0x04}; assert(!hidDecodeKeyboard(k, shorter, sizeof shorter, &mod, keys)); } static void testBleKeyboard() { HidReportMapInfo info; assert(hidParseReportMap(kBleKeyboard, sizeof kBleKeyboard, &info)); assert(info.kbd_count == 1); // the media keys are not a keyboard report const HidKeyboardReport& k = info.kbd[0]; assert(k.report_id == 1); assert(k.mod_bit == 0 && k.keys_bit == 16 && k.bits == 64); assert(info.have_leds && info.leds.report_id == 1 && info.leds.caps_bit == 1); // "y", then "y" and "u" together. uint8_t mod = 0, keys[6]; const uint8_t y[8] = {0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00}; assert(hidDecodeKeyboard(k, y, sizeof y, &mod, keys)); const uint8_t want_y[6] = {0x1C}; expectKeys(keys, want_y); const uint8_t yu[8] = {0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x00, 0x00}; assert(hidDecodeKeyboard(k, yu, sizeof yu, &mod, keys)); const uint8_t want_yu[6] = {0x1C, 0x18}; expectKeys(keys, want_yu); uint8_t led[4]; assert(hidEncodeLeds(info.leds, false, true, led, sizeof led) == 1 && led[0] == 0x02); assert(hidEncodeLeds(info.leds, true, true, led, sizeof led) == 1 && led[0] == 0x03); assert(hidEncodeLeds(info.leds, false, false, led, sizeof led) == 1 && led[0] == 0x00); } static void testSologicKeyboard() { assert(sizeof kSologicKeyboard == 101); HidReportMapInfo info; assert(hidParseReportMap(kSologicKeyboard, sizeof kSologicKeyboard, &info)); assert(info.kbd_count == 1); const HidKeyboardReport& k = info.kbd[0]; assert(k.report_id == 2); assert(k.mod_bit == 0); assert(k.keys_bit == 16 && k.keys_count == 5 && k.keys_size == 8); assert(k.keys_lmin == 0 && k.keys_lmax == 255 && k.keys_umin == 0); assert(k.bitmap_bit < 0); assert(k.bits == 64); assert(info.have_leds && info.leds.report_id == 2); assert(info.leds.num_bit == 0 && info.leds.caps_bit == 1 && info.leds.bits == 8); // "y" and "u" held (as captured), then a report with only the vendor byte set, // which is not a key. uint8_t mod = 0, keys[6]; const uint8_t yu[8] = {0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x00, 0x00}; assert(hidDecodeKeyboard(k, yu, sizeof yu, &mod, keys)); const uint8_t want_yu[6] = {0x1C, 0x18}; expectKeys(keys, want_yu); const uint8_t vendor[8] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5A}; assert(hidDecodeKeyboard(k, vendor, sizeof vendor, &mod, keys)); assert(mod == 0x02); const uint8_t none[6] = {}; expectKeys(keys, none); } static void testNkroKeyboard() { HidReportMapInfo info; assert(hidParseReportMap(kNkroKeyboard, sizeof kNkroKeyboard, &info)); assert(info.kbd_count == 1); const HidKeyboardReport& k = info.kbd[0]; assert(k.report_id == 6); assert(k.mod_bit == 0); assert(k.keys_bit < 0); assert(k.bitmap_bit == 8 && k.bitmap_first == 0 && k.bitmap_count == 240); assert(k.bits == 248); assert(info.have_leds && info.leds.report_id == 6 && info.leds.num_bit == 0 && info.leds.caps_bit == 1); uint8_t report[31] = {}; report[0] = 0x02; // left shift report[(8 + 0x04) / 8] |= 1 << ((8 + 0x04) % 8); // a report[(8 + 0x1E) / 8] |= 1 << ((8 + 0x1E) % 8); // 1 report[(8 + 0xE4) / 8] |= 1 << ((8 + 0xE4) % 8); // right ctrl, as a bitmap bit uint8_t mod = 0, keys[6]; assert(hidDecodeKeyboard(k, report, sizeof report, &mod, keys)); assert(mod == (0x02 | 0x10)); const uint8_t want[6] = {0x04, 0x1E}; expectKeys(keys, want); // Eight keys down: the first six are kept. uint8_t many[31] = {}; for (int u = 0x04; u < 0x0C; u++) many[(8 + u) / 8] |= (uint8_t)(1 << ((8 + u) % 8)); assert(hidDecodeKeyboard(k, many, sizeof many, &mod, keys)); const uint8_t want_many[6] = {0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; expectKeys(keys, want_many); // Cut short (a notification limited to 20 bytes): the bitmap does not fit. assert(!hidDecodeKeyboard(k, report, 20, &mod, keys)); } static void testKeyboardAndTouchpad() { HidReportMapInfo info; assert(hidParseReportMap(kKeyboardAndTouchpad, sizeof kKeyboardAndTouchpad, &info)); assert(info.kbd_count == 1); assert(info.kbd[0].report_id == 1); assert(info.kbd[0].bits == 64); assert(!info.have_leds); } static void testQuirkyKeyboard() { HidReportMapInfo info; assert(hidParseReportMap(kQuirkyKeyboard, sizeof kQuirkyKeyboard, &info)); assert(info.kbd_count == 1); const HidKeyboardReport& k = info.kbd[0]; assert(k.mod_bit == 0); assert(k.keys_bit == 16 && k.keys_count == 5); assert(k.keys_lmin == 0 && k.keys_lmax == 255); assert(k.bits == 56); const uint8_t report[7] = {0x40, 0x00, 0x14, 0xE1, 0x00, 0x00, 0x00}; // AltGr + q, shift as an entry uint8_t mod = 0, keys[6]; assert(hidDecodeKeyboard(k, report, sizeof report, &mod, keys)); assert(mod == (0x40 | 0x02)); const uint8_t want[6] = {0x14}; expectKeys(keys, want); } static void testNotKeyboards() { HidReportMapInfo info; assert(!hidParseReportMap(kMediaOnly, sizeof kMediaOnly, &info)); assert(info.kbd_count == 0); assert(!hidParseReportMap(nullptr, 0, &info)); // Every truncation of a valid map parses without reading past its end. for (size_t n = 0; n < sizeof kBleKeyboard; n++) { uint8_t buf[sizeof kBleKeyboard]; memcpy(buf, kBleKeyboard, n); hidParseReportMap(buf, n, &info); assert(info.kbd_count <= HidReportMapInfo::kMaxKeyboards); } // Garbage never yields a layout that points outside a 64-byte report and // then decodes past it. uint32_t seed = 12345; for (int round = 0; round < 20000; round++) { uint8_t buf[48]; for (uint8_t& b : buf) { seed = seed * 1103515245u + 12345u; b = (uint8_t)(seed >> 16); } if (!hidParseReportMap(buf, sizeof buf, &info)) continue; uint8_t report[64] = {}; uint8_t mod, keys[6]; for (int i = 0; i < info.kbd_count; i++) hidDecodeKeyboard(info.kbd[i], report, sizeof report, &mod, keys); } } int main() { testSpecKeyboard(); testBleKeyboard(); testSologicKeyboard(); testNkroKeyboard(); testKeyboardAndTouchpad(); testQuirkyKeyboard(); testNotKeyboards(); printf("hid report map: ok\n"); return 0; }