diff --git a/firmware_p4/components/Applications/bad_usb/README.md b/firmware_p4/components/Applications/bad_usb/README.md index b89fc708..473fe3ad 100644 --- a/firmware_p4/components/Applications/bad_usb/README.md +++ b/firmware_p4/components/Applications/bad_usb/README.md @@ -75,11 +75,19 @@ void hid_hal_wait_for_connection(void); ### Keyboard Layouts (`hid_layouts.h`) ```c -void hid_layouts_type_string_us(const char *str); -void hid_layouts_type_string_abnt2(const char *str); +void hid_layouts_type_string(ducky_layout_t layout, const char *str); ``` -- `hid_layouts_type_string_us` maps ASCII characters to US keyboard HID keycodes. -- `hid_layouts_type_string_abnt2` handles Brazilian Portuguese layout including UTF-8 dead-key sequences for accented characters (e.g. a, e, c, a, o). +- Types `str` with the given layout, emitting one or more `hid_hal_press_key` + calls per character. Out-of-range `layout` values fall back to `DUCKY_LAYOUT_US`. +- Layouts are **pure data** (defined in `hid_layouts.c`): each is a 128-entry + table indexed directly by the ASCII byte — `{keycode, modifier, dead_keycode, + dead_modifier}` — plus an optional supplemental table for UTF-8 two-byte + sequences (accented characters). A single generic walker drives every layout, + so adding a layout adds no new code path. +- **Dead keys** (e.g. ABNT2 accents, `'`, `"`) are encoded as an optional prefix + press in the entry: `dead_keycode` is pressed before the base `keycode`. An + all-zero entry (or no table match) is an unmapped character and produces no + output. ### DuckyScript Parser (`ducky_parser.h`) @@ -132,3 +140,14 @@ Modifier keys can be combined: `CTRL SHIFT ESC`, `GUI r`, `ALT F4`. | US (QWERTY) | `DUCKY_LAYOUT_US` | Default. Standard ASCII mapping. | | ABNT2 (Brazil) | `DUCKY_LAYOUT_ABNT2` | Dead-key accent support, remapped punctuation. | +### Registering a new layout + +Adding a layout is pure data — no changes to the parser or dispatch: + +1. Add the enum value to `ducky_layout_t` in `ducky_parser.h` (before + `DUCKY_LAYOUT_COUNT`). +2. Declare a `static const hid_key_entry_t s_ascii_[128]` table in + `hid_layouts.c` (and an optional `s_utf8_[]` for accented characters). +3. Add one row to the `s_layouts[]` registry, indexed by the new enum value. +4. (Optional) add a selectable entry in the BadUSB layout UI + (`ui/screens/badusb/ui_badusb_layout.c`). diff --git a/firmware_p4/components/Applications/bad_usb/ducky_parser.c b/firmware_p4/components/Applications/bad_usb/ducky_parser.c index a3c7c93f..4cb4a0ac 100644 --- a/firmware_p4/components/Applications/bad_usb/ducky_parser.c +++ b/firmware_p4/components/Applications/bad_usb/ducky_parser.c @@ -280,11 +280,7 @@ static void process_line(char *line) { } else if (strcmp(cmd, "STRING") == 0) { char *text = strtok_r(NULL, "", &saveptr); if (text != NULL) { - if (s_layout == DUCKY_LAYOUT_ABNT2) { - hid_layouts_type_string_abnt2(text); - } else { - hid_layouts_type_string_us(text); - } + hid_layouts_type_string(s_layout, text); } } else if (strcmp(cmd, "MOUSE_MOVE") == 0) { char *arg_x = strtok_r(NULL, " ", &saveptr); diff --git a/firmware_p4/components/Applications/bad_usb/hid_layouts.c b/firmware_p4/components/Applications/bad_usb/hid_layouts.c index 01c5ff61..509cf6ad 100644 --- a/firmware_p4/components/Applications/bad_usb/hid_layouts.c +++ b/firmware_p4/components/Applications/bad_usb/hid_layouts.c @@ -15,15 +15,13 @@ #include "hid_layouts.h" -#include +#include +#include -#include "esp_log.h" #include "class/hid/hid_device.h" #include "hid_hal.h" -static const char *TAG = "HID_LAYOUTS"; - #ifndef HID_KEY_INTERNATIONAL_1 #define HID_KEY_INTERNATIONAL_1 0x87 #endif @@ -32,7 +30,12 @@ static const char *TAG = "HID_LAYOUTS"; #define HID_KEY_NON_US_BACKSLASH 0x64 #endif -// ABNT2 UTF-8 byte pairs for accented characters +// Short aliases to keep the layout tables readable. +#define MOD_SHIFT KEYBOARD_MODIFIER_LEFTSHIFT +#define MOD_ALTGR KEYBOARD_MODIFIER_RIGHTALT + +// ABNT2 UTF-8 byte pairs for accented characters. The lead byte is always +// 0xC3; the values below are the second (trailing) byte of each sequence. #define UTF8_LOWER_C_CEDILLA_B2 0xA7 // c = 0xC3 0xA7 #define UTF8_UPPER_C_CEDILLA_B2 0x87 // C = 0xC3 0x87 #define UTF8_LOWER_A_ACUTE_B2 0xA1 // a = 0xC3 0xA1 @@ -48,343 +51,262 @@ static const char *TAG = "HID_LAYOUTS"; #define UTF8_LOWER_A_GRAVE_B2 0xA0 // a = 0xC3 0xA0 #define UTF8_2BYTE_LEAD 0xC3 -static bool try_decode_abnt2_utf8(uint8_t c1, uint8_t c2); +// A single character's HID translation. A "dead key" (e.g. an accent) is +// expressed as an optional prefix press that is sent before the base key: +// type_entry() presses (dead_keycode, dead_modifier) first when non-zero, +// then (keycode, modifier). An all-zero entry is an unmapped character and +// produces no output. +typedef struct { + uint8_t keycode; + uint8_t modifier; + uint8_t dead_keycode; + uint8_t dead_modifier; +} hid_key_entry_t; -void hid_layouts_type_string_us(const char *str) { - for (size_t i = 0; str[i] != '\0'; ++i) { - char c = str[i]; - uint8_t keycode = 0; - uint8_t modifier = 0; - - if (c >= 'a' && c <= 'z') { - keycode = HID_KEY_A + (c - 'a'); - } else if (c >= 'A' && c <= 'Z') { - keycode = HID_KEY_A + (c - 'A'); - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - } else if (c >= '1' && c <= '9') { - keycode = HID_KEY_1 + (c - '1'); - } else if (c == '0') { - keycode = HID_KEY_0; - } else { - switch (c) { - case ' ': - keycode = HID_KEY_SPACE; - break; - case '!': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_1; - break; - case '@': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_2; - break; - case '#': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_3; - break; - case '$': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_4; - break; - case '%': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_5; - break; - case '^': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_6; - break; - case '&': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_7; - break; - case '*': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_8; - break; - case '(': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_9; - break; - case ')': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_0; - break; - case '-': - keycode = HID_KEY_MINUS; - break; - case '_': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_MINUS; - break; - case '=': - keycode = HID_KEY_EQUAL; - break; - case '+': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_EQUAL; - break; - case '.': - keycode = HID_KEY_PERIOD; - break; - case ',': - keycode = HID_KEY_COMMA; - break; - case '/': - keycode = HID_KEY_SLASH; - break; - case '?': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_SLASH; - break; - case ':': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_SEMICOLON; - break; - case ';': - keycode = HID_KEY_SEMICOLON; - break; - default: - break; - } - } +// A UTF-8 two-byte sequence (b0, b1) mapped to a key entry. +typedef struct { + uint8_t b0; + uint8_t b1; + hid_key_entry_t entry; +} hid_utf8_entry_t; - if (keycode != 0) { - hid_hal_press_key(keycode, modifier); - } - } -} +// A keyboard layout: a 128-entry ASCII table indexed directly by byte value, +// plus an optional supplemental table for UTF-8 two-byte sequences. +typedef struct { + const hid_key_entry_t *ascii; // 128 entries + const hid_utf8_entry_t *utf8; // NULL when the layout has no UTF-8 entries + size_t utf8_count; +} hid_layout_t; -void hid_layouts_type_string_abnt2(const char *str) { - for (size_t i = 0; str[i] != '\0'; ++i) { - uint8_t c1 = (uint8_t)str[i]; - uint8_t c2 = (uint8_t)str[i + 1]; +// --------------------------------------------------------------------------- +// US (QWERTY) layout +// --------------------------------------------------------------------------- +static const hid_key_entry_t s_ascii_us[128] = { + [' '] = {HID_KEY_SPACE, 0, 0, 0}, - // Single quote -> dead key acute + space - if (c1 == '\'') { - hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); - hid_hal_press_key(HID_KEY_SPACE, 0); - continue; - } + ['a'] = {HID_KEY_A, 0, 0, 0}, ['b'] = {HID_KEY_B, 0, 0, 0}, + ['c'] = {HID_KEY_C, 0, 0, 0}, ['d'] = {HID_KEY_D, 0, 0, 0}, + ['e'] = {HID_KEY_E, 0, 0, 0}, ['f'] = {HID_KEY_F, 0, 0, 0}, + ['g'] = {HID_KEY_G, 0, 0, 0}, ['h'] = {HID_KEY_H, 0, 0, 0}, + ['i'] = {HID_KEY_I, 0, 0, 0}, ['j'] = {HID_KEY_J, 0, 0, 0}, + ['k'] = {HID_KEY_K, 0, 0, 0}, ['l'] = {HID_KEY_L, 0, 0, 0}, + ['m'] = {HID_KEY_M, 0, 0, 0}, ['n'] = {HID_KEY_N, 0, 0, 0}, + ['o'] = {HID_KEY_O, 0, 0, 0}, ['p'] = {HID_KEY_P, 0, 0, 0}, + ['q'] = {HID_KEY_Q, 0, 0, 0}, ['r'] = {HID_KEY_R, 0, 0, 0}, + ['s'] = {HID_KEY_S, 0, 0, 0}, ['t'] = {HID_KEY_T, 0, 0, 0}, + ['u'] = {HID_KEY_U, 0, 0, 0}, ['v'] = {HID_KEY_V, 0, 0, 0}, + ['w'] = {HID_KEY_W, 0, 0, 0}, ['x'] = {HID_KEY_X, 0, 0, 0}, + ['y'] = {HID_KEY_Y, 0, 0, 0}, ['z'] = {HID_KEY_Z, 0, 0, 0}, - // Double quote -> dead key acute + shift - if (c1 == '"') { - hid_hal_press_key(HID_KEY_BRACKET_LEFT, KEYBOARD_MODIFIER_LEFTSHIFT); - continue; - } + ['A'] = {HID_KEY_A, MOD_SHIFT, 0, 0}, ['B'] = {HID_KEY_B, MOD_SHIFT, 0, 0}, + ['C'] = {HID_KEY_C, MOD_SHIFT, 0, 0}, ['D'] = {HID_KEY_D, MOD_SHIFT, 0, 0}, + ['E'] = {HID_KEY_E, MOD_SHIFT, 0, 0}, ['F'] = {HID_KEY_F, MOD_SHIFT, 0, 0}, + ['G'] = {HID_KEY_G, MOD_SHIFT, 0, 0}, ['H'] = {HID_KEY_H, MOD_SHIFT, 0, 0}, + ['I'] = {HID_KEY_I, MOD_SHIFT, 0, 0}, ['J'] = {HID_KEY_J, MOD_SHIFT, 0, 0}, + ['K'] = {HID_KEY_K, MOD_SHIFT, 0, 0}, ['L'] = {HID_KEY_L, MOD_SHIFT, 0, 0}, + ['M'] = {HID_KEY_M, MOD_SHIFT, 0, 0}, ['N'] = {HID_KEY_N, MOD_SHIFT, 0, 0}, + ['O'] = {HID_KEY_O, MOD_SHIFT, 0, 0}, ['P'] = {HID_KEY_P, MOD_SHIFT, 0, 0}, + ['Q'] = {HID_KEY_Q, MOD_SHIFT, 0, 0}, ['R'] = {HID_KEY_R, MOD_SHIFT, 0, 0}, + ['S'] = {HID_KEY_S, MOD_SHIFT, 0, 0}, ['T'] = {HID_KEY_T, MOD_SHIFT, 0, 0}, + ['U'] = {HID_KEY_U, MOD_SHIFT, 0, 0}, ['V'] = {HID_KEY_V, MOD_SHIFT, 0, 0}, + ['W'] = {HID_KEY_W, MOD_SHIFT, 0, 0}, ['X'] = {HID_KEY_X, MOD_SHIFT, 0, 0}, + ['Y'] = {HID_KEY_Y, MOD_SHIFT, 0, 0}, ['Z'] = {HID_KEY_Z, MOD_SHIFT, 0, 0}, - // Try UTF-8 two-byte sequences (accented characters) - if ((c1 & 0xE0) == 0xC0 && c2 != '\0') { - if (try_decode_abnt2_utf8(c1, c2)) { - i++; // Skip second byte - continue; - } - } + ['1'] = {HID_KEY_1, 0, 0, 0}, ['2'] = {HID_KEY_2, 0, 0, 0}, + ['3'] = {HID_KEY_3, 0, 0, 0}, ['4'] = {HID_KEY_4, 0, 0, 0}, + ['5'] = {HID_KEY_5, 0, 0, 0}, ['6'] = {HID_KEY_6, 0, 0, 0}, + ['7'] = {HID_KEY_7, 0, 0, 0}, ['8'] = {HID_KEY_8, 0, 0, 0}, + ['9'] = {HID_KEY_9, 0, 0, 0}, ['0'] = {HID_KEY_0, 0, 0, 0}, - uint8_t keycode = 0; - uint8_t modifier = 0; - - if (c1 >= 'a' && c1 <= 'z') { - keycode = HID_KEY_A + (c1 - 'a'); - } else if (c1 >= 'A' && c1 <= 'Z') { - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_A + (c1 - 'A'); - } else if (c1 >= '1' && c1 <= '9') { - keycode = HID_KEY_1 + (c1 - '1'); - } else if (c1 == '0') { - keycode = HID_KEY_0; - } else { - switch (c1) { - case '!': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_1; - break; - case '@': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_2; - break; - case '#': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_3; - break; - case '$': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_4; - break; - case '%': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_5; - break; - case '&': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_7; - break; - case '*': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_8; - break; - case '(': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_9; - break; - case ')': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_0; - break; - case ' ': - keycode = HID_KEY_SPACE; - break; - case '\n': - keycode = HID_KEY_ENTER; - break; - case '\t': - keycode = HID_KEY_TAB; - break; - case '-': - keycode = HID_KEY_MINUS; - break; - case '=': - keycode = HID_KEY_EQUAL; - break; - case '_': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_MINUS; - break; - case '+': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_EQUAL; - break; - case '.': - keycode = HID_KEY_PERIOD; - break; - case ',': - keycode = HID_KEY_COMMA; - break; - case ';': - keycode = HID_KEY_SLASH; - break; - case ':': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_SLASH; - break; - case '/': - keycode = HID_KEY_INTERNATIONAL_1; - break; - case '?': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_INTERNATIONAL_1; - break; - case '[': - modifier = KEYBOARD_MODIFIER_RIGHTALT; - keycode = HID_KEY_BRACKET_LEFT; - break; - case '{': - modifier = KEYBOARD_MODIFIER_RIGHTALT | KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_BRACKET_LEFT; - break; - case ']': - modifier = KEYBOARD_MODIFIER_RIGHTALT; - keycode = HID_KEY_BRACKET_RIGHT; - break; - case '}': - modifier = KEYBOARD_MODIFIER_RIGHTALT | KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_BRACKET_RIGHT; - break; - case '\\': - keycode = HID_KEY_NON_US_BACKSLASH; - break; - case '|': - modifier = KEYBOARD_MODIFIER_LEFTSHIFT; - keycode = HID_KEY_NON_US_BACKSLASH; - break; - default: - break; - } - } + ['!'] = {HID_KEY_1, MOD_SHIFT, 0, 0}, ['@'] = {HID_KEY_2, MOD_SHIFT, 0, 0}, + ['#'] = {HID_KEY_3, MOD_SHIFT, 0, 0}, ['$'] = {HID_KEY_4, MOD_SHIFT, 0, 0}, + ['%'] = {HID_KEY_5, MOD_SHIFT, 0, 0}, ['^'] = {HID_KEY_6, MOD_SHIFT, 0, 0}, + ['&'] = {HID_KEY_7, MOD_SHIFT, 0, 0}, ['*'] = {HID_KEY_8, MOD_SHIFT, 0, 0}, + ['('] = {HID_KEY_9, MOD_SHIFT, 0, 0}, [')'] = {HID_KEY_0, MOD_SHIFT, 0, 0}, - if (keycode != 0) { - hid_hal_press_key(keycode, modifier); - } - } -} + ['-'] = {HID_KEY_MINUS, 0, 0, 0}, ['_'] = {HID_KEY_MINUS, MOD_SHIFT, 0, 0}, + ['='] = {HID_KEY_EQUAL, 0, 0, 0}, ['+'] = {HID_KEY_EQUAL, MOD_SHIFT, 0, 0}, + ['.'] = {HID_KEY_PERIOD, 0, 0, 0}, [','] = {HID_KEY_COMMA, 0, 0, 0}, + ['/'] = {HID_KEY_SLASH, 0, 0, 0}, ['?'] = {HID_KEY_SLASH, MOD_SHIFT, 0, 0}, + [';'] = {HID_KEY_SEMICOLON, 0, 0, 0}, [':'] = {HID_KEY_SEMICOLON, MOD_SHIFT, 0, 0}, +}; -static bool try_decode_abnt2_utf8(uint8_t c1, uint8_t c2) { - if (c1 != UTF8_2BYTE_LEAD) { - return false; - } +// --------------------------------------------------------------------------- +// Brazilian ABNT2 layout +// --------------------------------------------------------------------------- +static const hid_key_entry_t s_ascii_abnt2[128] = { + [' '] = {HID_KEY_SPACE, 0, 0, 0}, + ['\n'] = {HID_KEY_ENTER, 0, 0, 0}, + ['\t'] = {HID_KEY_TAB, 0, 0, 0}, - // Cedilla - if (c2 == UTF8_LOWER_C_CEDILLA_B2) { - hid_hal_press_key(HID_KEY_SEMICOLON, 0); - return true; - } - if (c2 == UTF8_UPPER_C_CEDILLA_B2) { - hid_hal_press_key(HID_KEY_SEMICOLON, KEYBOARD_MODIFIER_LEFTSHIFT); - return true; - } + ['a'] = {HID_KEY_A, 0, 0, 0}, + ['b'] = {HID_KEY_B, 0, 0, 0}, + ['c'] = {HID_KEY_C, 0, 0, 0}, + ['d'] = {HID_KEY_D, 0, 0, 0}, + ['e'] = {HID_KEY_E, 0, 0, 0}, + ['f'] = {HID_KEY_F, 0, 0, 0}, + ['g'] = {HID_KEY_G, 0, 0, 0}, + ['h'] = {HID_KEY_H, 0, 0, 0}, + ['i'] = {HID_KEY_I, 0, 0, 0}, + ['j'] = {HID_KEY_J, 0, 0, 0}, + ['k'] = {HID_KEY_K, 0, 0, 0}, + ['l'] = {HID_KEY_L, 0, 0, 0}, + ['m'] = {HID_KEY_M, 0, 0, 0}, + ['n'] = {HID_KEY_N, 0, 0, 0}, + ['o'] = {HID_KEY_O, 0, 0, 0}, + ['p'] = {HID_KEY_P, 0, 0, 0}, + ['q'] = {HID_KEY_Q, 0, 0, 0}, + ['r'] = {HID_KEY_R, 0, 0, 0}, + ['s'] = {HID_KEY_S, 0, 0, 0}, + ['t'] = {HID_KEY_T, 0, 0, 0}, + ['u'] = {HID_KEY_U, 0, 0, 0}, + ['v'] = {HID_KEY_V, 0, 0, 0}, + ['w'] = {HID_KEY_W, 0, 0, 0}, + ['x'] = {HID_KEY_X, 0, 0, 0}, + ['y'] = {HID_KEY_Y, 0, 0, 0}, + ['z'] = {HID_KEY_Z, 0, 0, 0}, - // Acute accent (dead key = BRACKET_LEFT) - if (c2 == UTF8_LOWER_A_ACUTE_B2) { - hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); - hid_hal_press_key(HID_KEY_A, 0); - return true; - } - if (c2 == UTF8_LOWER_E_ACUTE_B2) { - hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); - hid_hal_press_key(HID_KEY_E, 0); - return true; - } - if (c2 == UTF8_LOWER_I_ACUTE_B2) { - hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); - hid_hal_press_key(HID_KEY_I, 0); - return true; - } - if (c2 == UTF8_LOWER_O_ACUTE_B2) { - hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); - hid_hal_press_key(HID_KEY_O, 0); - return true; - } - if (c2 == UTF8_LOWER_U_ACUTE_B2) { - hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); - hid_hal_press_key(HID_KEY_U, 0); - return true; - } + ['A'] = {HID_KEY_A, MOD_SHIFT, 0, 0}, + ['B'] = {HID_KEY_B, MOD_SHIFT, 0, 0}, + ['C'] = {HID_KEY_C, MOD_SHIFT, 0, 0}, + ['D'] = {HID_KEY_D, MOD_SHIFT, 0, 0}, + ['E'] = {HID_KEY_E, MOD_SHIFT, 0, 0}, + ['F'] = {HID_KEY_F, MOD_SHIFT, 0, 0}, + ['G'] = {HID_KEY_G, MOD_SHIFT, 0, 0}, + ['H'] = {HID_KEY_H, MOD_SHIFT, 0, 0}, + ['I'] = {HID_KEY_I, MOD_SHIFT, 0, 0}, + ['J'] = {HID_KEY_J, MOD_SHIFT, 0, 0}, + ['K'] = {HID_KEY_K, MOD_SHIFT, 0, 0}, + ['L'] = {HID_KEY_L, MOD_SHIFT, 0, 0}, + ['M'] = {HID_KEY_M, MOD_SHIFT, 0, 0}, + ['N'] = {HID_KEY_N, MOD_SHIFT, 0, 0}, + ['O'] = {HID_KEY_O, MOD_SHIFT, 0, 0}, + ['P'] = {HID_KEY_P, MOD_SHIFT, 0, 0}, + ['Q'] = {HID_KEY_Q, MOD_SHIFT, 0, 0}, + ['R'] = {HID_KEY_R, MOD_SHIFT, 0, 0}, + ['S'] = {HID_KEY_S, MOD_SHIFT, 0, 0}, + ['T'] = {HID_KEY_T, MOD_SHIFT, 0, 0}, + ['U'] = {HID_KEY_U, MOD_SHIFT, 0, 0}, + ['V'] = {HID_KEY_V, MOD_SHIFT, 0, 0}, + ['W'] = {HID_KEY_W, MOD_SHIFT, 0, 0}, + ['X'] = {HID_KEY_X, MOD_SHIFT, 0, 0}, + ['Y'] = {HID_KEY_Y, MOD_SHIFT, 0, 0}, + ['Z'] = {HID_KEY_Z, MOD_SHIFT, 0, 0}, - // Circumflex accent (dead key = APOSTROPHE + SHIFT) - if (c2 == UTF8_LOWER_A_CIRCUM_B2) { - hid_hal_press_key(HID_KEY_APOSTROPHE, KEYBOARD_MODIFIER_LEFTSHIFT); - hid_hal_press_key(HID_KEY_A, 0); - return true; - } - if (c2 == UTF8_LOWER_E_CIRCUM_B2) { - hid_hal_press_key(HID_KEY_APOSTROPHE, KEYBOARD_MODIFIER_LEFTSHIFT); - hid_hal_press_key(HID_KEY_E, 0); - return true; - } - if (c2 == UTF8_LOWER_O_CIRCUM_B2) { - hid_hal_press_key(HID_KEY_APOSTROPHE, KEYBOARD_MODIFIER_LEFTSHIFT); - hid_hal_press_key(HID_KEY_O, 0); - return true; - } + ['1'] = {HID_KEY_1, 0, 0, 0}, + ['2'] = {HID_KEY_2, 0, 0, 0}, + ['3'] = {HID_KEY_3, 0, 0, 0}, + ['4'] = {HID_KEY_4, 0, 0, 0}, + ['5'] = {HID_KEY_5, 0, 0, 0}, + ['6'] = {HID_KEY_6, 0, 0, 0}, + ['7'] = {HID_KEY_7, 0, 0, 0}, + ['8'] = {HID_KEY_8, 0, 0, 0}, + ['9'] = {HID_KEY_9, 0, 0, 0}, + ['0'] = {HID_KEY_0, 0, 0, 0}, + + ['!'] = {HID_KEY_1, MOD_SHIFT, 0, 0}, + ['@'] = {HID_KEY_2, MOD_SHIFT, 0, 0}, + ['#'] = {HID_KEY_3, MOD_SHIFT, 0, 0}, + ['$'] = {HID_KEY_4, MOD_SHIFT, 0, 0}, + ['%'] = {HID_KEY_5, MOD_SHIFT, 0, 0}, + ['&'] = {HID_KEY_7, MOD_SHIFT, 0, 0}, + ['*'] = {HID_KEY_8, MOD_SHIFT, 0, 0}, + ['('] = {HID_KEY_9, MOD_SHIFT, 0, 0}, + [')'] = {HID_KEY_0, MOD_SHIFT, 0, 0}, + + ['-'] = {HID_KEY_MINUS, 0, 0, 0}, + ['='] = {HID_KEY_EQUAL, 0, 0, 0}, + ['_'] = {HID_KEY_MINUS, MOD_SHIFT, 0, 0}, + ['+'] = {HID_KEY_EQUAL, MOD_SHIFT, 0, 0}, + ['.'] = {HID_KEY_PERIOD, 0, 0, 0}, + [','] = {HID_KEY_COMMA, 0, 0, 0}, + [';'] = {HID_KEY_SLASH, 0, 0, 0}, + [':'] = {HID_KEY_SLASH, MOD_SHIFT, 0, 0}, + ['/'] = {HID_KEY_INTERNATIONAL_1, 0, 0, 0}, + ['?'] = {HID_KEY_INTERNATIONAL_1, MOD_SHIFT, 0, 0}, + + ['['] = {HID_KEY_BRACKET_LEFT, MOD_ALTGR, 0, 0}, + ['{'] = {HID_KEY_BRACKET_LEFT, MOD_ALTGR | MOD_SHIFT, 0, 0}, + [']'] = {HID_KEY_BRACKET_RIGHT, MOD_ALTGR, 0, 0}, + ['}'] = {HID_KEY_BRACKET_RIGHT, MOD_ALTGR | MOD_SHIFT, 0, 0}, + ['\\'] = {HID_KEY_NON_US_BACKSLASH, 0, 0, 0}, + ['|'] = {HID_KEY_NON_US_BACKSLASH, MOD_SHIFT, 0, 0}, + + // Dead-key punctuation: acute accent dead key (BRACKET_LEFT) then space + // yields a literal apostrophe; shift+dead key yields a literal quote. + ['\''] = {HID_KEY_SPACE, 0, HID_KEY_BRACKET_LEFT, 0}, + ['"'] = {HID_KEY_BRACKET_LEFT, MOD_SHIFT, 0, 0}, +}; + +// ABNT2 accented characters (UTF-8 two-byte sequences, lead byte 0xC3). +static const hid_utf8_entry_t s_utf8_abnt2[] = { + // Cedilla (direct key, no dead-key prefix) + {UTF8_2BYTE_LEAD, UTF8_LOWER_C_CEDILLA_B2, {HID_KEY_SEMICOLON, 0, 0, 0}}, + {UTF8_2BYTE_LEAD, UTF8_UPPER_C_CEDILLA_B2, {HID_KEY_SEMICOLON, MOD_SHIFT, 0, 0}}, + + // Acute accent (dead key = BRACKET_LEFT) + {UTF8_2BYTE_LEAD, UTF8_LOWER_A_ACUTE_B2, {HID_KEY_A, 0, HID_KEY_BRACKET_LEFT, 0}}, + {UTF8_2BYTE_LEAD, UTF8_LOWER_E_ACUTE_B2, {HID_KEY_E, 0, HID_KEY_BRACKET_LEFT, 0}}, + {UTF8_2BYTE_LEAD, UTF8_LOWER_I_ACUTE_B2, {HID_KEY_I, 0, HID_KEY_BRACKET_LEFT, 0}}, + {UTF8_2BYTE_LEAD, UTF8_LOWER_O_ACUTE_B2, {HID_KEY_O, 0, HID_KEY_BRACKET_LEFT, 0}}, + {UTF8_2BYTE_LEAD, UTF8_LOWER_U_ACUTE_B2, {HID_KEY_U, 0, HID_KEY_BRACKET_LEFT, 0}}, + + // Circumflex accent (dead key = APOSTROPHE + SHIFT) + {UTF8_2BYTE_LEAD, UTF8_LOWER_A_CIRCUM_B2, {HID_KEY_A, 0, HID_KEY_APOSTROPHE, MOD_SHIFT}}, + {UTF8_2BYTE_LEAD, UTF8_LOWER_E_CIRCUM_B2, {HID_KEY_E, 0, HID_KEY_APOSTROPHE, MOD_SHIFT}}, + {UTF8_2BYTE_LEAD, UTF8_LOWER_O_CIRCUM_B2, {HID_KEY_O, 0, HID_KEY_APOSTROPHE, MOD_SHIFT}}, + + // Tilde (dead key = APOSTROPHE) + {UTF8_2BYTE_LEAD, UTF8_LOWER_A_TILDE_B2, {HID_KEY_A, 0, HID_KEY_APOSTROPHE, 0}}, + {UTF8_2BYTE_LEAD, UTF8_LOWER_O_TILDE_B2, {HID_KEY_O, 0, HID_KEY_APOSTROPHE, 0}}, - // Tilde (dead key = APOSTROPHE) - if (c2 == UTF8_LOWER_A_TILDE_B2) { - hid_hal_press_key(HID_KEY_APOSTROPHE, 0); - hid_hal_press_key(HID_KEY_A, 0); - return true; + // Grave accent (dead key = BRACKET_LEFT + SHIFT) + {UTF8_2BYTE_LEAD, UTF8_LOWER_A_GRAVE_B2, {HID_KEY_A, 0, HID_KEY_BRACKET_LEFT, MOD_SHIFT}}, +}; + +// Registry indexed by ducky_layout_t. To add a layout, add its enum value in +// ducky_parser.h, declare its tables above, and add a row here. +static const hid_layout_t s_layouts[DUCKY_LAYOUT_COUNT] = { + [DUCKY_LAYOUT_US] = {s_ascii_us, NULL, 0}, + [DUCKY_LAYOUT_ABNT2] = {s_ascii_abnt2, + s_utf8_abnt2, + sizeof(s_utf8_abnt2) / sizeof(s_utf8_abnt2[0])}, +}; + +static void type_entry(const hid_key_entry_t *e) { + if (e->dead_keycode != 0) { + hid_hal_press_key(e->dead_keycode, e->dead_modifier); } - if (c2 == UTF8_LOWER_O_TILDE_B2) { - hid_hal_press_key(HID_KEY_APOSTROPHE, 0); - hid_hal_press_key(HID_KEY_O, 0); - return true; + if (e->keycode != 0) { + hid_hal_press_key(e->keycode, e->modifier); } +} - // Grave accent (dead key = BRACKET_LEFT + SHIFT) - if (c2 == UTF8_LOWER_A_GRAVE_B2) { - hid_hal_press_key(HID_KEY_BRACKET_LEFT, KEYBOARD_MODIFIER_LEFTSHIFT); - hid_hal_press_key(HID_KEY_A, 0); - return true; +void hid_layouts_type_string(ducky_layout_t layout, const char *str) { + if (layout < 0 || layout >= DUCKY_LAYOUT_COUNT) { + layout = DUCKY_LAYOUT_US; } + const hid_layout_t *L = &s_layouts[layout]; - return false; + for (size_t i = 0; str[i] != '\0'; ++i) { + uint8_t c = (uint8_t)str[i]; + + if (c < 0x80) { + type_entry(&L->ascii[c]); + continue; + } + + // UTF-8 two-byte lead: resolve via the layout's supplemental table. + uint8_t c2 = (uint8_t)str[i + 1]; + if ((c & 0xE0) == 0xC0 && c2 != '\0' && L->utf8 != NULL) { + for (size_t k = 0; k < L->utf8_count; ++k) { + if (L->utf8[k].b0 == c && L->utf8[k].b1 == c2) { + type_entry(&L->utf8[k].entry); + i++; // consume the trailing byte + break; + } + } + } + // Unmapped / unknown multibyte: produce nothing (matches prior behavior). + } } diff --git a/firmware_p4/components/Applications/bad_usb/include/hid_layouts.h b/firmware_p4/components/Applications/bad_usb/include/hid_layouts.h index 8b32b30b..93264d58 100644 --- a/firmware_p4/components/Applications/bad_usb/include/hid_layouts.h +++ b/firmware_p4/components/Applications/bad_usb/include/hid_layouts.h @@ -22,25 +22,22 @@ extern "C" { #include -/** - * @brief Type a string using the US keyboard layout. - * - * Translates each character to the corresponding HID keycode + modifier - * and sends it through the HAL. - * - * @param str Null-terminated string to type. - */ -void hid_layouts_type_string_us(const char *str); +#include "ducky_parser.h" /** - * @brief Type a string using the Brazilian ABNT2 keyboard layout. + * @brief Type a string using the given keyboard layout. * - * Handles dead-key sequences for accented characters (e.g. a, e, c) - * and remapped punctuation keys specific to the ABNT2 layout. + * Each layout is a data table that maps characters to HID keycodes and + * modifiers (see hid_layouts.c). ASCII bytes are looked up by direct index; + * UTF-8 two-byte sequences (e.g. accented characters) are resolved through a + * per-layout supplemental table. Dead-key sequences are expressed as an + * optional prefix press in the table entry, so they need no special-case + * code here. * - * @param str Null-terminated UTF-8 string to type. + * @param layout Keyboard layout to use. Out-of-range values fall back to US. + * @param str Null-terminated UTF-8 string to type. */ -void hid_layouts_type_string_abnt2(const char *str); +void hid_layouts_type_string(ducky_layout_t layout, const char *str); #ifdef __cplusplus } diff --git a/tools/hid_layouts_equiv_test/.gitignore b/tools/hid_layouts_equiv_test/.gitignore new file mode 100644 index 00000000..c96cfd10 --- /dev/null +++ b/tools/hid_layouts_equiv_test/.gitignore @@ -0,0 +1,4 @@ +equiv_test +equiv_test.exe +*.o +*.obj diff --git a/tools/hid_layouts_equiv_test/README.md b/tools/hid_layouts_equiv_test/README.md new file mode 100644 index 00000000..e9d20aff --- /dev/null +++ b/tools/hid_layouts_equiv_test/README.md @@ -0,0 +1,58 @@ +# HID layouts equivalence test + +A host-side (no-hardware) proof that the table-driven refactor of +`firmware_p4/components/Applications/bad_usb/hid_layouts.c` is +**behavior-preserving**. + +It compiles the **real, current** `hid_layouts.c` together with a **frozen copy +of the pre-refactor implementation** (`hid_layouts_reference_old.c`, captured +from `origin/main`). Both are driven with identical input while +`hid_hal_press_key()` is stubbed to record every `(keycode, modifier)` press. +The test asserts the two press logs are identical for: + +- every single byte `0x01..0xFF`, +- every `0xC3 0xXX` two-byte sequence (the ABNT2 UTF-8 accent range), +- assorted other / malformed multibyte leads, and +- a set of curated real-world strings (incl. all ABNT2 accents and dead keys). + +Exit code `0` = fully equivalent; `1` = at least one mismatch (printed with the +offending input bytes and both press sequences). + +## Run it + +Requires any C99/C11 host compiler (gcc or clang recommended). + +```sh +# macOS / Linux / Git-Bash / WSL +./run_equiv.sh + +# Windows PowerShell +powershell -ExecutionPolicy Bypass -File .\run_equiv.ps1 +``` + +Expected output ends with: + +``` +==== HID layouts equivalence: 1048 cases, 0 mismatch(es) ==== +RESULT: PASS (new table-driven output == frozen old output) +``` + +## Files + +| File | Role | +|------|------| +| `equiv_test.c` | Test driver + `hid_hal_press_key` logging stub + `main`. | +| `hid_layouts_reference_old.c` | Frozen pre-refactor oracle (do not edit). | +| `shims/` | Minimal stand-ins for TinyUSB / ESP-IDF / firmware headers so the real `hid_layouts.c` compiles on a host. | +| `run_equiv.sh` / `run_equiv.ps1` | Build + run. | + +## Notes + +- This directory lives under `tools/` (outside `components/`) on purpose: the + BadUSB component's `CMakeLists.txt` uses `file(GLOB_RECURSE ... "bad_usb/*.c")`, + so a `.c` placed inside the component would be swept into the firmware build. +- The shims define HID keycodes / modifier bitmasks with their real USB HID + values. `HID_KEY_INTERNATIONAL_1` / `HID_KEY_NON_US_BACKSLASH` are left + undefined so both translation units use their identical built-in fallbacks. +- If the legitimate pre-refactor behavior ever changes, regenerate + `hid_layouts_reference_old.c` from history rather than editing it. diff --git a/tools/hid_layouts_equiv_test/equiv_test.c b/tools/hid_layouts_equiv_test/equiv_test.c new file mode 100644 index 00000000..84d92514 --- /dev/null +++ b/tools/hid_layouts_equiv_test/equiv_test.c @@ -0,0 +1,160 @@ +// =========================================================================== +// HID layouts equivalence test (host-side, no hardware). +// +// Proves the table-driven refactor of hid_layouts.c is behavior-preserving: +// for every input it asserts the NEW hid_layouts_type_string() emits exactly +// the same hid_hal_press_key(keycode, modifier) sequence as the frozen, +// pre-refactor functions hid_layouts_type_string_us()/_abnt2(). +// +// Build & run: see run_equiv.sh / run_equiv.ps1 in this directory. +// Exit code 0 = all equivalent; 1 = at least one mismatch. +// =========================================================================== + +#include +#include +#include + +#include "hid_layouts.h" // new + legacy prototypes, ducky_layout_t + +// --- press log: our stub for hid_hal_press_key() --------------------------- +#define LOG_CAP 8192 +static uint8_t g_kc[LOG_CAP]; +static uint8_t g_mod[LOG_CAP]; +static int g_n; + +void hid_hal_press_key(uint8_t keycode, uint8_t modifiers) { + if (g_n < LOG_CAP) { + g_kc[g_n] = keycode; + g_mod[g_n] = modifiers; + } + g_n++; // keep counting past LOG_CAP so length mismatches are still detected +} + +typedef struct { + int n; + uint8_t kc[LOG_CAP]; + uint8_t mod[LOG_CAP]; +} press_log_t; + +static void snapshot(press_log_t *out) { + out->n = g_n; + int n = g_n < LOG_CAP ? g_n : LOG_CAP; + memcpy(out->kc, g_kc, (size_t)n); + memcpy(out->mod, g_mod, (size_t)n); +} + +static int logs_equal(const press_log_t *a, const press_log_t *b) { + if (a->n != b->n) { + return 0; + } + int n = a->n < LOG_CAP ? a->n : LOG_CAP; + for (int i = 0; i < n; ++i) { + if (a->kc[i] != b->kc[i] || a->mod[i] != b->mod[i]) { + return 0; + } + } + return 1; +} + +static void print_hex(const char *s) { + for (const unsigned char *p = (const unsigned char *)s; *p; ++p) { + printf("%02X ", *p); + } +} + +static void print_log(const press_log_t *l) { + int n = l->n < LOG_CAP ? l->n : LOG_CAP; + for (int i = 0; i < n; ++i) { + printf("(%02X,%02X) ", l->kc[i], l->mod[i]); + } + if (l->n > LOG_CAP) { + printf("...[+%d truncated]", l->n - LOG_CAP); + } +} + +// --- comparison driver ------------------------------------------------------ +typedef void (*legacy_fn)(const char *); + +static int g_tests = 0; +static int g_fails = 0; + +static void check(const char *label, ducky_layout_t layout, legacy_fn oracle, + const char *s) { + press_log_t a, b; + + g_n = 0; + oracle(s); + snapshot(&a); + + g_n = 0; + hid_layouts_type_string(layout, s); + snapshot(&b); + + ++g_tests; + if (!logs_equal(&a, &b)) { + ++g_fails; + printf("MISMATCH [%-5s] input bytes: ", label); + print_hex(s); + printf("\n old(%d): ", a.n); + print_log(&a); + printf("\n new(%d): ", b.n); + print_log(&b); + printf("\n"); + } +} + +// Run one input against both layouts, each vs its legacy oracle. +static void check_both(const char *s) { + check("US", DUCKY_LAYOUT_US, hid_layouts_type_string_us, s); + check("ABNT2", DUCKY_LAYOUT_ABNT2, hid_layouts_type_string_abnt2, s); +} + +int main(void) { + char one[2] = {0, 0}; + char two[3] = {0, 0, 0}; + + // 1) Every single byte 0x01..0xFF (covers all ASCII + stray high bytes). + for (int b = 1; b < 256; ++b) { + one[0] = (char)b; + check_both(one); + } + + // 2) Every 0xC3 0xXX pair (the ABNT2 UTF-8 accent lead), 0xXX = 0x01..0xFF. + two[0] = (char)0xC3; + for (int b = 1; b < 256; ++b) { + two[1] = (char)b; + check_both(two); + } + + // 3) Other multibyte leads / malformed sequences (exercise fall-through). + check_both("\xC3"); // lone lead at end of string + check_both("\xC2\xA1"); // 0xC2 lead, not in the ABNT2 table + check_both("\xC4\x80"); // 0xC4 lead + check_both("\xE2\x82\xAC"); // 3-byte sequence + check_both("\xF0\x9F\x98\x80"); // 4-byte sequence + + // 4) Curated strings (integration: chaining, skips, mixed content). + check_both(""); + check_both("Hello, World!"); + check_both("The quick brown fox 0123456789"); + check_both("!@#$%^&*()-_=+[]{}\\|;:'\",.<>/?`~"); + check_both("user@example.com C:\\Users\\test"); + check_both("acao coracao nao"); // ASCII-only baseline + check_both("A" "\xC3\xA7" "\xC3\xA3" "o do cora" "\xC3\xA7" "\xC3\xA3" "o"); + check_both("'single' and \"double\" quotes"); + // Full ABNT2 accent showcase: c C / a e i o u / a e o / a o / a + check_both("\xC3\xA7" "\xC3\x87" " " + "\xC3\xA1" "\xC3\xA9" "\xC3\xAD" "\xC3\xB3" "\xC3\xBA" " " + "\xC3\xA2" "\xC3\xAA" "\xC3\xB4" " " + "\xC3\xA3" "\xC3\xB5" " " + "\xC3\xA0"); + + printf("\n==== HID layouts equivalence: %d cases, %d mismatch(es) ====\n", + g_tests, g_fails); + if (g_fails == 0) { + printf("RESULT: PASS (new table-driven output == frozen old output)\n"); + } else { + printf("RESULT: FAIL (%d mismatch(es) above)\n", g_fails); + } + return g_fails == 0 ? 0 : 1; +} diff --git a/tools/hid_layouts_equiv_test/hid_layouts_reference_old.c b/tools/hid_layouts_equiv_test/hid_layouts_reference_old.c new file mode 100644 index 00000000..5c3f9ce3 --- /dev/null +++ b/tools/hid_layouts_equiv_test/hid_layouts_reference_old.c @@ -0,0 +1,403 @@ +// =========================================================================== +// FROZEN REFERENCE — DO NOT EDIT. +// +// Verbatim copy of firmware_p4/components/Applications/bad_usb/hid_layouts.c as +// it existed BEFORE the table-driven refactor (origin/main @ 7a530b52). It is +// the oracle for the equivalence test: equiv_test.c drives both these legacy +// functions and the new hid_layouts_type_string() with identical input and +// asserts the emitted hid_hal_press_key() sequences match exactly. +// +// If the pre-refactor behavior ever needs to change, this file should be +// regenerated from history, not hand-edited. +// =========================================================================== + +// Copyright (c) 2025 HIGH CODE LLC +// +// TentacleOS is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TentacleOS is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TentacleOS. If not, see . + +#include "hid_layouts.h" + +#include + +#include "esp_log.h" +#include "class/hid/hid_device.h" + +#include "hid_hal.h" + +static const char *TAG = "HID_LAYOUTS"; + +#ifndef HID_KEY_INTERNATIONAL_1 +#define HID_KEY_INTERNATIONAL_1 0x87 +#endif + +#ifndef HID_KEY_NON_US_BACKSLASH +#define HID_KEY_NON_US_BACKSLASH 0x64 +#endif + +// ABNT2 UTF-8 byte pairs for accented characters +#define UTF8_LOWER_C_CEDILLA_B2 0xA7 // c = 0xC3 0xA7 +#define UTF8_UPPER_C_CEDILLA_B2 0x87 // C = 0xC3 0x87 +#define UTF8_LOWER_A_ACUTE_B2 0xA1 // a = 0xC3 0xA1 +#define UTF8_LOWER_E_ACUTE_B2 0xA9 // e = 0xC3 0xA9 +#define UTF8_LOWER_I_ACUTE_B2 0xAD // i = 0xC3 0xAD +#define UTF8_LOWER_O_ACUTE_B2 0xB3 // o = 0xC3 0xB3 +#define UTF8_LOWER_U_ACUTE_B2 0xBA // u = 0xC3 0xBA +#define UTF8_LOWER_A_CIRCUM_B2 0xA2 // a = 0xC3 0xA2 +#define UTF8_LOWER_E_CIRCUM_B2 0xAA // e = 0xC3 0xAA +#define UTF8_LOWER_O_CIRCUM_B2 0xB4 // o = 0xC3 0xB4 +#define UTF8_LOWER_A_TILDE_B2 0xA3 // a = 0xC3 0xA3 +#define UTF8_LOWER_O_TILDE_B2 0xB5 // o = 0xC3 0xB5 +#define UTF8_LOWER_A_GRAVE_B2 0xA0 // a = 0xC3 0xA0 +#define UTF8_2BYTE_LEAD 0xC3 + +static bool try_decode_abnt2_utf8(uint8_t c1, uint8_t c2); + +void hid_layouts_type_string_us(const char *str) { + for (size_t i = 0; str[i] != '\0'; ++i) { + char c = str[i]; + uint8_t keycode = 0; + uint8_t modifier = 0; + + if (c >= 'a' && c <= 'z') { + keycode = HID_KEY_A + (c - 'a'); + } else if (c >= 'A' && c <= 'Z') { + keycode = HID_KEY_A + (c - 'A'); + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + } else if (c >= '1' && c <= '9') { + keycode = HID_KEY_1 + (c - '1'); + } else if (c == '0') { + keycode = HID_KEY_0; + } else { + switch (c) { + case ' ': + keycode = HID_KEY_SPACE; + break; + case '!': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_1; + break; + case '@': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_2; + break; + case '#': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_3; + break; + case '$': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_4; + break; + case '%': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_5; + break; + case '^': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_6; + break; + case '&': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_7; + break; + case '*': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_8; + break; + case '(': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_9; + break; + case ')': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_0; + break; + case '-': + keycode = HID_KEY_MINUS; + break; + case '_': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_MINUS; + break; + case '=': + keycode = HID_KEY_EQUAL; + break; + case '+': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_EQUAL; + break; + case '.': + keycode = HID_KEY_PERIOD; + break; + case ',': + keycode = HID_KEY_COMMA; + break; + case '/': + keycode = HID_KEY_SLASH; + break; + case '?': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_SLASH; + break; + case ':': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_SEMICOLON; + break; + case ';': + keycode = HID_KEY_SEMICOLON; + break; + default: + break; + } + } + + if (keycode != 0) { + hid_hal_press_key(keycode, modifier); + } + } +} + +void hid_layouts_type_string_abnt2(const char *str) { + for (size_t i = 0; str[i] != '\0'; ++i) { + uint8_t c1 = (uint8_t)str[i]; + uint8_t c2 = (uint8_t)str[i + 1]; + + // Single quote -> dead key acute + space + if (c1 == '\'') { + hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); + hid_hal_press_key(HID_KEY_SPACE, 0); + continue; + } + + // Double quote -> dead key acute + shift + if (c1 == '"') { + hid_hal_press_key(HID_KEY_BRACKET_LEFT, KEYBOARD_MODIFIER_LEFTSHIFT); + continue; + } + + // Try UTF-8 two-byte sequences (accented characters) + if ((c1 & 0xE0) == 0xC0 && c2 != '\0') { + if (try_decode_abnt2_utf8(c1, c2)) { + i++; // Skip second byte + continue; + } + } + + uint8_t keycode = 0; + uint8_t modifier = 0; + + if (c1 >= 'a' && c1 <= 'z') { + keycode = HID_KEY_A + (c1 - 'a'); + } else if (c1 >= 'A' && c1 <= 'Z') { + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_A + (c1 - 'A'); + } else if (c1 >= '1' && c1 <= '9') { + keycode = HID_KEY_1 + (c1 - '1'); + } else if (c1 == '0') { + keycode = HID_KEY_0; + } else { + switch (c1) { + case '!': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_1; + break; + case '@': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_2; + break; + case '#': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_3; + break; + case '$': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_4; + break; + case '%': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_5; + break; + case '&': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_7; + break; + case '*': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_8; + break; + case '(': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_9; + break; + case ')': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_0; + break; + case ' ': + keycode = HID_KEY_SPACE; + break; + case '\n': + keycode = HID_KEY_ENTER; + break; + case '\t': + keycode = HID_KEY_TAB; + break; + case '-': + keycode = HID_KEY_MINUS; + break; + case '=': + keycode = HID_KEY_EQUAL; + break; + case '_': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_MINUS; + break; + case '+': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_EQUAL; + break; + case '.': + keycode = HID_KEY_PERIOD; + break; + case ',': + keycode = HID_KEY_COMMA; + break; + case ';': + keycode = HID_KEY_SLASH; + break; + case ':': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_SLASH; + break; + case '/': + keycode = HID_KEY_INTERNATIONAL_1; + break; + case '?': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_INTERNATIONAL_1; + break; + case '[': + modifier = KEYBOARD_MODIFIER_RIGHTALT; + keycode = HID_KEY_BRACKET_LEFT; + break; + case '{': + modifier = KEYBOARD_MODIFIER_RIGHTALT | KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_BRACKET_LEFT; + break; + case ']': + modifier = KEYBOARD_MODIFIER_RIGHTALT; + keycode = HID_KEY_BRACKET_RIGHT; + break; + case '}': + modifier = KEYBOARD_MODIFIER_RIGHTALT | KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_BRACKET_RIGHT; + break; + case '\\': + keycode = HID_KEY_NON_US_BACKSLASH; + break; + case '|': + modifier = KEYBOARD_MODIFIER_LEFTSHIFT; + keycode = HID_KEY_NON_US_BACKSLASH; + break; + default: + break; + } + } + + if (keycode != 0) { + hid_hal_press_key(keycode, modifier); + } + } +} + +static bool try_decode_abnt2_utf8(uint8_t c1, uint8_t c2) { + if (c1 != UTF8_2BYTE_LEAD) { + return false; + } + + // Cedilla + if (c2 == UTF8_LOWER_C_CEDILLA_B2) { + hid_hal_press_key(HID_KEY_SEMICOLON, 0); + return true; + } + if (c2 == UTF8_UPPER_C_CEDILLA_B2) { + hid_hal_press_key(HID_KEY_SEMICOLON, KEYBOARD_MODIFIER_LEFTSHIFT); + return true; + } + + // Acute accent (dead key = BRACKET_LEFT) + if (c2 == UTF8_LOWER_A_ACUTE_B2) { + hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); + hid_hal_press_key(HID_KEY_A, 0); + return true; + } + if (c2 == UTF8_LOWER_E_ACUTE_B2) { + hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); + hid_hal_press_key(HID_KEY_E, 0); + return true; + } + if (c2 == UTF8_LOWER_I_ACUTE_B2) { + hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); + hid_hal_press_key(HID_KEY_I, 0); + return true; + } + if (c2 == UTF8_LOWER_O_ACUTE_B2) { + hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); + hid_hal_press_key(HID_KEY_O, 0); + return true; + } + if (c2 == UTF8_LOWER_U_ACUTE_B2) { + hid_hal_press_key(HID_KEY_BRACKET_LEFT, 0); + hid_hal_press_key(HID_KEY_U, 0); + return true; + } + + // Circumflex accent (dead key = APOSTROPHE + SHIFT) + if (c2 == UTF8_LOWER_A_CIRCUM_B2) { + hid_hal_press_key(HID_KEY_APOSTROPHE, KEYBOARD_MODIFIER_LEFTSHIFT); + hid_hal_press_key(HID_KEY_A, 0); + return true; + } + if (c2 == UTF8_LOWER_E_CIRCUM_B2) { + hid_hal_press_key(HID_KEY_APOSTROPHE, KEYBOARD_MODIFIER_LEFTSHIFT); + hid_hal_press_key(HID_KEY_E, 0); + return true; + } + if (c2 == UTF8_LOWER_O_CIRCUM_B2) { + hid_hal_press_key(HID_KEY_APOSTROPHE, KEYBOARD_MODIFIER_LEFTSHIFT); + hid_hal_press_key(HID_KEY_O, 0); + return true; + } + + // Tilde (dead key = APOSTROPHE) + if (c2 == UTF8_LOWER_A_TILDE_B2) { + hid_hal_press_key(HID_KEY_APOSTROPHE, 0); + hid_hal_press_key(HID_KEY_A, 0); + return true; + } + if (c2 == UTF8_LOWER_O_TILDE_B2) { + hid_hal_press_key(HID_KEY_APOSTROPHE, 0); + hid_hal_press_key(HID_KEY_O, 0); + return true; + } + + // Grave accent (dead key = BRACKET_LEFT + SHIFT) + if (c2 == UTF8_LOWER_A_GRAVE_B2) { + hid_hal_press_key(HID_KEY_BRACKET_LEFT, KEYBOARD_MODIFIER_LEFTSHIFT); + hid_hal_press_key(HID_KEY_A, 0); + return true; + } + + return false; +} diff --git a/tools/hid_layouts_equiv_test/run_equiv.ps1 b/tools/hid_layouts_equiv_test/run_equiv.ps1 new file mode 100644 index 00000000..90702d29 --- /dev/null +++ b/tools/hid_layouts_equiv_test/run_equiv.ps1 @@ -0,0 +1,25 @@ +# Build and run the HID layouts equivalence test (Windows). +# Usage: powershell -ExecutionPolicy Bypass -File .\run_equiv.ps1 +$ErrorActionPreference = 'Stop' +Set-Location $PSScriptRoot + +$cc = $null +foreach ($cand in @('gcc', 'clang', 'cc')) { + if (Get-Command $cand -ErrorAction SilentlyContinue) { $cc = $cand; break } +} +if (-not $cc) { + Write-Error "No C compiler found on PATH (looked for gcc, clang, cc)." + exit 2 +} + +$newSrc = '..\..\firmware_p4\components\Applications\bad_usb\hid_layouts.c' + +Write-Host "Compiling with: $cc" +& $cc -std=c11 -Wall -Wno-unused-variable -I shims ` + equiv_test.c hid_layouts_reference_old.c $newSrc ` + -o equiv_test.exe +if ($LASTEXITCODE -ne 0) { Write-Error "Compilation failed."; exit 1 } + +Write-Host "Running:" +& .\equiv_test.exe +exit $LASTEXITCODE diff --git a/tools/hid_layouts_equiv_test/run_equiv.sh b/tools/hid_layouts_equiv_test/run_equiv.sh new file mode 100644 index 00000000..ebaac655 --- /dev/null +++ b/tools/hid_layouts_equiv_test/run_equiv.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Build and run the HID layouts equivalence test. +# Usage: ./run_equiv.sh (uses cc, or $CC if set) +set -euo pipefail +cd "$(dirname "$0")" + +CC="${CC:-cc}" +NEW_SRC="../../firmware_p4/components/Applications/bad_usb/hid_layouts.c" + +echo "Compiling with: $CC" +"$CC" -std=c11 -Wall -Wno-unused-variable -I shims \ + equiv_test.c hid_layouts_reference_old.c "$NEW_SRC" \ + -o equiv_test + +echo "Running:" +./equiv_test diff --git a/tools/hid_layouts_equiv_test/shims/class/hid/hid_device.h b/tools/hid_layouts_equiv_test/shims/class/hid/hid_device.h new file mode 100644 index 00000000..3b124ef6 --- /dev/null +++ b/tools/hid_layouts_equiv_test/shims/class/hid/hid_device.h @@ -0,0 +1,39 @@ +// Host-test shim for TinyUSB's . +// +// Provides only the HID usage IDs and modifier bitmasks referenced by +// hid_layouts.c. Values mirror the real USB HID definitions so the equivalence +// test exercises the true keycodes. HID_KEY_INTERNATIONAL_1 and +// HID_KEY_NON_US_BACKSLASH are intentionally NOT defined here, so both the old +// and new translation units fall back to their own identical `#ifndef` +// defaults (0x87 / 0x64) — keeping the comparison consistent. +#ifndef SHIM_HID_DEVICE_H +#define SHIM_HID_DEVICE_H + +enum { + HID_KEY_A = 0x04, HID_KEY_B, HID_KEY_C, HID_KEY_D, HID_KEY_E, HID_KEY_F, + HID_KEY_G, HID_KEY_H, HID_KEY_I, HID_KEY_J, HID_KEY_K, HID_KEY_L, HID_KEY_M, + HID_KEY_N, HID_KEY_O, HID_KEY_P, HID_KEY_Q, HID_KEY_R, HID_KEY_S, HID_KEY_T, + HID_KEY_U, HID_KEY_V, HID_KEY_W, HID_KEY_X, HID_KEY_Y, HID_KEY_Z, // 0x04..0x1D + HID_KEY_1, HID_KEY_2, HID_KEY_3, HID_KEY_4, HID_KEY_5, + HID_KEY_6, HID_KEY_7, HID_KEY_8, HID_KEY_9, HID_KEY_0, // 0x1E..0x27 + HID_KEY_ENTER, // 0x28 + HID_KEY_ESCAPE, HID_KEY_BACKSPACE, HID_KEY_TAB, HID_KEY_SPACE, // 0x29..0x2C + HID_KEY_MINUS, HID_KEY_EQUAL, // 0x2D..0x2E + HID_KEY_BRACKET_LEFT, HID_KEY_BRACKET_RIGHT, // 0x2F..0x30 + HID_KEY_BACKSLASH, HID_KEY_EUROPE_1, // 0x31..0x32 + HID_KEY_SEMICOLON, HID_KEY_APOSTROPHE, HID_KEY_GRAVE, // 0x33..0x35 + HID_KEY_COMMA, HID_KEY_PERIOD, HID_KEY_SLASH // 0x36..0x38 +}; + +enum { + KEYBOARD_MODIFIER_LEFTCTRL = 0x01, + KEYBOARD_MODIFIER_LEFTSHIFT = 0x02, + KEYBOARD_MODIFIER_LEFTALT = 0x04, + KEYBOARD_MODIFIER_LEFTGUI = 0x08, + KEYBOARD_MODIFIER_RIGHTCTRL = 0x10, + KEYBOARD_MODIFIER_RIGHTSHIFT = 0x20, + KEYBOARD_MODIFIER_RIGHTALT = 0x40, + KEYBOARD_MODIFIER_RIGHTGUI = 0x80 +}; + +#endif // SHIM_HID_DEVICE_H diff --git a/tools/hid_layouts_equiv_test/shims/ducky_parser.h b/tools/hid_layouts_equiv_test/shims/ducky_parser.h new file mode 100644 index 00000000..6a8ab59f --- /dev/null +++ b/tools/hid_layouts_equiv_test/shims/ducky_parser.h @@ -0,0 +1,12 @@ +// Host-test shim for ducky_parser.h. Only the layout enum is needed by +// hid_layouts.c; it must match the real definition in the firmware header. +#ifndef SHIM_DUCKY_PARSER_H +#define SHIM_DUCKY_PARSER_H + +typedef enum { + DUCKY_LAYOUT_US = 0, + DUCKY_LAYOUT_ABNT2, + DUCKY_LAYOUT_COUNT +} ducky_layout_t; + +#endif // SHIM_DUCKY_PARSER_H diff --git a/tools/hid_layouts_equiv_test/shims/esp_log.h b/tools/hid_layouts_equiv_test/shims/esp_log.h new file mode 100644 index 00000000..20ce0c2a --- /dev/null +++ b/tools/hid_layouts_equiv_test/shims/esp_log.h @@ -0,0 +1,12 @@ +// Host-test shim for esp_log.h. The frozen reference file includes this header; +// it makes no logging calls, so the macros are no-ops. +#ifndef SHIM_ESP_LOG_H +#define SHIM_ESP_LOG_H + +#define ESP_LOGE(tag, ...) ((void)0) +#define ESP_LOGW(tag, ...) ((void)0) +#define ESP_LOGI(tag, ...) ((void)0) +#define ESP_LOGD(tag, ...) ((void)0) +#define ESP_LOGV(tag, ...) ((void)0) + +#endif // SHIM_ESP_LOG_H diff --git a/tools/hid_layouts_equiv_test/shims/hid_hal.h b/tools/hid_layouts_equiv_test/shims/hid_hal.h new file mode 100644 index 00000000..8ed4c3b5 --- /dev/null +++ b/tools/hid_layouts_equiv_test/shims/hid_hal.h @@ -0,0 +1,11 @@ +// Host-test shim for hid_hal.h. The equivalence harness (equiv_test.c) provides +// the definition of hid_hal_press_key(), which records each press into a log +// instead of touching USB. +#ifndef SHIM_HID_HAL_H +#define SHIM_HID_HAL_H + +#include + +void hid_hal_press_key(uint8_t keycode, uint8_t modifiers); + +#endif // SHIM_HID_HAL_H diff --git a/tools/hid_layouts_equiv_test/shims/hid_layouts.h b/tools/hid_layouts_equiv_test/shims/hid_layouts.h new file mode 100644 index 00000000..40189650 --- /dev/null +++ b/tools/hid_layouts_equiv_test/shims/hid_layouts.h @@ -0,0 +1,21 @@ +// Host-test shim for hid_layouts.h. +// +// Declares both the NEW generic entry point (compiled from the real +// firmware_p4/.../bad_usb/hid_layouts.c) and the LEGACY per-layout functions +// (compiled from the frozen hid_layouts_reference_old.c), so both translation +// units compile cleanly against one header. +#ifndef SHIM_HID_LAYOUTS_H +#define SHIM_HID_LAYOUTS_H + +#include + +#include "ducky_parser.h" + +// New, table-driven API (the code under test). +void hid_layouts_type_string(ducky_layout_t layout, const char *str); + +// Legacy API (frozen pre-refactor reference, the oracle). +void hid_layouts_type_string_us(const char *str); +void hid_layouts_type_string_abnt2(const char *str); + +#endif // SHIM_HID_LAYOUTS_H