mirror of https://github.com/nirenjan/libx52.git
feat: Add layout file parsing to x52d
This change is an initial step to adding support for profiles in x52d. This will allow the user to create a keyboard layout in an easy to read/write text based format, and have it compiled into a flat layout that's easy for the daemon to parse and load into memory. This layout can then be used to map a user's action key to the actual input usage needed. This is necessary, because keyboards don't actually send the character that is typed, but just the position on the keyboard. For example, on a French AZERTY keyboard, the A key would actually send the usage for `Keyboard_q_and_Q`. The OS would translate that into the letter 'a' (or 'A' if Shift key is held) and pass that to the active window. This commit adds the full logic necessary for the layout loading, validation and compiling, as well as tests for the compiler and loader.master
parent
f52e328a8b
commit
75f0125f54
|
|
@ -29,6 +29,10 @@ Module.symvers
|
|||
# Vim swap files
|
||||
.*.swp
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Autotools objects
|
||||
.deps
|
||||
.dirstamp
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@
|
|||
/* Define to the location of the local state directory */
|
||||
#mesondefine LOCALSTATEDIR
|
||||
|
||||
/* Define to the installation data directory (e.g. $prefix/share) */
|
||||
#mesondefine DATADIR
|
||||
|
||||
/* Define to the location of the log directory */
|
||||
#define LOGDIR LOCALSTATEDIR "/log"
|
||||
|
||||
|
|
|
|||
|
|
@ -277,6 +277,7 @@ static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
|
|||
[X52D_MOD_COMMAND] = "command",
|
||||
[X52D_MOD_CLIENT] = "client",
|
||||
[X52D_MOD_NOTIFY] = "notify",
|
||||
[X52D_MOD_KEYBOARD_LAYOUT] = "keyboard_layout",
|
||||
};
|
||||
|
||||
// This corresponds to the levels in pinelog
|
||||
|
|
|
|||
|
|
@ -105,4 +105,8 @@ CFG(Profiles, ClutchEnabled, clutch_enabled, bool, false)
|
|||
// be held down to remain in clutch mode.
|
||||
CFG(Profiles, ClutchLatched, clutch_latched, bool, false)
|
||||
|
||||
// KeyboardLayout is the default keyboard layout used when mapping
|
||||
// profile keys to keyboard events.
|
||||
CFG(Profiles, KeyboardLayout, profile_keyboard_layout, string, us)
|
||||
|
||||
#undef CFG
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ struct x52d_config {
|
|||
bool clutch_latched;
|
||||
|
||||
char profiles_dir[NAME_MAX];
|
||||
|
||||
char profile_keyboard_layout[NAME_MAX];
|
||||
};
|
||||
|
||||
/* Callback functions for configuration */
|
||||
|
|
@ -84,6 +86,7 @@ void x52d_cfg_set_Mouse_Deadzone(int param);
|
|||
void x52d_cfg_set_Profiles_Directory(char* param);
|
||||
void x52d_cfg_set_Profiles_ClutchEnabled(bool param);
|
||||
void x52d_cfg_set_Profiles_ClutchLatched(bool param);
|
||||
void x52d_cfg_set_Profiles_KeyboardLayout(char *param);
|
||||
|
||||
int x52d_config_process_kv(void *user, const char *section, const char *key, const char *value);
|
||||
const char *x52d_config_get_param(struct x52d_config *cfg, const char *section, const char *key);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ enum {
|
|||
X52D_MOD_COMMAND,
|
||||
X52D_MOD_CLIENT,
|
||||
X52D_MOD_NOTIFY,
|
||||
X52D_MOD_KEYBOARD_LAYOUT,
|
||||
|
||||
X52D_MOD_MAX
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - CRC-32 (zlib / Python zlib.crc32 compatible)
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Table matches zlib's crc_table (reflect, poly 0xEDB88320). */
|
||||
static const uint32_t x52_crc32_table[256] = {
|
||||
0x00000000u, 0x77073096u, 0xee0e612cu, 0x990951bau, 0x076dc419u, 0x706af48fu, 0xe963a535u,
|
||||
0x9e6495a3u, 0x0edb8832u, 0x79dcb8a4u, 0xe0d5e91eu, 0x97d2d988u, 0x09b64c2bu, 0x7eb17cbdu,
|
||||
0xe7b82d07u, 0x90bf1d91u, 0x1db71064u, 0x6ab020f2u, 0xf3b97148u, 0x84be41deu, 0x1adad47du,
|
||||
0x6ddde4ebu, 0xf4d4b551u, 0x83d385c7u, 0x136c9856u, 0x646ba8c0u, 0xfd62f97au, 0x8a65c9ecu,
|
||||
0x14015c4fu, 0x63066cd9u, 0xfa0f3d63u, 0x8d080df5u, 0x3b6e20c8u, 0x4c69105eu, 0xd56041e4u,
|
||||
0xa2677172u, 0x3c03e4d1u, 0x4b04d447u, 0xd20d85fdu, 0xa50ab56bu, 0x35b5a8fau, 0x42b2986cu,
|
||||
0xdbbbc9d6u, 0xacbcf940u, 0x32d86ce3u, 0x45df5c75u, 0xdcd60dcfu, 0xabd13d59u, 0x26d930acu,
|
||||
0x51de003au, 0xc8d75180u, 0xbfd06116u, 0x21b4f4b5u, 0x56b3c423u, 0xcfba9599u, 0xb8bda50fu,
|
||||
0x2802b89eu, 0x5f058808u, 0xc60cd9b2u, 0xb10be924u, 0x2f6f7c87u, 0x58684c11u, 0xc1611dabu,
|
||||
0xb6662d3du, 0x76dc4190u, 0x01db7106u, 0x98d220bcu, 0xefd5102au, 0x71b18589u, 0x06b6b51fu,
|
||||
0x9fbfe4a5u, 0xe8b8d433u, 0x7807c9a2u, 0x0f00f934u, 0x9609a88eu, 0xe10e9818u, 0x7f6a0dbbu,
|
||||
0x086d3d2du, 0x91646c97u, 0xe6635c01u, 0x6b6b51f4u, 0x1c6c6162u, 0x856530d8u, 0xf262004eu,
|
||||
0x6c0695edu, 0x1b01a57bu, 0x8208f4c1u, 0xf50fc457u, 0x65b0d9c6u, 0x12b7e950u, 0x8bbeb8eau,
|
||||
0xfcb9887cu, 0x62dd1ddfu, 0x15da2d49u, 0x8cd37cf3u, 0xfbd44c65u, 0x4db26158u, 0x3ab551ceu,
|
||||
0xa3bc0074u, 0xd4bb30e2u, 0x4adfa541u, 0x3dd895d7u, 0xa4d1c46du, 0xd3d6f4fbu, 0x4369e96au,
|
||||
0x346ed9fcu, 0xad678846u, 0xda60b8d0u, 0x44042d73u, 0x33031de5u, 0xaa0a4c5fu, 0xdd0d7cc9u,
|
||||
0x5005713cu, 0x270241aau, 0xbe0b1010u, 0xc90c2086u, 0x5768b525u, 0x206f85b3u, 0xb966d409u,
|
||||
0xce61e49fu, 0x5edef90eu, 0x29d9c998u, 0xb0d09822u, 0xc7d7a8b4u, 0x59b33d17u, 0x2eb40d81u,
|
||||
0xb7bd5c3bu, 0xc0ba6cadu, 0xedb88320u, 0x9abfb3b6u, 0x03b6e20cu, 0x74b1d29au, 0xead54739u,
|
||||
0x9dd277afu, 0x04db2615u, 0x73dc1683u, 0xe3630b12u, 0x94643b84u, 0x0d6d6a3eu, 0x7a6a5aa8u,
|
||||
0xe40ecf0bu, 0x9309ff9du, 0x0a00ae27u, 0x7d079eb1u, 0xf00f9344u, 0x8708a3d2u, 0x1e01f268u,
|
||||
0x6906c2feu, 0xf762575du, 0x806567cbu, 0x196c3671u, 0x6e6b06e7u, 0xfed41b76u, 0x89d32be0u,
|
||||
0x10da7a5au, 0x67dd4accu, 0xf9b9df6fu, 0x8ebeeff9u, 0x17b7be43u, 0x60b08ed5u, 0xd6d6a3e8u,
|
||||
0xa1d1937eu, 0x38d8c2c4u, 0x4fdff252u, 0xd1bb67f1u, 0xa6bc5767u, 0x3fb506ddu, 0x48b2364bu,
|
||||
0xd80d2bdau, 0xaf0a1b4cu, 0x36034af6u, 0x41047a60u, 0xdf60efc3u, 0xa867df55u, 0x316e8eefu,
|
||||
0x4669be79u, 0xcb61b38cu, 0xbc66831au, 0x256fd2a0u, 0x5268e236u, 0xcc0c7795u, 0xbb0b4703u,
|
||||
0x220216b9u, 0x5505262fu, 0xc5ba3bbeu, 0xb2bd0b28u, 0x2bb45a92u, 0x5cb36a04u, 0xc2d7ffa7u,
|
||||
0xb5d0cf31u, 0x2cd99e8bu, 0x5bdeae1du, 0x9b64c2b0u, 0xec63f226u, 0x756aa39cu, 0x026d930au,
|
||||
0x9c0906a9u, 0xeb0e363fu, 0x72076785u, 0x05005713u, 0x95bf4a82u, 0xe2b87a14u, 0x7bb12baeu,
|
||||
0x0cb61b38u, 0x92d28e9bu, 0xe5d5be0du, 0x7cdcefb7u, 0x0bdbdf21u, 0x86d3d2d4u, 0xf1d4e242u,
|
||||
0x68ddb3f8u, 0x1fda836eu, 0x81be16cdu, 0xf6b9265bu, 0x6fb077e1u, 0x18b74777u, 0x88085ae6u,
|
||||
0xff0f6a70u, 0x66063bcau, 0x11010b5cu, 0x8f659effu, 0xf862ae69u, 0x616bffd3u, 0x166ccf45u,
|
||||
0xa00ae278u, 0xd70dd2eeu, 0x4e048354u, 0x3903b3c2u, 0xa7672661u, 0xd06016f7u, 0x4969474du,
|
||||
0x3e6e77dbu, 0xaed16a4au, 0xd9d65adcu, 0x40df0b66u, 0x37d83bf0u, 0xa9bcae53u, 0xdebb9ec5u,
|
||||
0x47b2cf7fu, 0x30b5ffe9u, 0xbdbdf21cu, 0xcabac28au, 0x53b39330u, 0x24b4a3a6u, 0xbad03605u,
|
||||
0xcdd70693u, 0x54de5729u, 0x23d967bfu, 0xb3667a2eu, 0xc4614ab8u, 0x5d681b02u, 0x2a6f2b94u,
|
||||
0xb40bbe37u, 0xc30c8ea1u, 0x5a05df1bu, 0x2d02ef8du,
|
||||
};
|
||||
|
||||
uint32_t x52_crc32_init(void)
|
||||
{
|
||||
return X52_CRC32_INIT;
|
||||
}
|
||||
|
||||
uint32_t x52_crc32_update(uint32_t crc, const void *data, size_t len)
|
||||
{
|
||||
const unsigned char *p = data;
|
||||
|
||||
if (len == 0) {
|
||||
return crc;
|
||||
}
|
||||
|
||||
crc = ~crc;
|
||||
|
||||
while (len != 0) {
|
||||
crc = x52_crc32_table[(crc ^ *p++) & 0xffu] ^ (crc >> 8);
|
||||
len--;
|
||||
}
|
||||
|
||||
return ~crc;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - CRC-32 (zlib / Python zlib.crc32 compatible)
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file crc32.h
|
||||
* @brief IEEE/ZIP CRC-32 matching @c zlib.crc32 / Python @c zlib.crc32 (polynomial 0xEDB88320).
|
||||
*
|
||||
* Incremental use: @code
|
||||
* uint32_t crc = x52_crc32_init();
|
||||
* crc = x52_crc32_update(crc, chunk0, len0);
|
||||
* crc = x52_crc32_update(crc, chunk1, len1);
|
||||
* // crc is final value (unsigned 32-bit, same encoding as Python after & 0xFFFFFFFF)
|
||||
* @endcode
|
||||
*/
|
||||
#ifndef X52D_CRC32_H
|
||||
#define X52D_CRC32_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Initial accumulator value (same as first argument to zlib @c crc32 for a new stream). */
|
||||
#define X52_CRC32_INIT 0u
|
||||
|
||||
/** Start a new CRC computation. */
|
||||
uint32_t x52_crc32_init(void);
|
||||
|
||||
/**
|
||||
* Feed zero or more bytes into a running CRC.
|
||||
*
|
||||
* @param crc Value from @ref x52_crc32_init or a prior @ref x52_crc32_update
|
||||
* @param data Input bytes; must be non-NULL if @p len is non-zero
|
||||
* @param len Number of bytes
|
||||
* @return Updated CRC-32 (same as Python <tt>zlib.crc32(data, crc) & 0xFFFFFFFF</tt>
|
||||
* when @p data is the new chunk only)
|
||||
*/
|
||||
uint32_t x52_crc32_update(uint32_t crc, const void *data, size_t len);
|
||||
|
||||
/** Alias for @ref x52_crc32_update (zlib-style name). */
|
||||
static inline uint32_t x52_crc32(uint32_t crc, const void *data, size_t len)
|
||||
{
|
||||
return x52_crc32_update(crc, data, len);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* X52D_CRC32_H */
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - CRC-32 unit tests
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
|
||||
/* Golden values: Python zlib.crc32(data) & 0xFFFFFFFF */
|
||||
|
||||
static void test_init_zero(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal((int)x52_crc32_init(), 0);
|
||||
assert_int_equal((int)X52_CRC32_INIT, 0);
|
||||
}
|
||||
|
||||
static void test_empty(void **state)
|
||||
{
|
||||
(void)state;
|
||||
uint32_t crc = x52_crc32_init();
|
||||
crc = x52_crc32_update(crc, NULL, 0);
|
||||
assert_true(crc == 0u);
|
||||
crc = x52_crc32_update(crc, "", 0);
|
||||
assert_true(crc == 0u);
|
||||
}
|
||||
|
||||
static void test_len0_preserves(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const char *s = "abc";
|
||||
uint32_t crc = x52_crc32_update(0, s, 3);
|
||||
uint32_t again = x52_crc32_update(crc, s, 0);
|
||||
assert_true(again == crc);
|
||||
}
|
||||
|
||||
static void test_string_123456789(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t crc = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
assert_true(crc == 0xcbf43926u);
|
||||
}
|
||||
|
||||
static void test_bytes_0_to_255(void **state)
|
||||
{
|
||||
(void)state;
|
||||
unsigned char buf[256];
|
||||
for (unsigned i = 0; i < 256; i++) {
|
||||
buf[i] = (unsigned char)i;
|
||||
}
|
||||
uint32_t crc = x52_crc32_update(0, buf, sizeof(buf));
|
||||
assert_true(crc == 0x29058c73u);
|
||||
}
|
||||
|
||||
static void test_incremental_matches_one_shot(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t a = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
|
||||
uint32_t b = x52_crc32_init();
|
||||
b = x52_crc32_update(b, s, 3);
|
||||
b = x52_crc32_update(b, s + 3, 3);
|
||||
b = x52_crc32_update(b, s + 6, 3);
|
||||
|
||||
assert_true(b == a);
|
||||
}
|
||||
|
||||
static void test_chaining_second_segment(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char h[] = "hello";
|
||||
static const char w[] = "world";
|
||||
static const char hw[] = "helloworld";
|
||||
|
||||
uint32_t c = x52_crc32_update(0, h, sizeof(h) - 1u);
|
||||
c = x52_crc32_update(c, w, sizeof(w) - 1u);
|
||||
|
||||
uint32_t whole = x52_crc32_update(0, hw, sizeof(hw) - 1u);
|
||||
assert_true(c == whole);
|
||||
assert_true(c == 0xf9eb20adu);
|
||||
}
|
||||
|
||||
static void test_one_byte_at_a_time(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t expect = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
uint32_t crc = x52_crc32_init();
|
||||
for (size_t i = 0; i < sizeof(s) - 1u; i++) {
|
||||
crc = x52_crc32_update(crc, s + i, 1);
|
||||
}
|
||||
assert_true(crc == expect);
|
||||
}
|
||||
|
||||
static void test_x52_crc32_alias(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t u = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
uint32_t v = x52_crc32(0, s, sizeof(s) - 1u);
|
||||
assert_true(u == v);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_init_zero),
|
||||
cmocka_unit_test(test_empty),
|
||||
cmocka_unit_test(test_len0_preserves),
|
||||
cmocka_unit_test(test_string_123456789),
|
||||
cmocka_unit_test(test_bytes_0_to_255),
|
||||
cmocka_unit_test(test_incremental_matches_one_shot),
|
||||
cmocka_unit_test(test_chaining_second_segment),
|
||||
cmocka_unit_test(test_one_byte_at_a_time),
|
||||
cmocka_unit_test(test_x52_crc32_alias),
|
||||
};
|
||||
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - keyboard layout from config
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_KEYBOARD_LAYOUT
|
||||
#include "pinelog.h"
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/keyboard_layout.h>
|
||||
#include <daemon/layout_format.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static x52_layout *active_layout;
|
||||
|
||||
/**
|
||||
* Normal install: @c $DATADIR/x52d/<basename>.x52l
|
||||
*
|
||||
* If @c X52D_LAYOUT_DIR is set (non-empty), load @c $X52D_LAYOUT_DIR/<basename>.x52l instead so
|
||||
* uninstalled tests can point at the Meson build directory.
|
||||
*/
|
||||
static int load_layout_for_basename(const char *basename, x52_layout **out)
|
||||
{
|
||||
const char *layout_dir = getenv("X52D_LAYOUT_DIR");
|
||||
|
||||
if (layout_dir != NULL && layout_dir[0] != '\0') {
|
||||
char path[PATH_MAX];
|
||||
int n = snprintf(path, sizeof path, "%s/%s.x52l", layout_dir, basename);
|
||||
if (n < 0) {
|
||||
return EIO;
|
||||
}
|
||||
if ((size_t)n >= sizeof path) {
|
||||
return ENAMETOOLONG;
|
||||
}
|
||||
return x52_layout_load_path(path, out);
|
||||
}
|
||||
|
||||
return x52_layout_load_datadir(DATADIR, basename, out);
|
||||
}
|
||||
|
||||
const x52_layout *x52d_keyboard_layout_get(void)
|
||||
{
|
||||
return active_layout;
|
||||
}
|
||||
|
||||
void x52d_keyboard_layout_fini(void)
|
||||
{
|
||||
x52_layout_free(active_layout);
|
||||
active_layout = NULL;
|
||||
}
|
||||
|
||||
void x52d_keyboard_layout_reload(char *profile_keyboard_layout_value)
|
||||
{
|
||||
char basename[256];
|
||||
bool rejected = false;
|
||||
|
||||
x52_layout_normalize_keyboard_basename(profile_keyboard_layout_value, basename, sizeof basename, &rejected);
|
||||
if (rejected) {
|
||||
PINELOG_WARN(_("Invalid Profiles.KeyboardLayout value; using default layout basename 'us'"));
|
||||
}
|
||||
|
||||
x52_layout *new_layout = NULL;
|
||||
int err = load_layout_for_basename(basename, &new_layout);
|
||||
|
||||
if (err != 0 && strcmp(basename, "us") != 0) {
|
||||
PINELOG_WARN(
|
||||
_("Keyboard layout '%s' could not be loaded (%s); loading default 'us'"),
|
||||
basename, strerror(err > 0 ? err : EIO));
|
||||
err = load_layout_for_basename("us", &new_layout);
|
||||
}
|
||||
|
||||
if (err != 0) {
|
||||
PINELOG_FATAL(_("Could not load keyboard layout from %s/x52d (%s)"), DATADIR,
|
||||
strerror(err > 0 ? err : EIO));
|
||||
}
|
||||
|
||||
x52_layout *old_layout = active_layout;
|
||||
active_layout = new_layout;
|
||||
x52_layout_free(old_layout);
|
||||
|
||||
const char *desc = x52_layout_description(active_layout);
|
||||
PINELOG_INFO(_("Keyboard layout ready: %s (%s)"), x52_layout_name(active_layout),
|
||||
desc[0] != '\0' ? desc : _("no description"));
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Profiles_KeyboardLayout(char *param)
|
||||
{
|
||||
x52d_keyboard_layout_reload(param);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - keyboard layout from config
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_KEYBOARD_LAYOUT_H
|
||||
#define X52D_KEYBOARD_LAYOUT_H
|
||||
|
||||
#include <daemon/layout_format.h>
|
||||
|
||||
/**
|
||||
* @brief Load or reload layout from @c profile_keyboard_layout (@c Profiles.KeyboardLayout).
|
||||
*
|
||||
* Resolves @c $datadir/x52d/<basename>.x52l with basename hardening; falls back to @c us once if the
|
||||
* requested file is missing. Exits the process if no layout can be loaded.
|
||||
*/
|
||||
void x52d_keyboard_layout_reload(char *profile_keyboard_layout_value);
|
||||
|
||||
/** Active layout after @ref x52d_keyboard_layout_reload, or @c NULL before the first apply. */
|
||||
const x52_layout *x52d_keyboard_layout_get(void);
|
||||
|
||||
void x52d_keyboard_layout_fini(void);
|
||||
|
||||
#endif /* X52D_KEYBOARD_LAYOUT_H */
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - x52layout v1 binary format
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file layout_format.h
|
||||
* @brief On-disk keyboard layout (@c .x52l) v1: constants, documentation, load and lookup API.
|
||||
*
|
||||
* The file is a dense index: entry @c entries[c] maps Unicode scalar @c c when
|
||||
* @c 0 <= c < codepoint_limit. For @c c >= codepoint_limit there is no mapping.
|
||||
*
|
||||
* **Header (128 bytes, all multi-byte integers big-endian / network order):**
|
||||
* - **0..3:** magic @c 'X' @c '5' @c '2' @c 'L'
|
||||
* - **4..5:** @c version (must be @ref X52_LAYOUT_FORMAT_VERSION)
|
||||
* - **6..7:** @c flags (v1: only @ref X52_LAYOUT_FLAG_NAME_TRUNCATED and/or
|
||||
* @ref X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED; other bits are reserved and must be zero)
|
||||
* - **8..11:** @c codepoint_limit — exclusive end of range; number of two-byte rows in @c entries
|
||||
* - **12..15:** @c checksum — CRC-32 (ZIP/IEEE, same as Python @c zlib.crc32) over the full
|
||||
* file with bytes 12..15 taken as zero when computing the digest
|
||||
* - **16..47:** @c layout_name (required: at least one character before @c NUL; remainder zero)
|
||||
* - **48..111:** @c description (optional, NUL-terminated, remainder zero)
|
||||
* - **112..127:** reserved (ignored on read in v1)
|
||||
*
|
||||
* **128+:** @c entries[] — pairs @c (modifiers, usage_key) for HID page 0x07; @c (0, 0) is empty.
|
||||
*
|
||||
* **File size:** exactly @c 128 + 2 * @c codepoint_limit bytes.
|
||||
*/
|
||||
#ifndef X52D_LAYOUT_FORMAT_H
|
||||
#define X52D_LAYOUT_FORMAT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Four-byte magic at offset 0 (not NUL-terminated in the file). */
|
||||
#define X52_LAYOUT_MAGIC_0 'X'
|
||||
#define X52_LAYOUT_MAGIC_1 '5'
|
||||
#define X52_LAYOUT_MAGIC_2 '2'
|
||||
#define X52_LAYOUT_MAGIC_3 'L'
|
||||
|
||||
/** Total header size and byte offset of the @c entries table. */
|
||||
#define X52_LAYOUT_HEADER_BYTES 128u
|
||||
|
||||
#define X52_LAYOUT_FORMAT_VERSION 1u
|
||||
|
||||
/** Inclusive maximum for @c codepoint_limit - 1 (Unicode scalar space + sentinel ceiling). */
|
||||
#define X52_LAYOUT_CODEPOINT_LIMIT_MAX 0x110000u
|
||||
|
||||
/** @c layout_name field size in the header (offset 16). */
|
||||
#define X52_LAYOUT_NAME_FIELD_BYTES 32u
|
||||
|
||||
/** @c description field size in the header (offset 48). */
|
||||
#define X52_LAYOUT_DESCRIPTION_FIELD_BYTES 64u
|
||||
|
||||
/**
|
||||
* v1 header @c flags (big-endian on disk). @ref x52_layout_flags returns the host-endian value.
|
||||
*/
|
||||
#define X52_LAYOUT_FLAG_NAME_TRUNCATED 1u
|
||||
#define X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED 2u
|
||||
|
||||
/** Bitmask of flags defined for v1; other bits must be zero. */
|
||||
#define X52_LAYOUT_FLAGS_KNOWN (X52_LAYOUT_FLAG_NAME_TRUNCATED | X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED)
|
||||
|
||||
/** Loaded layout snapshot (opaque): full file copy, validated at load time. */
|
||||
typedef struct x52_layout x52_layout;
|
||||
|
||||
/**
|
||||
* @brief Read a layout file into a malloc'd snapshot and validate it (no @c mmap).
|
||||
*
|
||||
* @param path Path to the @c .x52l file; must not be @c NULL (otherwise @c EINVAL)
|
||||
* @param out On success, receives a new @ref x52_layout; must not be @c NULL; caller must @ref x52_layout_free
|
||||
*
|
||||
* @returns 0 on success, or a positive @c errno value (@c EINVAL, @c ENOMEM, @c EIO, @c ENOENT, …)
|
||||
*/
|
||||
int x52_layout_load_path(const char *path, x52_layout **out);
|
||||
|
||||
/**
|
||||
* @brief Turn @c Profiles.KeyboardLayout INI value into a safe layout basename.
|
||||
*
|
||||
* Empty or @c NULL yields @c "us" with @p rejected_out @c false. Values containing @c '/' ,
|
||||
* @c '\\' , @c ".." , disallowed characters, or oversize strings are rejected: @p out becomes
|
||||
* @c "us" and @p rejected_out is @c true (caller should log once). Requires @p out_sz @c >= 3.
|
||||
*
|
||||
* Allowed characters: ASCII alphanumeric, @c '_' , @c '-'.
|
||||
*/
|
||||
void x52_layout_normalize_keyboard_basename(const char *cfg_value, char *out, size_t out_sz, bool *rejected_out);
|
||||
|
||||
/**
|
||||
* @brief Build @c <datadir>/x52d/<basename>.x52l into @p path.
|
||||
*
|
||||
* @returns 0 on success, or @c ENAMETOOLONG if the path does not fit
|
||||
*/
|
||||
int x52_layout_join_file_path(char *path, size_t path_sz, const char *datadir, const char *basename);
|
||||
|
||||
/**
|
||||
* @brief Load @c join(datadir, "x52d", basename + ".x52l") after the same validation as @ref x52_layout_load_path.
|
||||
*
|
||||
* @returns 0 on success, or a positive @c errno (e.g. @c ENOENT, @c EINVAL, @c ENAMETOOLONG)
|
||||
*/
|
||||
int x52_layout_load_datadir(const char *datadir, const char *basename, x52_layout **out);
|
||||
|
||||
/**
|
||||
* @brief Copy @p data into an owned buffer and validate it.
|
||||
*
|
||||
* Same validation rules as @ref x52_layout_load_path (magic, version, flags, size, CRC-32, entries,
|
||||
* non-empty @c layout_name, etc.).
|
||||
*
|
||||
* @param data Layout file bytes; may be @c NULL only if @p len is zero (otherwise @c EINVAL)
|
||||
* @param len Number of bytes in @p data
|
||||
* @param out On success, receives a new @ref x52_layout; must not be @c NULL; caller must @ref x52_layout_free
|
||||
*
|
||||
* @returns 0 on success, or a positive @c errno value (@c EINVAL, @c ENOMEM, …)
|
||||
*/
|
||||
int x52_layout_load_memory(const void *data, size_t len, x52_layout **out);
|
||||
|
||||
/**
|
||||
* @brief Release a layout loaded by @ref x52_layout_load_path or @ref x52_layout_load_memory.
|
||||
*
|
||||
* @param layout Layout to free; @c NULL is a no-op
|
||||
*/
|
||||
void x52_layout_free(x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Exclusive end of the Unicode scalar range covered by @c entries (same as on-disk @c codepoint_limit).
|
||||
*
|
||||
* Lookups for @c code_point >= this value are not in the table.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns The limit value, or @c 0 if @p layout is @c NULL
|
||||
*/
|
||||
uint32_t x52_layout_codepoint_limit(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Host-endian copy of the on-disk @c flags field at header offset 6.
|
||||
*
|
||||
* Only bits in @ref X52_LAYOUT_FLAGS_KNOWN may be set in valid files; the loader rejects unknown bits.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns Flag word, or @c 0 if @p layout is @c NULL
|
||||
*/
|
||||
uint16_t x52_layout_flags(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Layout name from the header @c layout_name field.
|
||||
*
|
||||
* If @ref X52_LAYOUT_FLAG_NAME_TRUNCATED is set, the returned string is the on-disk name plus
|
||||
* @c "<truncated>". The pointer remains valid until @ref x52_layout_free; do not modify the string.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns Read-only NUL-terminated UTF-8 string; empty string if @p layout is @c NULL
|
||||
*/
|
||||
const char *x52_layout_name(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Optional description from the header @c description field.
|
||||
*
|
||||
* If @ref X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED is set, the returned string is the on-disk text plus
|
||||
* @c "<truncated>". The pointer remains valid until @ref x52_layout_free; do not modify the string.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns Read-only NUL-terminated UTF-8 string, or empty string if @p layout is @c NULL or the field is empty
|
||||
*/
|
||||
const char *x52_layout_description(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief O(1) lookup of the HID chord for Unicode scalar @p code_point.
|
||||
*
|
||||
* Returns @c false when @p code_point is out of range, when the slot is empty @c (modifiers, usage_key) = (0, 0),
|
||||
* or when any argument is invalid. On @c false, @p modifiers_out and @p usage_key_out are left unchanged.
|
||||
*
|
||||
* @param layout Loaded layout; if @c NULL, returns @c false
|
||||
* @param code_point Unicode scalar value
|
||||
* @param[out] modifiers_out HID keyboard modifier byte (@ref vkm_key_modifiers bits); if @c NULL, returns @c false
|
||||
* @param[out] usage_key_out HID usage (page 0x07); if @c NULL, returns @c false
|
||||
*
|
||||
* @returns @c true if a non-empty mapping exists and was written to @p modifiers_out and @p usage_key_out
|
||||
*/
|
||||
bool x52_layout_lookup(const x52_layout *layout, uint32_t code_point, uint8_t *modifiers_out,
|
||||
uint8_t *usage_key_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* X52D_LAYOUT_FORMAT_H */
|
||||
|
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - x52layout v1 loader
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
#include <daemon/layout_format.h>
|
||||
#include <daemon/layout_usage_allowlist.h>
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
struct x52_layout {
|
||||
uint8_t *data;
|
||||
size_t size;
|
||||
uint32_t codepoint_limit;
|
||||
uint16_t flags;
|
||||
char *name;
|
||||
char *description;
|
||||
};
|
||||
|
||||
static size_t meta_field_len(const uint8_t *field, size_t nbytes)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < nbytes; i++) {
|
||||
if (field[i] == '\0') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
static char *copy_meta_string(const uint8_t *field, size_t nbytes, bool add_trunc_suffix)
|
||||
{
|
||||
static const char suf[] = "<truncated>";
|
||||
const size_t suf_len = sizeof(suf) - 1u;
|
||||
size_t base_len = meta_field_len(field, nbytes);
|
||||
size_t total = base_len + (add_trunc_suffix ? suf_len : 0);
|
||||
char *s = (char *)malloc(total + 1u);
|
||||
|
||||
if (s == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (base_len != 0) {
|
||||
memcpy(s, field, base_len);
|
||||
}
|
||||
if (add_trunc_suffix) {
|
||||
memcpy(s + base_len, suf, suf_len);
|
||||
}
|
||||
s[total] = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
static uint16_t read_be16(const uint8_t *p)
|
||||
{
|
||||
return (uint16_t)((uint16_t)p[0] << 8 | (uint16_t)p[1]);
|
||||
}
|
||||
|
||||
static uint32_t read_be32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
static uint32_t x52_layout_file_crc(const uint8_t *buf, size_t len)
|
||||
{
|
||||
uint32_t crc = x52_crc32_init();
|
||||
crc = x52_crc32_update(crc, buf, 12u);
|
||||
static const uint8_t zero_chk[4] = {0, 0, 0, 0};
|
||||
crc = x52_crc32_update(crc, zero_chk, 4u);
|
||||
if (len > 16u) {
|
||||
crc = x52_crc32_update(crc, buf + 16u, len - 16u);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
static int validate_entries(const uint8_t *buf, uint32_t codepoint_limit)
|
||||
{
|
||||
const uint8_t *base = buf + X52_LAYOUT_HEADER_BYTES;
|
||||
|
||||
for (uint32_t i = 0; i < codepoint_limit; i++) {
|
||||
uint8_t usage = base[2u * (size_t)i + 1u];
|
||||
if (usage == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!x52_layout_usage_key_allowed(usage)) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int layout_validate_and_adopt(uint8_t *buf, size_t len, x52_layout **out)
|
||||
{
|
||||
if (out == NULL) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
*out = NULL;
|
||||
|
||||
if (len < X52_LAYOUT_HEADER_BYTES) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
if (memcmp(buf, "X52L", 4) != 0) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint16_t version = read_be16(buf + 4);
|
||||
uint16_t flags = read_be16(buf + 6);
|
||||
uint32_t codepoint_limit = read_be32(buf + 8);
|
||||
uint32_t stored_crc = read_be32(buf + 12);
|
||||
|
||||
if (version != X52_LAYOUT_FORMAT_VERSION) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
if ((flags & (uint16_t)~X52_LAYOUT_FLAGS_KNOWN) != 0) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
if (codepoint_limit > X52_LAYOUT_CODEPOINT_LIMIT_MAX) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
size_t body = (size_t)codepoint_limit * 2u;
|
||||
size_t expected = X52_LAYOUT_HEADER_BYTES + body;
|
||||
if (expected < X52_LAYOUT_HEADER_BYTES || len != expected) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
if (meta_field_len(buf + 16, X52_LAYOUT_NAME_FIELD_BYTES) == 0) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint32_t calc = x52_layout_file_crc(buf, len);
|
||||
if (calc != stored_crc) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
int en = validate_entries(buf, codepoint_limit);
|
||||
if (en != 0) {
|
||||
free(buf);
|
||||
return en;
|
||||
}
|
||||
|
||||
x52_layout *L = (x52_layout *)malloc(sizeof *L);
|
||||
if (L == NULL) {
|
||||
free(buf);
|
||||
return ENOMEM;
|
||||
}
|
||||
L->data = buf;
|
||||
L->size = len;
|
||||
L->codepoint_limit = codepoint_limit;
|
||||
L->flags = flags;
|
||||
L->name = copy_meta_string(buf + 16, X52_LAYOUT_NAME_FIELD_BYTES,
|
||||
(flags & X52_LAYOUT_FLAG_NAME_TRUNCATED) != 0);
|
||||
if (L->name == NULL) {
|
||||
free(L);
|
||||
free(buf);
|
||||
return ENOMEM;
|
||||
}
|
||||
L->description = copy_meta_string(buf + 48, X52_LAYOUT_DESCRIPTION_FIELD_BYTES,
|
||||
(flags & X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED) != 0);
|
||||
if (L->description == NULL) {
|
||||
free(L->name);
|
||||
free(L);
|
||||
free(buf);
|
||||
return ENOMEM;
|
||||
}
|
||||
*out = L;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void x52_layout_normalize_keyboard_basename(const char *cfg_value, char *out, size_t out_sz, bool *rejected_out)
|
||||
{
|
||||
*rejected_out = false;
|
||||
if (out_sz < 3) {
|
||||
if (out_sz > 0) {
|
||||
out[0] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cfg_value == NULL || cfg_value[0] == '\0') {
|
||||
out[0] = 'u';
|
||||
out[1] = 's';
|
||||
out[2] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (strchr(cfg_value, '/') != NULL || strchr(cfg_value, '\\') != NULL ||
|
||||
strstr(cfg_value, "..") != NULL) {
|
||||
goto failed_normalization;
|
||||
}
|
||||
|
||||
size_t len = strlen(cfg_value);
|
||||
if (len >= out_sz) {
|
||||
goto failed_normalization;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
unsigned char c = (unsigned char)cfg_value[i];
|
||||
if (!isalnum((int)c) && c != '_' && c != '-') {
|
||||
goto failed_normalization;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(out, cfg_value, len + 1u);
|
||||
return;
|
||||
|
||||
failed_normalization:
|
||||
*rejected_out = true;
|
||||
out[0] = 'u';
|
||||
out[1] = 's';
|
||||
out[2] = '\0';
|
||||
}
|
||||
|
||||
int x52_layout_join_file_path(char *path, size_t path_sz, const char *datadir, const char *basename)
|
||||
{
|
||||
if (path == NULL || datadir == NULL || basename == NULL || path_sz == 0) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
int n = snprintf(path, path_sz, "%s/x52d/%s.x52l", datadir, basename);
|
||||
if (n < 0) {
|
||||
return EIO;
|
||||
}
|
||||
if ((size_t)n >= path_sz) {
|
||||
return ENAMETOOLONG;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int x52_layout_load_datadir(const char *datadir, const char *basename, x52_layout **out)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
|
||||
int rc = x52_layout_join_file_path(path, sizeof path, datadir, basename);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
return x52_layout_load_path(path, out);
|
||||
}
|
||||
|
||||
int x52_layout_load_path(const char *path, x52_layout **out)
|
||||
{
|
||||
if (path == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (fp == NULL) {
|
||||
return errno != 0 ? errno : EIO;
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
int e = errno != 0 ? errno : EIO;
|
||||
fclose(fp);
|
||||
return e;
|
||||
}
|
||||
|
||||
long pos = ftell(fp);
|
||||
if (pos < 0) {
|
||||
int e = errno != 0 ? errno : EIO;
|
||||
fclose(fp);
|
||||
return e;
|
||||
}
|
||||
if (fseek(fp, 0, SEEK_SET) != 0) {
|
||||
int e = errno != 0 ? errno : EIO;
|
||||
fclose(fp);
|
||||
return e;
|
||||
}
|
||||
|
||||
size_t len = (size_t)pos;
|
||||
if ((long)len != pos || pos < (long)X52_LAYOUT_HEADER_BYTES) {
|
||||
fclose(fp);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint8_t *buf = (uint8_t *)malloc(len);
|
||||
if (buf == NULL) {
|
||||
fclose(fp);
|
||||
return ENOMEM;
|
||||
}
|
||||
|
||||
size_t n = fread(buf, 1, len, fp);
|
||||
fclose(fp);
|
||||
if (n != len) {
|
||||
free(buf);
|
||||
return EIO;
|
||||
}
|
||||
|
||||
return layout_validate_and_adopt(buf, len, out);
|
||||
}
|
||||
|
||||
int x52_layout_load_memory(const void *data, size_t len, x52_layout **out)
|
||||
{
|
||||
if (data == NULL && len != 0) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint8_t *buf = (uint8_t *)malloc(len);
|
||||
if (buf == NULL) {
|
||||
return ENOMEM;
|
||||
}
|
||||
if (len != 0) {
|
||||
memcpy(buf, data, len);
|
||||
}
|
||||
return layout_validate_and_adopt(buf, len, out);
|
||||
}
|
||||
|
||||
void x52_layout_free(x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL) {
|
||||
return;
|
||||
}
|
||||
free(layout->name);
|
||||
free(layout->description);
|
||||
free(layout->data);
|
||||
free(layout);
|
||||
}
|
||||
|
||||
uint32_t x52_layout_codepoint_limit(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return layout->codepoint_limit;
|
||||
}
|
||||
|
||||
uint16_t x52_layout_flags(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return layout->flags;
|
||||
}
|
||||
|
||||
const char *x52_layout_name(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL || layout->name == NULL) {
|
||||
return "";
|
||||
}
|
||||
return layout->name;
|
||||
}
|
||||
|
||||
const char *x52_layout_description(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL || layout->description == NULL) {
|
||||
return "";
|
||||
}
|
||||
return layout->description;
|
||||
}
|
||||
|
||||
bool x52_layout_lookup(const x52_layout *layout, uint32_t code_point, uint8_t *modifiers_out,
|
||||
uint8_t *usage_key_out)
|
||||
{
|
||||
if (layout == NULL || modifiers_out == NULL || usage_key_out == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (code_point >= layout->codepoint_limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t off = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)code_point;
|
||||
uint8_t mod = layout->data[off];
|
||||
uint8_t usage = layout->data[off + 1u];
|
||||
if (usage == 0) {
|
||||
return false;
|
||||
}
|
||||
*modifiers_out = mod;
|
||||
*usage_key_out = usage;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - x52layout loader tests
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
#include <daemon/layout_format.h>
|
||||
#include <vkm/vkm.h>
|
||||
|
||||
static void write_be16(uint8_t *p, uint16_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v >> 8);
|
||||
p[1] = (uint8_t)v;
|
||||
}
|
||||
|
||||
static void write_be32(uint8_t *p, uint32_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v >> 24);
|
||||
p[1] = (uint8_t)(v >> 16);
|
||||
p[2] = (uint8_t)(v >> 8);
|
||||
p[3] = (uint8_t)v;
|
||||
}
|
||||
|
||||
static uint32_t read_be32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
/** Patch checksum (offsets 12..15) after building the rest; @p len is full file size. */
|
||||
static void finalize_crc(uint8_t *buf, size_t len)
|
||||
{
|
||||
uint32_t crc = x52_crc32_init();
|
||||
crc = x52_crc32_update(crc, buf, 12u);
|
||||
static const uint8_t z[4] = {0, 0, 0, 0};
|
||||
crc = x52_crc32_update(crc, z, 4u);
|
||||
if (len > 16u) {
|
||||
crc = x52_crc32_update(crc, buf + 16u, len - 16u);
|
||||
}
|
||||
write_be32(buf + 12, crc);
|
||||
}
|
||||
|
||||
/** Python @c zlib.crc32 over @c minimal v1 layout (@c limit=1, name @c "x", empty entry). */
|
||||
#define X52_LAYOUT_TEST_MINIMAL_ZLIB_CRC32 0xc951bfaau
|
||||
|
||||
static void test_load_minimal_lookup(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 98u; /* enough for 'a' at 97 */
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)limit;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "minimal", 8);
|
||||
/* chord for 'a': VKM_KEY_A */
|
||||
size_t off = X52_LAYOUT_HEADER_BYTES + 2u * 97u;
|
||||
buf[off] = 0x02; /* LSHIFT */
|
||||
buf[off + 1] = (uint8_t)VKM_KEY_A;
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_int_equal((int)x52_layout_codepoint_limit(L), (int)limit);
|
||||
|
||||
uint8_t m = 0xff;
|
||||
uint8_t u = 0xff;
|
||||
assert_true(x52_layout_lookup(L, 97u, &m, &u));
|
||||
assert_int_equal((int)m, 0x02);
|
||||
assert_int_equal((int)u, (int)VKM_KEY_A);
|
||||
|
||||
m = 0;
|
||||
u = 0;
|
||||
assert_false(x52_layout_lookup(L, 0u, &m, &u));
|
||||
assert_false(x52_layout_lookup(L, limit, &m, &u));
|
||||
|
||||
assert_int_equal((int)x52_layout_flags(L), 0);
|
||||
assert_string_equal(x52_layout_name(L), "minimal");
|
||||
assert_string_equal(x52_layout_description(L), "");
|
||||
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_bad_checksum(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
write_be32(buf + 12, 0xdeadbeefu);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_layout_crc_matches_python_zlib_minimal(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
assert_int_equal((int)read_be32(buf + 12), (int)X52_LAYOUT_TEST_MINIMAL_ZLIB_CRC32);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_string_equal(x52_layout_name(L), "x");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_tampered_checksum_byte(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
buf[12] ^= 0x01u;
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_codepoint_limit_not_big_endian(void **state)
|
||||
{
|
||||
(void)state;
|
||||
/* Little-endian uint32_t 1 in the codepoint_limit field: read_be32 → 0x01000000. */
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)limit;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
buf[8] = 0x01u;
|
||||
buf[9] = 0x00u;
|
||||
buf[10] = 0x00u;
|
||||
buf[11] = 0x00u;
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_version_word_not_big_endian_one(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
/* Native little-endian 0x0001 would appear as 01 00 — not BE version 1 (00 01). */
|
||||
buf[4] = 0x01u;
|
||||
buf[5] = 0x00u;
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_size_mismatch(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 4u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)limit;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len - 1u, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_disallowed_usage(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
buf[X52_LAYOUT_HEADER_BYTES + 1u] = 0x3A; /* VKM_KEY_F1 — not in allowlist */
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_plain(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "us", 3);
|
||||
memcpy(buf + 48, "US QWERTY", 10);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_string_equal(x52_layout_name(L), "us");
|
||||
assert_string_equal(x52_layout_description(L), "US QWERTY");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_truncated_suffix(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6,
|
||||
(uint16_t)(X52_LAYOUT_FLAG_NAME_TRUNCATED | X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED));
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "longish", 8);
|
||||
memcpy(buf + 48, "desc", 5);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_int_equal((int)x52_layout_flags(L),
|
||||
(int)(X52_LAYOUT_FLAG_NAME_TRUNCATED | X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED));
|
||||
assert_string_equal(x52_layout_name(L), "longish<truncated>");
|
||||
assert_string_equal(x52_layout_description(L), "desc<truncated>");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_name_truncated_flag_only(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, X52_LAYOUT_FLAG_NAME_TRUNCATED);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "nm", 3);
|
||||
memcpy(buf + 48, "plain", 6);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_int_equal((int)x52_layout_flags(L), (int)X52_LAYOUT_FLAG_NAME_TRUNCATED);
|
||||
assert_string_equal(x52_layout_name(L), "nm<truncated>");
|
||||
assert_string_equal(x52_layout_description(L), "plain");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_unknown_flags(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0x8000);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_empty_name(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_basename_normalize_and_join(void **state)
|
||||
{
|
||||
(void)state;
|
||||
char out[256];
|
||||
bool rej;
|
||||
|
||||
x52_layout_normalize_keyboard_basename(NULL, out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("", out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("de", out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "de");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("ab_cd-9", out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "ab_cd-9");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("../x", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("a/b", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("bad name", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("a\\b", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("x..y", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
memset(out, 0, sizeof out);
|
||||
x52_layout_normalize_keyboard_basename("almost..", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
char path[PATH_MAX];
|
||||
assert_int_equal(x52_layout_join_file_path(path, sizeof path, "/usr/share", "us"), 0);
|
||||
assert_string_equal(path, "/usr/share/x52d/us.x52l");
|
||||
assert_int_equal(x52_layout_join_file_path(path, 20, "/usr/share", "us"), ENAMETOOLONG);
|
||||
}
|
||||
|
||||
static void test_reject_version(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, 99);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "v", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_load_minimal_lookup),
|
||||
cmocka_unit_test(test_metadata_plain),
|
||||
cmocka_unit_test(test_metadata_truncated_suffix),
|
||||
cmocka_unit_test(test_metadata_name_truncated_flag_only),
|
||||
cmocka_unit_test(test_reject_unknown_flags),
|
||||
cmocka_unit_test(test_reject_empty_name),
|
||||
cmocka_unit_test(test_reject_bad_checksum),
|
||||
cmocka_unit_test(test_layout_crc_matches_python_zlib_minimal),
|
||||
cmocka_unit_test(test_reject_tampered_checksum_byte),
|
||||
cmocka_unit_test(test_reject_codepoint_limit_not_big_endian),
|
||||
cmocka_unit_test(test_reject_version_word_not_big_endian_one),
|
||||
cmocka_unit_test(test_reject_size_mismatch),
|
||||
cmocka_unit_test(test_reject_disallowed_usage),
|
||||
cmocka_unit_test(test_reject_version),
|
||||
cmocka_unit_test(test_basename_normalize_and_join),
|
||||
};
|
||||
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Keyboard layout HID usage allowlist
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include <daemon/layout_usage_allowlist.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <vkm/vkm.h>
|
||||
|
||||
/* layout binary and compiler must stay aligned with vkm_key numeric values. */
|
||||
_Static_assert((unsigned)VKM_KEY_A == 0x04u, "vkm_key main block start");
|
||||
_Static_assert((unsigned)VKM_KEY_CAPS_LOCK == 0x39u, "vkm_key main block end");
|
||||
_Static_assert((unsigned)VKM_KEY_INTL_BACKSLASH == 0x64u, "vkm_key ISO backslash");
|
||||
|
||||
bool x52_layout_usage_key_allowed(uint8_t usage)
|
||||
{
|
||||
if (usage == 0) {
|
||||
return false;
|
||||
}
|
||||
if (usage >= 0x04 && usage <= 0x39) {
|
||||
return true;
|
||||
}
|
||||
if (usage == 0x64) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Keyboard layout HID usage allowlist
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file layout_usage_allowlist.h
|
||||
* @brief HID keyboard/page (0x07) usages permitted as layout chord \c usage_key bytes.
|
||||
*
|
||||
* The set is the USB HID "main block" (usages @c 0x04-@c 0x39, i.e. @c VKM_KEY_A
|
||||
* through @c VKM_KEY_CAPS_LOCK) plus @c VKM_KEY_INTL_BACKSLASH (@c 0x64). It excludes
|
||||
* @c VKM_KEY_NONE, modifiers, function row, navigation cluster, keypad, and all other
|
||||
* @ref vkm_key values; same rule as @c tools/x52compile_layout.py.
|
||||
*/
|
||||
#ifndef X52D_LAYOUT_USAGE_ALLOWLIST_H
|
||||
#define X52D_LAYOUT_USAGE_ALLOWLIST_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Return whether @p usage may appear as the non-modifier byte in a layout entry.
|
||||
*
|
||||
* @param usage HID usage ID (page 0x07), same encoding as @ref vkm_key.
|
||||
*
|
||||
* @returns true if @p usage is in the main-block allowlist; false for @c VKM_KEY_NONE
|
||||
* and for any disallowed usage.
|
||||
*/
|
||||
bool x52_layout_usage_key_allowed(uint8_t usage);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* X52D_LAYOUT_USAGE_ALLOWLIST_H */
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - layout HID usage allowlist tests
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <daemon/layout_usage_allowlist.h>
|
||||
#include <vkm/vkm.h>
|
||||
|
||||
static void test_allows_main_block(void **state)
|
||||
{
|
||||
(void)state;
|
||||
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_A));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_Z));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_1));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_0));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_SPACE));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_CAPS_LOCK));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_INTL_BACKSLASH));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_NONUS_HASH));
|
||||
}
|
||||
|
||||
static void test_rejects_disallowed(void **state)
|
||||
{
|
||||
(void)state;
|
||||
|
||||
assert_false(x52_layout_usage_key_allowed(0));
|
||||
assert_false(x52_layout_usage_key_allowed(VKM_KEY_F1));
|
||||
assert_false(x52_layout_usage_key_allowed(VKM_KEY_LEFT_CTRL));
|
||||
assert_false(x52_layout_usage_key_allowed(VKM_KEY_KEYPAD_1));
|
||||
assert_false(x52_layout_usage_key_allowed(0x03));
|
||||
assert_false(x52_layout_usage_key_allowed(0x3A));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_allows_main_block),
|
||||
cmocka_unit_test(test_rejects_disallowed),
|
||||
};
|
||||
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
#include <daemon/command.h>
|
||||
#include <daemon/notify.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
#include <daemon/keyboard_layout.h>
|
||||
#include <libx52/x52dcomm.h>
|
||||
#include "pinelog.h"
|
||||
|
||||
|
|
@ -364,6 +365,7 @@ int main(int argc, char **argv)
|
|||
PINELOG_INFO(_("Received termination signal %s"), strsignal(flag_quit));
|
||||
|
||||
cleanup:
|
||||
x52d_keyboard_layout_fini();
|
||||
// Stop device threads
|
||||
x52d_clock_exit();
|
||||
x52d_dev_exit();
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ x52d_sources = [
|
|||
'command.c',
|
||||
'io.c',
|
||||
'mouse_handler.c',
|
||||
'layout_usage_allowlist.c',
|
||||
'layout_load.c',
|
||||
'keyboard_layout.c',
|
||||
'crc32.c',
|
||||
]
|
||||
|
||||
dep_threads = dependency('threads')
|
||||
|
|
@ -60,6 +64,19 @@ exe_x52ctl = executable('x52ctl', 'daemon_control.c',
|
|||
install_data('x52d.conf',
|
||||
install_dir: join_paths(get_option('sysconfdir'), 'x52d'))
|
||||
|
||||
us_x52l = custom_target(
|
||||
'us-x52l',
|
||||
input: files('../data/layouts/us.layout'),
|
||||
output: 'us.x52l',
|
||||
command: [
|
||||
python,
|
||||
join_paths(meson.project_source_root(), 'tools', 'x52compile_layout.py'),
|
||||
'@INPUT@',
|
||||
'@OUTPUT@',
|
||||
],
|
||||
install: true,
|
||||
install_dir: join_paths(get_option('datadir'), 'x52d'))
|
||||
|
||||
test('daemon-communication', files('test_daemon_comm.py')[0],
|
||||
depends: [exe_x52d, exe_x52ctl], protocol: 'tap')
|
||||
|
||||
|
|
@ -70,6 +87,43 @@ x52d_mouse_test = executable('x52d-mouse-test', x52d_mouse_test_sources,
|
|||
|
||||
test('x52d-mouse-test', x52d_mouse_test, protocol: 'tap')
|
||||
|
||||
layout_usage_allowlist_test = executable('layout-usage-allowlist-test',
|
||||
'layout_usage_allowlist_test.c',
|
||||
'layout_usage_allowlist.c',
|
||||
build_by_default: false,
|
||||
include_directories: includes,
|
||||
dependencies: [dep_cmocka, dep_config_h])
|
||||
|
||||
test('layout-usage-allowlist', layout_usage_allowlist_test, protocol: 'tap')
|
||||
|
||||
crc32_test = executable('crc32-test', 'crc32_test.c', 'crc32.c',
|
||||
build_by_default: false,
|
||||
include_directories: includes,
|
||||
dependencies: [dep_cmocka, dep_config_h])
|
||||
|
||||
test('crc32', crc32_test, protocol: 'tap')
|
||||
|
||||
layout_load_test = executable('layout-load-test',
|
||||
'layout_load_test.c',
|
||||
'layout_load.c',
|
||||
'layout_usage_allowlist.c',
|
||||
'crc32.c',
|
||||
build_by_default: false,
|
||||
include_directories: includes,
|
||||
dependencies: [dep_cmocka, dep_config_h])
|
||||
|
||||
test('layout-load', layout_load_test, protocol: 'tap')
|
||||
|
||||
pymod_daemon = import('python')
|
||||
python_layout_test = pymod_daemon.find_installation('python3')
|
||||
test('layout-usage-allowlist-sync', python_layout_test,
|
||||
args: [join_paths(meson.project_source_root(), 'tools', 'test_layout_allowlist_sync.py')],
|
||||
protocol: 'tap')
|
||||
|
||||
test('layout-compile-py', python_layout_test,
|
||||
args: [join_paths(meson.project_source_root(), 'tools', 'test_x52compile_layout.py')],
|
||||
protocol: 'tap')
|
||||
|
||||
# Install service file
|
||||
if dep_systemd.found()
|
||||
systemd_system_unit_dir = get_option('systemd-unit-dir')
|
||||
|
|
|
|||
|
|
@ -134,7 +134,11 @@ class Test:
|
|||
with open(daemon_cmdline[4], 'w', encoding='utf-8'):
|
||||
pass
|
||||
|
||||
self.daemon = subprocess.Popen(daemon_cmdline) # pylint: disable=consider-using-with
|
||||
env = os.environ.copy()
|
||||
# Uninstalled build: us.x52l lives next to the x52d binary (see daemon/meson.build).
|
||||
env['X52D_LAYOUT_DIR'] = os.path.dirname(self.program)
|
||||
|
||||
self.daemon = subprocess.Popen(daemon_cmdline, env=env) # pylint: disable=consider-using-with
|
||||
|
||||
print("# Sleeping 2 seconds for daemon to start")
|
||||
time.sleep(2)
|
||||
|
|
|
|||
|
|
@ -139,6 +139,10 @@ ClutchEnabled=no
|
|||
# be held down to remain in clutch mode.
|
||||
ClutchLatched=no
|
||||
|
||||
# KeyboardLayout is a basename only (alphanumeric, underscore, hyphen), not a path.
|
||||
# Resolves to $datadir/x52d/<basename>.x52l; default us uses the installed us.x52l pack.
|
||||
KeyboardLayout=us
|
||||
|
||||
##################
|
||||
#X52 Input Servic#
|
||||
#Version 0.3.3 #
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
# US QWERTY — main block (letters, digits, punctuation, space, controls)
|
||||
name: us
|
||||
description: US QWERTY main block
|
||||
|
||||
U+0008 BACKSPACE
|
||||
U+0009 TAB
|
||||
U+000A ENTER
|
||||
U+000D ENTER
|
||||
U+001B ESCAPE
|
||||
U+0020 SPACE
|
||||
|
||||
a a
|
||||
b b
|
||||
c c
|
||||
d d
|
||||
e e
|
||||
f f
|
||||
g g
|
||||
h h
|
||||
i i
|
||||
j j
|
||||
k k
|
||||
l l
|
||||
m m
|
||||
n n
|
||||
o o
|
||||
p p
|
||||
q q
|
||||
r r
|
||||
s s
|
||||
t t
|
||||
u u
|
||||
v v
|
||||
w w
|
||||
x x
|
||||
y y
|
||||
z z
|
||||
|
||||
A SHIFT+a
|
||||
B SHIFT+b
|
||||
C SHIFT+c
|
||||
D SHIFT+d
|
||||
E SHIFT+e
|
||||
F SHIFT+f
|
||||
G SHIFT+g
|
||||
H SHIFT+h
|
||||
I SHIFT+i
|
||||
J SHIFT+j
|
||||
K SHIFT+k
|
||||
L SHIFT+l
|
||||
M SHIFT+m
|
||||
N SHIFT+n
|
||||
O SHIFT+o
|
||||
P SHIFT+p
|
||||
Q SHIFT+q
|
||||
R SHIFT+r
|
||||
S SHIFT+s
|
||||
T SHIFT+t
|
||||
U SHIFT+u
|
||||
V SHIFT+v
|
||||
W SHIFT+w
|
||||
X SHIFT+x
|
||||
Y SHIFT+y
|
||||
Z SHIFT+z
|
||||
|
||||
1 1
|
||||
2 2
|
||||
3 3
|
||||
4 4
|
||||
5 5
|
||||
6 6
|
||||
7 7
|
||||
8 8
|
||||
9 9
|
||||
0 0
|
||||
! SHIFT+1
|
||||
@ SHIFT+2
|
||||
# SHIFT+3
|
||||
$ SHIFT+4
|
||||
% SHIFT+5
|
||||
^ SHIFT+6
|
||||
& SHIFT+7
|
||||
* SHIFT+8
|
||||
( SHIFT+9
|
||||
) SHIFT+0
|
||||
_ SHIFT+MINUS
|
||||
+ SHIFT+EQUAL
|
||||
|
||||
[ LEFT_BRACKET
|
||||
{ SHIFT+LEFT_BRACKET
|
||||
] RIGHT_BRACKET
|
||||
} SHIFT+RIGHT_BRACKET
|
||||
\ BACKSLASH
|
||||
| SHIFT+BACKSLASH
|
||||
|
||||
; SEMICOLON
|
||||
: SHIFT+SEMICOLON
|
||||
' APOSTROPHE
|
||||
" SHIFT+APOSTROPHE
|
||||
` GRAVE
|
||||
~ SHIFT+GRAVE
|
||||
|
||||
, COMMA
|
||||
< SHIFT+COMMA
|
||||
. PERIOD
|
||||
> SHIFT+PERIOD
|
||||
/ SLASH
|
||||
? SHIFT+SLASH
|
||||
|
||||
- MINUS
|
||||
= EQUAL
|
||||
|
|
@ -50,6 +50,7 @@ cdata.set_quoted('PACKAGE_NAME', meson.project_name())
|
|||
cdata.set_quoted('LOCALEDIR', get_option('prefix') / get_option('localedir'))
|
||||
cdata.set_quoted('SYSCONFDIR', get_option('prefix') / get_option('sysconfdir'))
|
||||
cdata.set_quoted('LOCALSTATEDIR', get_option('prefix') / get_option('localstatedir'))
|
||||
cdata.set_quoted('DATADIR', get_option('prefix') / get_option('datadir'))
|
||||
cdata.set_quoted('PACKAGE_VERSION', meson.project_version())
|
||||
cdata.set_quoted('VERSION', meson.project_version())
|
||||
cdata.set10('ENABLE_NLS', not get_option('nls').disabled())
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ daemon/config_parser.c
|
|||
daemon/daemon_control.c
|
||||
daemon/device.c
|
||||
daemon/io.c
|
||||
daemon/keyboard_layout.c
|
||||
daemon/main.c
|
||||
daemon/mouse.c
|
||||
daemon/mouse_handler.c
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: libx52 0.3.3\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n"
|
||||
"POT-Creation-Date: 2026-04-03 23:44-0700\n"
|
||||
"POT-Creation-Date: 2026-04-04 23:11-0700\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -599,41 +599,41 @@ msgstr ""
|
|||
msgid "Shutting down X52 clock manager thread"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/command.c:379
|
||||
#: daemon/command.c:380
|
||||
#, c-format
|
||||
msgid "Error reading from client %d: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/command.c:390
|
||||
#: daemon/command.c:391
|
||||
#, c-format
|
||||
msgid "Short write to client %d; expected %d bytes, wrote %d bytes"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/command.c:415
|
||||
#: daemon/command.c:416
|
||||
#, c-format
|
||||
msgid "Error %d during command loop: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/command.c:442
|
||||
#: daemon/command.c:443
|
||||
#, c-format
|
||||
msgid "Error creating command socket: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/command.c:450
|
||||
#: daemon/command.c:451
|
||||
#, c-format
|
||||
msgid "Error marking command socket as nonblocking: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/command.c:456
|
||||
#: daemon/command.c:457
|
||||
#, c-format
|
||||
msgid "Error listening on command socket: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/command.c:460
|
||||
#: daemon/command.c:461
|
||||
msgid "Starting command processing thread"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/command.c:478
|
||||
#: daemon/command.c:479
|
||||
msgid "Shutting down command processing thread"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -774,17 +774,41 @@ msgstr ""
|
|||
msgid "Shutting down X52 I/O driver thread"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:67
|
||||
#: daemon/keyboard_layout.c:69
|
||||
msgid ""
|
||||
"Invalid Profiles.KeyboardLayout value; using default layout basename 'us'"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/keyboard_layout.c:77
|
||||
#, c-format
|
||||
msgid "Keyboard layout '%s' could not be loaded (%s); loading default 'us'"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/keyboard_layout.c:83
|
||||
#, c-format
|
||||
msgid "Could not load keyboard layout from %s/x52d (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/keyboard_layout.c:92
|
||||
#, c-format
|
||||
msgid "Keyboard layout ready: %s (%s)"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/keyboard_layout.c:93
|
||||
msgid "no description"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:68
|
||||
#, c-format
|
||||
msgid "Error %d setting log file: %s\n"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:83
|
||||
#: daemon/main.c:84
|
||||
#, c-format
|
||||
msgid "Error %d installing handler for signal %d: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:94
|
||||
#: daemon/main.c:95
|
||||
#, c-format
|
||||
msgid ""
|
||||
"Usage: %s [-f] [-v] [-q]\n"
|
||||
|
|
@ -794,88 +818,88 @@ msgid ""
|
|||
"\t[-b notify-socket-path]\n"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:129
|
||||
#: daemon/main.c:130
|
||||
#, c-format
|
||||
msgid "Daemon is already running as PID %u"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:271
|
||||
#: daemon/main.c:272
|
||||
#, c-format
|
||||
msgid "Unable to parse configuration override '%s'\n"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:303
|
||||
#: daemon/main.c:304
|
||||
#, c-format
|
||||
msgid "Foreground = %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:303 daemon/main.c:304
|
||||
#: daemon/main.c:304 daemon/main.c:305
|
||||
msgid "true"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:303 daemon/main.c:304
|
||||
#: daemon/main.c:304 daemon/main.c:305
|
||||
msgid "false"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:304
|
||||
#, c-format
|
||||
msgid "Quiet = %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:305
|
||||
#, c-format
|
||||
msgid "Verbosity = %d"
|
||||
msgid "Quiet = %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:306
|
||||
#, c-format
|
||||
msgid "Log file = %s"
|
||||
msgid "Verbosity = %d"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:307
|
||||
#, c-format
|
||||
msgid "Config file = %s"
|
||||
msgid "Log file = %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:308
|
||||
#, c-format
|
||||
msgid "PID file = %s"
|
||||
msgid "Config file = %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:309
|
||||
#, c-format
|
||||
msgid "Command socket = %s"
|
||||
msgid "PID file = %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:310
|
||||
#, c-format
|
||||
msgid "Command socket = %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:311
|
||||
#, c-format
|
||||
msgid "Notify socket = %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:321
|
||||
#: daemon/main.c:322
|
||||
#, c-format
|
||||
msgid "Error %d blocking signals on child threads: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:338
|
||||
#: daemon/main.c:339
|
||||
#, c-format
|
||||
msgid "Error %d unblocking signals on child threads: %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:351
|
||||
#: daemon/main.c:352
|
||||
msgid "Reloading X52 configuration"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:358
|
||||
#: daemon/main.c:359
|
||||
msgid "Saving X52 configuration to disk"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:364
|
||||
#: daemon/main.c:365
|
||||
#, c-format
|
||||
msgid "Received termination signal %s"
|
||||
msgstr ""
|
||||
|
||||
#: daemon/main.c:379
|
||||
#: daemon/main.c:381
|
||||
msgid "Shutting down X52 daemon"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
93
po/xx_PL.po
93
po/xx_PL.po
|
|
@ -5,10 +5,10 @@
|
|||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: libx52 0.2.3\n"
|
||||
"Project-Id-Version: libx52 0.3.3\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n"
|
||||
"POT-Creation-Date: 2026-04-03 23:44-0700\n"
|
||||
"PO-Revision-Date: 2026-04-01 20:50-0700\n"
|
||||
"POT-Creation-Date: 2026-04-04 23:11-0700\n"
|
||||
"PO-Revision-Date: 2026-04-04 12:00-0700\n"
|
||||
"Last-Translator: Nirenjan Krishnan <nirenjan@gmail.com>\n"
|
||||
"Language-Team: Dummy Language for testing i18n\n"
|
||||
"Language: xx_PL\n"
|
||||
|
|
@ -645,42 +645,42 @@ msgstr "Erroray %d initializingay ockclay eadthray: %s"
|
|||
msgid "Shutting down X52 clock manager thread"
|
||||
msgstr "Uttingshay ownday X52 ockclay anagermay eadthray"
|
||||
|
||||
#: daemon/command.c:379
|
||||
#: daemon/command.c:380
|
||||
#, c-format
|
||||
msgid "Error reading from client %d: %s"
|
||||
msgstr "Erroray eadingray omfray ientclay %d: %s"
|
||||
|
||||
#: daemon/command.c:390
|
||||
#: daemon/command.c:391
|
||||
#, c-format
|
||||
msgid "Short write to client %d; expected %d bytes, wrote %d bytes"
|
||||
msgstr ""
|
||||
"Ortshay itewray otay ientclay %d; expecteday %d ytesbay, otewray %d ytesbay"
|
||||
|
||||
#: daemon/command.c:415
|
||||
#: daemon/command.c:416
|
||||
#, c-format
|
||||
msgid "Error %d during command loop: %s"
|
||||
msgstr "Erroray %d uringday ommandcay ooplay: %s"
|
||||
|
||||
#: daemon/command.c:442
|
||||
#: daemon/command.c:443
|
||||
#, c-format
|
||||
msgid "Error creating command socket: %s"
|
||||
msgstr "Erroray eatingcray ommandcay ocketsay: %s"
|
||||
|
||||
#: daemon/command.c:450
|
||||
#: daemon/command.c:451
|
||||
#, c-format
|
||||
msgid "Error marking command socket as nonblocking: %s"
|
||||
msgstr "Erroray arkingmay ommandcay ocketsay asay onblockingnay: %s"
|
||||
|
||||
#: daemon/command.c:456
|
||||
#: daemon/command.c:457
|
||||
#, c-format
|
||||
msgid "Error listening on command socket: %s"
|
||||
msgstr "Erroray isteninglay onay ommandcay ocketsay: %s"
|
||||
|
||||
#: daemon/command.c:460
|
||||
#: daemon/command.c:461
|
||||
msgid "Starting command processing thread"
|
||||
msgstr "Artingstay ommandcay ocessingpray eadthray"
|
||||
|
||||
#: daemon/command.c:478
|
||||
#: daemon/command.c:479
|
||||
msgid "Shutting down command processing thread"
|
||||
msgstr "Uttingshay ownday ommandcay ocessingpray eadthray"
|
||||
|
||||
|
|
@ -821,17 +821,46 @@ msgstr "Erroray %d initializingay I/O iverdray eadthray: %s"
|
|||
msgid "Shutting down X52 I/O driver thread"
|
||||
msgstr "Uttingshay ownday X52 I/O iverdray eadthray"
|
||||
|
||||
#: daemon/main.c:67
|
||||
#: daemon/keyboard_layout.c:69
|
||||
msgid ""
|
||||
"Invalid Profiles.KeyboardLayout value; using default layout basename 'us'"
|
||||
msgstr ""
|
||||
"Invaliday Ofilespray.EyboardKayayoutLay aluevay; usingay efaultday ayoutlay "
|
||||
"asenambay 'us'"
|
||||
|
||||
#: daemon/keyboard_layout.c:77
|
||||
#, c-format
|
||||
msgid "Keyboard layout '%s' could not be loaded (%s); loading default 'us'"
|
||||
msgstr ""
|
||||
"EyboardKay ayoutlay '%s' ouldcay otnay ebay oadedlay (%s); oadinglay "
|
||||
"efaultday 'us'"
|
||||
|
||||
#: daemon/keyboard_layout.c:83
|
||||
#, c-format
|
||||
msgid "Could not load keyboard layout from %s/x52d (%s)"
|
||||
msgstr ""
|
||||
"ouldCay otnay oadlay eyboardkay ayoutlay omfray %s/x52d (%s)"
|
||||
|
||||
#: daemon/keyboard_layout.c:92
|
||||
#, c-format
|
||||
msgid "Keyboard layout ready: %s (%s)"
|
||||
msgstr "EyboardKay ayoutlay eadyray: %s (%s)"
|
||||
|
||||
#: daemon/keyboard_layout.c:93
|
||||
msgid "no description"
|
||||
msgstr "onay escriptionday"
|
||||
|
||||
#: daemon/main.c:68
|
||||
#, c-format
|
||||
msgid "Error %d setting log file: %s\n"
|
||||
msgstr "Erroray %d ettingsay oglay ilefay: %s\n"
|
||||
|
||||
#: daemon/main.c:83
|
||||
#: daemon/main.c:84
|
||||
#, c-format
|
||||
msgid "Error %d installing handler for signal %d: %s"
|
||||
msgstr "Erroray %d installingay andlerhay orfay ignalsay %d: %s"
|
||||
|
||||
#: daemon/main.c:94
|
||||
#: daemon/main.c:95
|
||||
#, c-format
|
||||
msgid ""
|
||||
"Usage: %s [-f] [-v] [-q]\n"
|
||||
|
|
@ -847,88 +876,88 @@ msgstr ""
|
|||
"\t[-b otifynay-ocketsay-athpay]\n"
|
||||
"\n"
|
||||
|
||||
#: daemon/main.c:129
|
||||
#: daemon/main.c:130
|
||||
#, c-format
|
||||
msgid "Daemon is already running as PID %u"
|
||||
msgstr "Aemonday isay alreadyay unningray asay IDPay %u"
|
||||
|
||||
#: daemon/main.c:271
|
||||
#: daemon/main.c:272
|
||||
#, c-format
|
||||
msgid "Unable to parse configuration override '%s'\n"
|
||||
msgstr "Unableay otay arsepay onfigurationcay overrideay '%s'\n"
|
||||
|
||||
#: daemon/main.c:303
|
||||
#: daemon/main.c:304
|
||||
#, c-format
|
||||
msgid "Foreground = %s"
|
||||
msgstr "Oregroundfay = %s"
|
||||
|
||||
#: daemon/main.c:303 daemon/main.c:304
|
||||
#: daemon/main.c:304 daemon/main.c:305
|
||||
msgid "true"
|
||||
msgstr "uetray"
|
||||
|
||||
#: daemon/main.c:303 daemon/main.c:304
|
||||
#: daemon/main.c:304 daemon/main.c:305
|
||||
msgid "false"
|
||||
msgstr "alsefay"
|
||||
|
||||
#: daemon/main.c:304
|
||||
#: daemon/main.c:305
|
||||
#, c-format
|
||||
msgid "Quiet = %s"
|
||||
msgstr "Uietqay = %s"
|
||||
|
||||
#: daemon/main.c:305
|
||||
#: daemon/main.c:306
|
||||
#, c-format
|
||||
msgid "Verbosity = %d"
|
||||
msgstr "Erbosityvay = %d"
|
||||
|
||||
#: daemon/main.c:306
|
||||
#: daemon/main.c:307
|
||||
#, c-format
|
||||
msgid "Log file = %s"
|
||||
msgstr "Oglay ilefay = %s"
|
||||
|
||||
#: daemon/main.c:307
|
||||
#: daemon/main.c:308
|
||||
#, c-format
|
||||
msgid "Config file = %s"
|
||||
msgstr "Onfigcay ilefay = %s"
|
||||
|
||||
#: daemon/main.c:308
|
||||
#: daemon/main.c:309
|
||||
#, c-format
|
||||
msgid "PID file = %s"
|
||||
msgstr "IDPay ilefay = %s"
|
||||
|
||||
#: daemon/main.c:309
|
||||
#: daemon/main.c:310
|
||||
#, c-format
|
||||
msgid "Command socket = %s"
|
||||
msgstr "Ommandcay ocketsay = %s"
|
||||
|
||||
#: daemon/main.c:310
|
||||
#: daemon/main.c:311
|
||||
#, c-format
|
||||
msgid "Notify socket = %s"
|
||||
msgstr "Otifynay ocketsay = %s"
|
||||
|
||||
#: daemon/main.c:321
|
||||
#: daemon/main.c:322
|
||||
#, c-format
|
||||
msgid "Error %d blocking signals on child threads: %s"
|
||||
msgstr "Erroray %d ockingblay ignalssay onay ildchay eadsthray: %s"
|
||||
|
||||
#: daemon/main.c:338
|
||||
#: daemon/main.c:339
|
||||
#, c-format
|
||||
msgid "Error %d unblocking signals on child threads: %s"
|
||||
msgstr "Erroray %d unblockingay ignalssay onay ildchay eadsthray: %s"
|
||||
|
||||
#: daemon/main.c:351
|
||||
#: daemon/main.c:352
|
||||
msgid "Reloading X52 configuration"
|
||||
msgstr "Eloadingray X52 onfigurationcay"
|
||||
|
||||
#: daemon/main.c:358
|
||||
#: daemon/main.c:359
|
||||
msgid "Saving X52 configuration to disk"
|
||||
msgstr "Avingsay X52 onfigurationcay otay iskday"
|
||||
|
||||
#: daemon/main.c:364
|
||||
#: daemon/main.c:365
|
||||
#, c-format
|
||||
msgid "Received termination signal %s"
|
||||
msgstr "Eceivedray erminationtay ignalsay %s"
|
||||
|
||||
#: daemon/main.c:379
|
||||
#: daemon/main.c:381
|
||||
msgid "Shutting down X52 daemon"
|
||||
msgstr "Uttingshay ownday X52 aemonday"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
"""Regression checks for layout HID allowlist and token maps (see vkm.h / layout_usage_allowlist.c)."""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def _emit_tap(test_cases):
|
||||
"""test_cases: iterable of (description, callable). Callable raises on failure."""
|
||||
case_list = list(test_cases)
|
||||
print("TAP version 13")
|
||||
print("1..%d" % len(case_list))
|
||||
failed = 0
|
||||
for i, (desc, fn) in enumerate(case_list, 1):
|
||||
try:
|
||||
fn()
|
||||
except AssertionError as e:
|
||||
failed += 1
|
||||
print("not ok %d - %s" % (i, desc))
|
||||
msg = str(e) if str(e) else "assertion failed"
|
||||
for line in msg.splitlines():
|
||||
print("# %s" % line)
|
||||
else:
|
||||
print("ok %d - %s" % (i, desc))
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
def main():
|
||||
root = pathlib.Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(root / "tools"))
|
||||
|
||||
import x52compile_layout as x
|
||||
|
||||
def check_allowed_set():
|
||||
expected = frozenset(range(0x04, 0x3A)) | frozenset((0x64,))
|
||||
assert x.ALLOWED_KEY_USAGES == expected, "%r != %r" % (x.ALLOWED_KEY_USAGES, expected)
|
||||
|
||||
def check_allowlist_c_range():
|
||||
src = (root / "daemon" / "layout_usage_allowlist.c").read_text(encoding="utf-8")
|
||||
assert "usage >= 0x04 && usage <= 0x39" in src, (
|
||||
"daemon/layout_usage_allowlist.c: expected main-block range check"
|
||||
)
|
||||
|
||||
def check_allowlist_c_intl():
|
||||
src = (root / "daemon" / "layout_usage_allowlist.c").read_text(encoding="utf-8")
|
||||
assert "usage == 0x64" in src, (
|
||||
"daemon/layout_usage_allowlist.c: expected INTL_BACKSLASH (0x64) check"
|
||||
)
|
||||
|
||||
def check_vkm_modifier_symbols():
|
||||
vkm = (root / "vkm" / "vkm.h").read_text(encoding="utf-8")
|
||||
for name in (
|
||||
"VKM_KEY_MOD_LCTRL",
|
||||
"VKM_KEY_MOD_LSHIFT",
|
||||
"VKM_KEY_MOD_LALT",
|
||||
"VKM_KEY_MOD_LGUI",
|
||||
"VKM_KEY_MOD_RCTRL",
|
||||
"VKM_KEY_MOD_RSHIFT",
|
||||
"VKM_KEY_MOD_RALT",
|
||||
"VKM_KEY_MOD_RGUI",
|
||||
):
|
||||
assert re.search(r"%s\s*=\s*\([^)]+\)" % re.escape(name), vkm), (
|
||||
"could not find %s in vkm.h" % name
|
||||
)
|
||||
|
||||
def check_modifier_bits():
|
||||
assert x.MODIFIER_TOKEN_TO_BIT["LCTRL"] == 1 << 0 and x.MODIFIER_TOKEN_TO_BIT["RALT"] == 1 << 6, (
|
||||
"modifier map mismatch vs expected HID bit layout"
|
||||
)
|
||||
|
||||
def check_tokens_in_allowlist():
|
||||
for _tok, usage in x.KEY_TOKEN_TO_USAGE.items():
|
||||
assert usage in x.ALLOWED_KEY_USAGES, (
|
||||
"token maps outside allowlist: %r -> %#x" % (_tok, usage)
|
||||
)
|
||||
|
||||
def check_usage_from_key_token_a():
|
||||
assert x.usage_from_key_token("a") == 0x04
|
||||
|
||||
def check_usage_from_key_token_space():
|
||||
assert x.usage_from_key_token("SPACE") == 0x2C
|
||||
|
||||
def check_modifier_mask_ctrl_shift():
|
||||
assert x.modifier_mask_from_tokens("CTRL+SHIFT") == (1 << 0) | (1 << 1)
|
||||
|
||||
tests = (
|
||||
("ALLOWED_KEY_USAGES matches main block + INTL_BACKSLASH", check_allowed_set),
|
||||
("layout_usage_allowlist.c main-block range", check_allowlist_c_range),
|
||||
("layout_usage_allowlist.c INTL_BACKSLASH (0x64)", check_allowlist_c_intl),
|
||||
("vkm.h modifier symbol names", check_vkm_modifier_symbols),
|
||||
("MODIFIER_TOKEN_TO_BIT LCTRL/RALT bits", check_modifier_bits),
|
||||
("KEY_TOKEN_TO_USAGE subset of allowlist", check_tokens_in_allowlist),
|
||||
("usage_from_key_token(a)", check_usage_from_key_token_a),
|
||||
("usage_from_key_token(SPACE)", check_usage_from_key_token_space),
|
||||
("modifier_mask_from_tokens(CTRL+SHIFT)", check_modifier_mask_ctrl_shift),
|
||||
)
|
||||
|
||||
return _emit_tap(tests)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
"""TAP tests for ``tools/x52compile_layout.py`` (v1 ``.x52l`` compiler)."""
|
||||
|
||||
import pathlib
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zlib
|
||||
|
||||
|
||||
def _emit_tap(test_cases):
|
||||
"""test_cases: iterable of (description, callable). Callable raises on failure."""
|
||||
case_list = list(test_cases)
|
||||
print("TAP version 13")
|
||||
print("1..%d" % len(case_list))
|
||||
failed = 0
|
||||
for i, (desc, fn) in enumerate(case_list, 1):
|
||||
try:
|
||||
fn()
|
||||
except AssertionError as e:
|
||||
failed += 1
|
||||
print("not ok %d - %s" % (i, desc))
|
||||
msg = str(e) if str(e) else "assertion failed"
|
||||
for line in msg.splitlines():
|
||||
print("# %s" % line)
|
||||
else:
|
||||
print("ok %d - %s" % (i, desc))
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
def _x52_crc32_verify(buf):
|
||||
"""CRC-32 over file with checksum field (bytes 12..15) taken as zero."""
|
||||
return zlib.crc32(buf[:12] + b"\0\0\0\0" + buf[16:]) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def main():
|
||||
root = pathlib.Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(root / "tools"))
|
||||
|
||||
import x52compile_layout as x
|
||||
|
||||
def check_layout_compile_error_is_syntax_error():
|
||||
assert issubclass(x.LayoutCompileError, SyntaxError)
|
||||
|
||||
def check_parse_chord_simple():
|
||||
mod, usage = x.parse_chord("a")
|
||||
assert mod == 0 and usage == 0x04
|
||||
|
||||
def check_parse_chord_shifted_letter():
|
||||
mod, usage = x.parse_chord("SHIFT+a")
|
||||
assert mod == (1 << 1) and usage == 0x04
|
||||
|
||||
def check_parse_chord_space_token():
|
||||
mod, usage = x.parse_chord("SPACE")
|
||||
assert mod == 0 and usage == 0x2C
|
||||
|
||||
def check_compile_minimal_header_and_entry():
|
||||
src = "name: t\na a\n"
|
||||
buf = x.compile_layout_source("minimal.layout", src)
|
||||
assert buf[0:4] == b"X52L"
|
||||
assert struct.unpack_from("!H", buf, 4)[0] == x.FORMAT_VERSION
|
||||
assert struct.unpack_from("!H", buf, 6)[0] == 0
|
||||
limit = struct.unpack_from("!I", buf, 8)[0]
|
||||
assert limit == 98
|
||||
assert len(buf) == x.HEADER_BYTES + 2 * limit
|
||||
off = x.HEADER_BYTES + 2 * ord('a')
|
||||
assert buf[off] == 0 and buf[off + 1] == 0x04
|
||||
stored = struct.unpack_from("!I", buf, 12)[0]
|
||||
assert stored == _x52_crc32_verify(buf)
|
||||
|
||||
def check_compile_name_and_description_fields():
|
||||
src = "name: demo\ndescription: hello\nU+0020 SPACE\n"
|
||||
buf = x.compile_layout_source("meta.layout", src)
|
||||
name_end = buf.index(b"\0", 16)
|
||||
assert buf[16:name_end] == b"demo"
|
||||
desc_end = buf.index(b"\0", 48)
|
||||
assert buf[48:desc_end] == b"hello"
|
||||
space_off = x.HEADER_BYTES + 2 * 0x20
|
||||
assert buf[space_off + 1] == 0x2C
|
||||
|
||||
def check_compile_shift_uppercase_mapping():
|
||||
src = "name: u\nA SHIFT+a\n"
|
||||
buf = x.compile_layout_source("up.layout", src)
|
||||
limit = struct.unpack_from("!I", buf, 8)[0]
|
||||
assert limit == ord("A") + 1
|
||||
off = x.HEADER_BYTES + 2 * ord("A")
|
||||
assert buf[off] == (1 << 1) and buf[off + 1] == 0x04
|
||||
|
||||
def check_reject_missing_name():
|
||||
try:
|
||||
x.compile_layout_source("bad.layout", "a a\n")
|
||||
except x.LayoutCompileError as e:
|
||||
assert "missing name" in str(e)
|
||||
else:
|
||||
assert False, "expected LayoutCompileError"
|
||||
|
||||
def check_reject_no_mappings():
|
||||
try:
|
||||
x.compile_layout_source("bad.layout", "name: x\n")
|
||||
except x.LayoutCompileError as e:
|
||||
assert "no mappings" in str(e)
|
||||
else:
|
||||
assert False, "expected LayoutCompileError"
|
||||
|
||||
def check_reject_duplicate_codepoint():
|
||||
src = "name: x\na a\na b\n"
|
||||
try:
|
||||
x.compile_layout_source("dup.layout", src)
|
||||
except x.LayoutCompileError as e:
|
||||
assert "duplicate" in str(e) and "U+0061" in str(e)
|
||||
assert "line" in str(e)
|
||||
else:
|
||||
assert False, "expected LayoutCompileError"
|
||||
|
||||
def check_reject_invalid_scalar():
|
||||
src = "name: x\nU+D800 SPACE\n"
|
||||
try:
|
||||
x.compile_layout_source("surrogate.layout", src)
|
||||
except x.LayoutCompileError as e:
|
||||
assert "invalid Unicode scalar" in str(e)
|
||||
assert e.filename == "surrogate.layout"
|
||||
assert e.lineno == 2
|
||||
else:
|
||||
assert False, "expected LayoutCompileError"
|
||||
|
||||
def check_reject_scalar_above_unicode_max():
|
||||
src = "name: x\nU+110000 SPACE\n"
|
||||
try:
|
||||
x.compile_layout_source("high.layout", src)
|
||||
except x.LayoutCompileError as e:
|
||||
assert "invalid Unicode scalar" in str(e)
|
||||
else:
|
||||
assert False, "expected LayoutCompileError"
|
||||
|
||||
def check_reject_bad_chord_line():
|
||||
src = "name: x\na NOT_A_KEY\n"
|
||||
try:
|
||||
x.compile_layout_source("chord.layout", src)
|
||||
except x.LayoutCompileError as e:
|
||||
assert "unknown key token" in str(e)
|
||||
assert e.lineno == 2
|
||||
else:
|
||||
assert False, "expected LayoutCompileError"
|
||||
|
||||
def check_reject_oversize_name():
|
||||
long_name = "n" * 32
|
||||
src = "name: %s\na a\n" % long_name
|
||||
try:
|
||||
x.compile_layout_source("long.layout", src)
|
||||
except x.LayoutCompileError as e:
|
||||
assert "exceeds" in str(e) and "UTF-8" in str(e)
|
||||
else:
|
||||
assert False, "expected LayoutCompileError"
|
||||
|
||||
def check_default_allows_name_at_31_bytes():
|
||||
name = "n" * 31
|
||||
src = "name: %s\na a\n" % name
|
||||
buf = x.compile_layout_source("ok31.layout", src)
|
||||
name_end = buf.index(b"\0", 16)
|
||||
assert buf[16:name_end] == name.encode("utf-8")
|
||||
assert struct.unpack_from("!H", buf, 6)[0] == 0
|
||||
|
||||
def check_default_allows_description_at_63_bytes():
|
||||
desc = "d" * 63
|
||||
src = "name: ok\ndescription: %s\na a\n" % desc
|
||||
buf = x.compile_layout_source("ok63.layout", src)
|
||||
desc_end = buf.index(b"\0", 48)
|
||||
assert buf[48:desc_end] == desc.encode("utf-8")
|
||||
assert struct.unpack_from("!H", buf, 6)[0] == 0
|
||||
|
||||
def check_reject_invalid_layout_name():
|
||||
for bad in ("bad/name", "a b", "caf\u00e9", "dot."):
|
||||
src = "name: %s\na a\n" % bad
|
||||
try:
|
||||
x.compile_layout_source("name.layout", src)
|
||||
except x.LayoutCompileError as e:
|
||||
assert "ASCII letters" in str(e) or "hyphen" in str(e)
|
||||
else:
|
||||
assert False, "expected LayoutCompileError for %r" % bad
|
||||
|
||||
def check_compile_name_hyphen_underscore():
|
||||
src = "name: a-b_9\na a\n"
|
||||
buf = x.compile_layout_source("hy.layout", src)
|
||||
name_end = buf.index(b"\0", 16)
|
||||
assert buf[16:name_end] == b"a-b_9"
|
||||
|
||||
def check_reject_oversize_description():
|
||||
long_desc = "d" * 64
|
||||
src = "name: x\ndescription: %s\na a\n" % long_desc
|
||||
try:
|
||||
x.compile_layout_source("longdesc.layout", src)
|
||||
except x.LayoutCompileError as e:
|
||||
assert "description exceeds" in str(e)
|
||||
else:
|
||||
assert False, "expected LayoutCompileError"
|
||||
|
||||
def check_truncate_metadata_sets_name_flag_and_field():
|
||||
long_name = "n" * 32
|
||||
src = "name: %s\na a\n" % long_name
|
||||
buf = x.compile_layout_source("trunc.layout", src, truncate_metadata=True)
|
||||
assert struct.unpack_from("!H", buf, 6)[0] == x.FLAG_NAME_TRUNCATED
|
||||
name_end = buf.index(b"\0", 16)
|
||||
assert buf[16:name_end] == (b"n" * 31)
|
||||
assert struct.unpack_from("!I", buf, 12)[0] == _x52_crc32_verify(buf)
|
||||
|
||||
def check_truncate_metadata_sets_description_flag():
|
||||
long_desc = "d" * 64
|
||||
src = "name: ok\ndescription: %s\na a\n" % long_desc
|
||||
buf = x.compile_layout_source("truncdesc.layout", src, truncate_metadata=True)
|
||||
assert struct.unpack_from("!H", buf, 6)[0] == x.FLAG_DESCRIPTION_TRUNCATED
|
||||
desc_end = buf.index(b"\0", 48)
|
||||
assert buf[48:desc_end] == (b"d" * 63)
|
||||
|
||||
def check_truncate_metadata_sets_both_flags_when_both_long():
|
||||
long_name = "n" * 32
|
||||
long_desc = "d" * 64
|
||||
src = "name: %s\ndescription: %s\na a\n" % (long_name, long_desc)
|
||||
buf = x.compile_layout_source("bothtrunc.layout", src, truncate_metadata=True)
|
||||
f = struct.unpack_from("!H", buf, 6)[0]
|
||||
assert f == (x.FLAG_NAME_TRUNCATED | x.FLAG_DESCRIPTION_TRUNCATED)
|
||||
name_end = buf.index(b"\0", 16)
|
||||
assert buf[16:name_end] == (b"n" * 31)
|
||||
desc_end = buf.index(b"\0", 48)
|
||||
assert buf[48:desc_end] == (b"d" * 63)
|
||||
assert struct.unpack_from("!I", buf, 12)[0] == _x52_crc32_verify(buf)
|
||||
|
||||
def check_truncate_metadata_utf8_safe_no_split():
|
||||
# Names are ASCII-only; exercise UTF-8 trim on description (32 × "é" → 63-byte cap).
|
||||
long_desc = "\u00e9" * 32
|
||||
src = "name: ok\ndescription: %s\na a\n" % long_desc
|
||||
buf = x.compile_layout_source("utf8.layout", src, truncate_metadata=True)
|
||||
assert struct.unpack_from("!H", buf, 6)[0] == x.FLAG_DESCRIPTION_TRUNCATED
|
||||
desc_end = buf.index(b"\0", 48)
|
||||
assert buf[48:desc_end].decode("utf-8") == "\u00e9" * 31
|
||||
|
||||
def check_data_layouts_us_layout_compiles():
|
||||
us_path = root / "data" / "layouts" / "us.layout"
|
||||
if not us_path.is_file():
|
||||
return
|
||||
text = us_path.read_text(encoding="utf-8")
|
||||
buf = x.compile_layout_source(str(us_path), text)
|
||||
assert buf[0:4] == b"X52L"
|
||||
limit = struct.unpack_from("!I", buf, 8)[0]
|
||||
assert len(buf) == x.HEADER_BYTES + 2 * limit
|
||||
assert struct.unpack_from("!I", buf, 12)[0] == _x52_crc32_verify(buf)
|
||||
|
||||
def check_compile_layout_file_rejects_mismatched_output_basename():
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
src = pathlib.Path(td) / "in.layout"
|
||||
src.write_text("name: us\na a\n", encoding="utf-8")
|
||||
bad = pathlib.Path(td) / "other.x52l"
|
||||
try:
|
||||
x.compile_layout_file(str(src), str(bad))
|
||||
except x.LayoutCompileError as e:
|
||||
assert "us.x52l" in str(e) and "other.x52l" in str(e)
|
||||
else:
|
||||
assert False, "expected LayoutCompileError"
|
||||
|
||||
def check_compile_layout_file_accepts_matching_basename():
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
src = pathlib.Path(td) / "in.layout"
|
||||
src.write_text("name: us\na a\n", encoding="utf-8")
|
||||
out = pathlib.Path(td) / "us.x52l"
|
||||
x.compile_layout_file(str(src), str(out))
|
||||
assert out.is_file()
|
||||
assert out.stat().st_size == x.HEADER_BYTES + 2 * 98
|
||||
|
||||
def check_compile_layout_file_truncated_name_sets_output_basename():
|
||||
long_name = "n" * 32
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
src = pathlib.Path(td) / "in.layout"
|
||||
src.write_text("name: %s\na a\n" % long_name, encoding="utf-8")
|
||||
# Stored name is 31 × "n" → file must be nnn... (31) + .x52l
|
||||
out = pathlib.Path(td) / ("n" * 31 + ".x52l")
|
||||
x.compile_layout_file(str(src), str(out), truncate_metadata=True)
|
||||
assert out.is_file()
|
||||
|
||||
def check_cli_truncate_metadata_sets_flags_in_output():
|
||||
long_name = "n" * 32
|
||||
long_desc = "d" * 64
|
||||
script = root / "tools" / "x52compile_layout.py"
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
td = pathlib.Path(td)
|
||||
src = td / "in.layout"
|
||||
src.write_text(
|
||||
"name: %s\ndescription: %s\na a\n" % (long_name, long_desc),
|
||||
encoding="utf-8",
|
||||
)
|
||||
out = td / ("n" * 31 + ".x52l")
|
||||
r = subprocess.run(
|
||||
[sys.executable, str(script), "--truncate-metadata", str(src), str(out)],
|
||||
cwd=str(root),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
assert r.returncode == 0, r.stderr
|
||||
buf = out.read_bytes()
|
||||
f = struct.unpack_from("!H", buf, 6)[0]
|
||||
assert f == (x.FLAG_NAME_TRUNCATED | x.FLAG_DESCRIPTION_TRUNCATED)
|
||||
assert struct.unpack_from("!I", buf, 12)[0] == _x52_crc32_verify(buf)
|
||||
|
||||
tests = (
|
||||
("LayoutCompileError subclasses SyntaxError", check_layout_compile_error_is_syntax_error),
|
||||
("parse_chord(a)", check_parse_chord_simple),
|
||||
("parse_chord(SHIFT+a)", check_parse_chord_shifted_letter),
|
||||
("parse_chord(SPACE)", check_parse_chord_space_token),
|
||||
("compile minimal layout: header, size, entry, CRC", check_compile_minimal_header_and_entry),
|
||||
("compile name/description and U+ mapping", check_compile_name_and_description_fields),
|
||||
("compile SHIFT+a for uppercase letter", check_compile_shift_uppercase_mapping),
|
||||
("reject missing name:", check_reject_missing_name),
|
||||
("reject no mappings", check_reject_no_mappings),
|
||||
("reject duplicate codepoint", check_reject_duplicate_codepoint),
|
||||
("reject surrogate scalar U+D800", check_reject_invalid_scalar),
|
||||
("reject scalar U+110000", check_reject_scalar_above_unicode_max),
|
||||
("reject unknown key token with lineno", check_reject_bad_chord_line),
|
||||
("reject name longer than 31 UTF-8 bytes", check_reject_oversize_name),
|
||||
("default: name length 31 UTF-8 bytes accepted", check_default_allows_name_at_31_bytes),
|
||||
("default: description length 63 UTF-8 bytes accepted", check_default_allows_description_at_63_bytes),
|
||||
("reject layout name outside ASCII alnum hyphen underscore", check_reject_invalid_layout_name),
|
||||
("compile name with hyphen and underscore", check_compile_name_hyphen_underscore),
|
||||
("reject description longer than 63 UTF-8 bytes", check_reject_oversize_description),
|
||||
("--truncate-metadata: name flag and 31-byte field", check_truncate_metadata_sets_name_flag_and_field),
|
||||
("truncate_metadata: description flag and 63-byte field", check_truncate_metadata_sets_description_flag),
|
||||
("truncate_metadata: both long name and description set both flags", check_truncate_metadata_sets_both_flags_when_both_long),
|
||||
("truncate_metadata: UTF-8 safe trim (no split codepoint)", check_truncate_metadata_utf8_safe_no_split),
|
||||
("CLI --truncate-metadata sets composite flags and valid CRC", check_cli_truncate_metadata_sets_flags_in_output),
|
||||
("data/layouts/us.layout compiles with valid CRC", check_data_layouts_us_layout_compiles),
|
||||
("compile_layout_file rejects wrong output basename", check_compile_layout_file_rejects_mismatched_output_basename),
|
||||
("compile_layout_file writes when basename matches name:", check_compile_layout_file_accepts_matching_basename),
|
||||
("compile_layout_file: truncated stored name defines output basename", check_compile_layout_file_truncated_name_sets_output_basename),
|
||||
)
|
||||
|
||||
return _emit_tap(tests)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,436 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
"""Keyboard layout source (``.layout``) compiler and HID token maps.
|
||||
|
||||
Compiles human-readable ``.layout`` files into v1 ``.x52l`` binaries (see
|
||||
``daemon/layout_format.h``). The output file basename must be
|
||||
``{name}.x52l`` with ``name`` taken from the ``name:`` metadata (after any
|
||||
``--truncate-metadata`` trimming), matching ``Profiles.KeyboardLayout``.
|
||||
The non-modifier key set matches
|
||||
``x52_layout_usage_key_allowed`` in ``daemon/layout_usage_allowlist.c`` and
|
||||
``vkm_key`` / ``vkm_key_modifiers`` in ``vkm/vkm.h``.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HID page 0x07 usages allowed as the layout RHS key (one byte, non-modifier).
|
||||
# Main block: 0x04 (A) through 0x39 (Caps Lock); plus ISO third row backslash (0x64).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ALLOWED_KEY_USAGES = frozenset(range(0x04, 0x3A)) | frozenset((0x64,))
|
||||
|
||||
|
||||
def _letter_usage(ch: str) -> int:
|
||||
o = ord(ch)
|
||||
if not (ord("A") <= o <= ord("Z") or ord("a") <= o <= ord("z")):
|
||||
raise ValueError(ch)
|
||||
return 0x04 + (ord(ch.upper()) - ord("A"))
|
||||
|
||||
|
||||
def _build_key_token_map() -> Dict[str, int]:
|
||||
m: Dict[str, int] = {}
|
||||
|
||||
for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
||||
u = _letter_usage(c)
|
||||
m[c] = u
|
||||
m[c.lower()] = u
|
||||
|
||||
digit_usages = [0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27]
|
||||
for d, u in zip("1234567890", digit_usages):
|
||||
m[d] = u
|
||||
|
||||
named = {
|
||||
"ENTER": 0x28,
|
||||
"ESCAPE": 0x29,
|
||||
"ESC": 0x29,
|
||||
"BACKSPACE": 0x2A,
|
||||
"TAB": 0x2B,
|
||||
"SPACE": 0x2C,
|
||||
"MINUS": 0x2D,
|
||||
"EQUAL": 0x2E,
|
||||
"LEFT_BRACKET": 0x2F,
|
||||
"RIGHT_BRACKET": 0x30,
|
||||
"BACKSLASH": 0x31,
|
||||
"NONUS_HASH": 0x32,
|
||||
"SEMICOLON": 0x33,
|
||||
"APOSTROPHE": 0x34,
|
||||
"GRAVE_ACCENT": 0x35,
|
||||
"GRAVE": 0x35,
|
||||
"COMMA": 0x36,
|
||||
"PERIOD": 0x37,
|
||||
"SLASH": 0x38,
|
||||
"CAPS_LOCK": 0x39,
|
||||
"CAPS": 0x39,
|
||||
"INTL_BACKSLASH": 0x64,
|
||||
}
|
||||
m.update(named)
|
||||
|
||||
bad = {k: v for k, v in m.items() if v not in ALLOWED_KEY_USAGES}
|
||||
if bad:
|
||||
raise RuntimeError("internal error: token maps to disallowed usage: %r" % bad)
|
||||
return m
|
||||
|
||||
|
||||
KEY_TOKEN_TO_USAGE: Dict[str, int] = _build_key_token_map()
|
||||
|
||||
# Matches vkm_key_modifiers in vkm/vkm.h (HID modifier byte bits).
|
||||
MODIFIER_TOKEN_TO_BIT: Dict[str, int] = {
|
||||
"LCTRL": 1 << 0,
|
||||
"LSHIFT": 1 << 1,
|
||||
"LALT": 1 << 2,
|
||||
"LGUI": 1 << 3,
|
||||
"RCTRL": 1 << 4,
|
||||
"RSHIFT": 1 << 5,
|
||||
"RALT": 1 << 6,
|
||||
"RGUI": 1 << 7,
|
||||
# Convenience aliases — same as VKM_KEY_MOD_* macros in vkm.h
|
||||
"CTRL": 1 << 0,
|
||||
"SHIFT": 1 << 1,
|
||||
"ALT": 1 << 2,
|
||||
"GUI": 1 << 3,
|
||||
}
|
||||
|
||||
_MOD_KNOWN_BITS = 0
|
||||
for _b in MODIFIER_TOKEN_TO_BIT.values():
|
||||
_MOD_KNOWN_BITS |= _b
|
||||
if _MOD_KNOWN_BITS != 0xFF:
|
||||
raise RuntimeError("internal error: modifier map must cover HID bits 0-7")
|
||||
|
||||
|
||||
def modifier_mask_from_tokens(tokens: str) -> int:
|
||||
"""OR together modifier bits for plus-separated tokens (e.g. ``'CTRL+SHIFT'``)."""
|
||||
mask = 0
|
||||
for raw in tokens.replace(" ", "").split("+"):
|
||||
if not raw:
|
||||
continue
|
||||
try:
|
||||
bit = MODIFIER_TOKEN_TO_BIT[raw.upper()]
|
||||
except KeyError as e:
|
||||
raise KeyError("unknown modifier token %r" % raw) from e
|
||||
mask |= bit
|
||||
return mask
|
||||
|
||||
|
||||
def usage_from_key_token(token: str) -> int:
|
||||
"""Resolve a single key token string to a HID usage (page 0x07)."""
|
||||
key = token.strip()
|
||||
if key.isalpha() and len(key) == 1:
|
||||
u = KEY_TOKEN_TO_USAGE[key]
|
||||
else:
|
||||
u = KEY_TOKEN_TO_USAGE[key.upper()]
|
||||
return u
|
||||
|
||||
|
||||
class LayoutCompileError(SyntaxError):
|
||||
"""Invalid layout source or oversize metadata.
|
||||
|
||||
Subclasses :class:`SyntaxError` so errors can carry ``filename`` and ``lineno``
|
||||
and render as ``message (path, line N)``. Pass ``path`` and ``lineno`` only
|
||||
when ``message`` is the detail alone; for whole-file problems pass one fully
|
||||
formatted string and leave location arguments unset.
|
||||
"""
|
||||
|
||||
def __init__(self, message, path=None, lineno=None):
|
||||
if path is not None and lineno is not None:
|
||||
SyntaxError.__init__(self, message, (path, lineno, None, None))
|
||||
else:
|
||||
SyntaxError.__init__(self, message)
|
||||
|
||||
|
||||
HEADER_BYTES = 128
|
||||
FORMAT_VERSION = 1
|
||||
CODEPOINT_LIMIT_MAX = 0x110000
|
||||
NAME_MAX_UTF8 = 31
|
||||
DESC_MAX_UTF8 = 63
|
||||
|
||||
# v1 header flags (big-endian on disk); must match daemon/layout_format.h
|
||||
FLAG_NAME_TRUNCATED = 1
|
||||
FLAG_DESCRIPTION_TRUNCATED = 2
|
||||
FLAGS_KNOWN_V1 = FLAG_NAME_TRUNCATED | FLAG_DESCRIPTION_TRUNCATED
|
||||
|
||||
# Fixed 128-byte header: magic, version, flags, codepoint_limit, checksum (BE).
|
||||
_HEADER_PREFIX = struct.Struct("!4sHHII")
|
||||
|
||||
# Layout basename / ``name:`` field: matches safe config tokens (see Profiles.KeyboardLayout).
|
||||
_LAYOUT_NAME_RE = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||
|
||||
|
||||
def _validate_layout_name(name: str, path: str) -> None:
|
||||
if not _LAYOUT_NAME_RE.fullmatch(name):
|
||||
raise LayoutCompileError(
|
||||
"%s: layout name must contain only ASCII letters, digits, hyphen, or underscore"
|
||||
% path
|
||||
)
|
||||
|
||||
|
||||
def _utf8_trim_to_max_bytes(text: str, max_bytes: int) -> Tuple[str, bool]:
|
||||
"""Return ``(s, truncated)`` with ``len(s.encode('utf-8')) <= max_bytes``."""
|
||||
raw = text.encode("utf-8")
|
||||
if len(raw) <= max_bytes:
|
||||
return text, False
|
||||
cut = raw[:max_bytes]
|
||||
while cut:
|
||||
try:
|
||||
return cut.decode("utf-8"), True
|
||||
except UnicodeDecodeError:
|
||||
cut = cut[:-1]
|
||||
return "", True
|
||||
|
||||
|
||||
def parse_chord(
|
||||
chord: str, path: Optional[str] = None, lineno: Optional[int] = None
|
||||
) -> Tuple[int, int]:
|
||||
"""Return ``(modifiers_byte, usage)`` for a plus-separated HID chord.
|
||||
|
||||
If ``path`` and ``lineno`` are given, :exc:`LayoutCompileError` includes that
|
||||
location (via :class:`SyntaxError`); omit them for programmatic chord checks.
|
||||
"""
|
||||
parts = [p.strip() for p in chord.replace(" ", "").split("+") if p.strip()]
|
||||
if not parts:
|
||||
raise LayoutCompileError("empty chord", path, lineno)
|
||||
key_tok = parts[-1]
|
||||
mod_parts = parts[:-1]
|
||||
try:
|
||||
mod = modifier_mask_from_tokens("+".join(mod_parts)) if mod_parts else 0
|
||||
except KeyError as e:
|
||||
raise LayoutCompileError("unknown modifier in %r" % chord, path, lineno) from e
|
||||
try:
|
||||
usage = usage_from_key_token(key_tok)
|
||||
except KeyError as e:
|
||||
raise LayoutCompileError("unknown key token in %r" % chord, path, lineno) from e
|
||||
if usage not in ALLOWED_KEY_USAGES:
|
||||
raise LayoutCompileError("disallowed key usage for %r" % chord, path, lineno)
|
||||
return mod & 0xFF, usage & 0xFF
|
||||
|
||||
|
||||
_U_LINE = re.compile(r"^[Uu]\+([0-9A-Fa-f]{1,6})\s+(.+)$")
|
||||
_META_NAME = re.compile(r"(?i)^name:\s*(.*)$")
|
||||
_META_DESC = re.compile(r"(?i)^description:\s*(.*)$")
|
||||
|
||||
|
||||
def _parse_layout_source(
|
||||
path: str, text: str
|
||||
) -> Tuple[str, str, List[Tuple[int, int, str]]]:
|
||||
"""First pass: metadata and mapping lines. Returns ``(name, description, raw_maps)``."""
|
||||
name = "" # type: str
|
||||
description = "" # type: str
|
||||
raw_maps = [] # type: List[Tuple[int, int, str]]
|
||||
|
||||
for lineno, line in enumerate(text.splitlines(), 1):
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
mn = _META_NAME.match(stripped)
|
||||
if mn:
|
||||
name = mn.group(1).strip()
|
||||
continue
|
||||
md = _META_DESC.match(stripped)
|
||||
if md:
|
||||
description = md.group(1).strip()
|
||||
continue
|
||||
cp, chord = _parse_mapping_line(path, lineno, stripped)
|
||||
raw_maps.append((cp, lineno, chord))
|
||||
|
||||
if not name:
|
||||
raise LayoutCompileError("%s: missing name: metadata" % path)
|
||||
|
||||
return name, description, raw_maps
|
||||
|
||||
|
||||
def _layout_stored_name_from_bytes(buf: bytes) -> str:
|
||||
"""UTF-8 layout name as stored in the v1 header field (offset 16, up to first NUL)."""
|
||||
end = buf.index(0, 16, 48)
|
||||
return buf[16:end].decode("utf-8")
|
||||
|
||||
|
||||
def _parse_mapping_line(path: str, lineno: int, line: str) -> Tuple[int, str]:
|
||||
s = line.strip()
|
||||
m = _U_LINE.match(s)
|
||||
if m:
|
||||
cp = int(m.group(1), 16)
|
||||
if cp < 0 or cp > 0x10FFFF or (cp >= 0xD800 and cp <= 0xDFFF):
|
||||
raise LayoutCompileError("invalid Unicode scalar", path, lineno)
|
||||
return cp, m.group(2).strip()
|
||||
if not s:
|
||||
raise LayoutCompileError("empty line", path, lineno)
|
||||
ch = s[0]
|
||||
rest = s[1:].lstrip()
|
||||
if not rest:
|
||||
raise LayoutCompileError("missing chord after character", path, lineno)
|
||||
return ord(ch), rest
|
||||
|
||||
|
||||
def compile_layout_source(path: str, text: str, *, truncate_metadata: bool = False) -> bytes:
|
||||
"""Parse ``.layout`` source and return ``.x52l`` bytes.
|
||||
|
||||
``codepoint_limit`` is ``max_mapped_scalar + 1`` (dense table size).
|
||||
|
||||
If ``truncate_metadata`` is false (default), oversize ``name:`` / ``description:``
|
||||
values are errors. If true, UTF-8 strings are trimmed to the field limits
|
||||
(no split codepoints) and ``FLAG_NAME_TRUNCATED`` / ``FLAG_DESCRIPTION_TRUNCATED``
|
||||
are set in the on-disk ``flags`` field.
|
||||
"""
|
||||
name, description, raw_maps = _parse_layout_source(path, text)
|
||||
|
||||
flags = 0
|
||||
if truncate_metadata:
|
||||
name, name_t = _utf8_trim_to_max_bytes(name, NAME_MAX_UTF8)
|
||||
if name_t:
|
||||
flags |= FLAG_NAME_TRUNCATED
|
||||
if not name:
|
||||
raise LayoutCompileError("%s: name: empty after truncation to metadata limit" % path)
|
||||
description, desc_t = _utf8_trim_to_max_bytes(description, DESC_MAX_UTF8)
|
||||
if desc_t:
|
||||
flags |= FLAG_DESCRIPTION_TRUNCATED
|
||||
else:
|
||||
name_b = name.encode("utf-8")
|
||||
if len(name_b) > NAME_MAX_UTF8:
|
||||
raise LayoutCompileError(
|
||||
"%s: layout name exceeds %d UTF-8 bytes" % (path, NAME_MAX_UTF8)
|
||||
)
|
||||
desc_b = description.encode("utf-8")
|
||||
if len(desc_b) > DESC_MAX_UTF8:
|
||||
raise LayoutCompileError(
|
||||
"%s: description exceeds %d UTF-8 bytes" % (path, DESC_MAX_UTF8)
|
||||
)
|
||||
|
||||
_validate_layout_name(name, path)
|
||||
|
||||
name_b = name.encode("utf-8")
|
||||
desc_b = description.encode("utf-8")
|
||||
assert len(name_b) <= NAME_MAX_UTF8
|
||||
assert len(desc_b) <= DESC_MAX_UTF8
|
||||
if flags & ~FLAGS_KNOWN_V1:
|
||||
raise RuntimeError("internal error: unknown layout flags bits")
|
||||
|
||||
if not raw_maps:
|
||||
raise LayoutCompileError("%s: no mappings" % path)
|
||||
|
||||
by_cp = {} # type: Dict[int, Tuple[int, int, int]]
|
||||
for cp, lineno, chord in raw_maps:
|
||||
if cp in by_cp:
|
||||
raise LayoutCompileError(
|
||||
"duplicate mapping for U+%04X (also at line %d)" % (cp, by_cp[cp][2]),
|
||||
path,
|
||||
lineno,
|
||||
)
|
||||
mod, usage = parse_chord(chord, path, lineno)
|
||||
if usage == 0:
|
||||
raise LayoutCompileError("chord must not resolve to usage 0", path, lineno)
|
||||
by_cp[cp] = (mod, usage, lineno)
|
||||
|
||||
max_cp = max(by_cp.keys())
|
||||
limit = max_cp + 1
|
||||
if limit > CODEPOINT_LIMIT_MAX:
|
||||
raise LayoutCompileError("%s: codepoint_limit would exceed 0x110000" % path)
|
||||
|
||||
body = limit * 2
|
||||
buf = bytearray(HEADER_BYTES + body)
|
||||
|
||||
_HEADER_PREFIX.pack_into(buf, 0, b"X52L", FORMAT_VERSION, flags, limit, 0)
|
||||
buf[16 : 16 + len(name_b)] = name_b
|
||||
buf[48 : 48 + len(desc_b)] = desc_b
|
||||
|
||||
base = HEADER_BYTES
|
||||
for cp, (mod, usage, _) in by_cp.items():
|
||||
off = base + 2 * cp
|
||||
buf[off] = mod
|
||||
buf[off + 1] = usage
|
||||
|
||||
crc_input = bytes(buf[0:12]) + b"\0\0\0\0" + bytes(buf[16:])
|
||||
crc = zlib.crc32(crc_input, 0) & 0xFFFFFFFF
|
||||
struct.pack_into("!I", buf, 12, crc)
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def compile_layout_file(
|
||||
src_path: str, dst_path: str, *, truncate_metadata: bool = False
|
||||
) -> None:
|
||||
"""Compile ``src_path`` to ``dst_path``.
|
||||
|
||||
The destination basename must be ``{name}.x52l`` where ``name`` is the layout name
|
||||
stored in the binary (from the ``name:`` line, after any ``--truncate-metadata``
|
||||
trimming) so it matches ``Profiles.KeyboardLayout``.
|
||||
"""
|
||||
with open(src_path, "r", encoding="utf-8-sig") as f:
|
||||
text = f.read()
|
||||
out = compile_layout_source(src_path, text, truncate_metadata=truncate_metadata)
|
||||
stored_name = _layout_stored_name_from_bytes(out)
|
||||
expected_base = stored_name + ".x52l"
|
||||
if os.path.basename(dst_path) != expected_base:
|
||||
raise LayoutCompileError(
|
||||
"%s: output file must be named %r (from layout name:), got %r"
|
||||
% (src_path, expected_base, os.path.basename(dst_path))
|
||||
)
|
||||
with open(dst_path, "wb") as f:
|
||||
f.write(out)
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Compile human-readable .layout source into a v1 .x52l binary "
|
||||
"(see daemon/layout_format.h)."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--truncate-metadata",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Trim oversize name:/description: to header field limits and set truncation flags."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"source",
|
||||
metavar="layout.source",
|
||||
help="Input .layout file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"output",
|
||||
nargs="?",
|
||||
metavar="out.x52l",
|
||||
help=(
|
||||
"Output path; basename must be {name}.x52l from layout metadata. "
|
||||
"If omitted, write ./{name}.x52l in the current directory."
|
||||
),
|
||||
)
|
||||
parsed = parser.parse_args(argv)
|
||||
|
||||
truncate_metadata = bool(parsed.truncate_metadata)
|
||||
src_path = parsed.source
|
||||
|
||||
try:
|
||||
if parsed.output is None:
|
||||
with open(src_path, "r", encoding="utf-8-sig") as f:
|
||||
text = f.read()
|
||||
out = compile_layout_source(
|
||||
src_path, text, truncate_metadata=truncate_metadata
|
||||
)
|
||||
stored_name = _layout_stored_name_from_bytes(out)
|
||||
dst_path = os.path.join(os.getcwd(), stored_name + ".x52l")
|
||||
with open(dst_path, "wb") as f:
|
||||
f.write(out)
|
||||
else:
|
||||
compile_layout_file(
|
||||
src_path, parsed.output, truncate_metadata=truncate_metadata
|
||||
)
|
||||
except LayoutCompileError as e:
|
||||
sys.stderr.write("%s\n" % e)
|
||||
return 1
|
||||
except OSError as e:
|
||||
sys.stderr.write("%s\n" % e)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in New Issue