diff --git a/libx52io/io_strings.c b/libx52io/io_strings.c index 794a704..3ae3d04 100644 --- a/libx52io/io_strings.c +++ b/libx52io/io_strings.c @@ -8,6 +8,7 @@ #include "config.h" #include +#include #include "libx52io.h" #include "gettext.h" @@ -16,6 +17,28 @@ * on one to the enumeration definitions. */ +/* Locale-independent: only ASCII A–Z fold with a–z; digits and underscore unchanged */ +static int x52io_ascii_strcasecmp(const char *a, const char *b) +{ + for (;;) { + unsigned char ca = (unsigned char)*a++; + unsigned char cb = (unsigned char)*b++; + + if (ca >= 'A' && ca <= 'Z') { + ca = (unsigned char)(ca - 'A' + 'a'); + } + if (cb >= 'A' && cb <= 'Z') { + cb = (unsigned char)(cb - 'A' + 'a'); + } + if (ca != cb) { + return (int)ca - (int)cb; + } + if (ca == '\0') { + return 0; + } + } +} + /* String mapping for axis */ static const char * _x52io_axis_str[LIBX52IO_AXIS_MAX] = { [LIBX52IO_AXIS_X] = "ABS_X", @@ -92,6 +115,78 @@ const char * libx52io_button_to_str(libx52io_button button) return NULL; } +int libx52io_axis_from_str(const char *str, libx52io_axis *axis) +{ + libx52io_axis i; + + if (!str || !axis) { + return LIBX52IO_ERROR_INVALID; + } + + for (i = LIBX52IO_AXIS_X; i < LIBX52IO_AXIS_MAX; i++) { + if (strcmp(str, _x52io_axis_str[i]) == 0) { + *axis = i; + return LIBX52IO_SUCCESS; + } + } + + return LIBX52IO_ERROR_INVALID; +} + +int libx52io_button_from_str(const char *str, libx52io_button *button) +{ + libx52io_button b; + + if (!str || !button) { + return LIBX52IO_ERROR_INVALID; + } + + for (b = LIBX52IO_BTN_TRIGGER; b < LIBX52IO_BUTTON_MAX; b++) { + if (strcmp(str, _x52io_button_str[b]) == 0) { + *button = b; + return LIBX52IO_SUCCESS; + } + } + + return LIBX52IO_ERROR_INVALID; +} + +int libx52io_axis_from_str_nocase(const char *str, libx52io_axis *axis) +{ + libx52io_axis i; + + if (!str || !axis) { + return LIBX52IO_ERROR_INVALID; + } + + for (i = LIBX52IO_AXIS_X; i < LIBX52IO_AXIS_MAX; i++) { + if (x52io_ascii_strcasecmp(str, _x52io_axis_str[i]) == 0) { + *axis = i; + return LIBX52IO_SUCCESS; + } + } + + return LIBX52IO_ERROR_INVALID; +} + +int libx52io_button_from_str_nocase(const char *str, libx52io_button *button) +{ + libx52io_button b; + + if (!str || !button) { + return LIBX52IO_ERROR_INVALID; + } + + for (b = LIBX52IO_BTN_TRIGGER; b < LIBX52IO_BUTTON_MAX; b++) { + if (x52io_ascii_strcasecmp(str, _x52io_button_str[b]) == 0) { + *button = b; + return LIBX52IO_SUCCESS; + } + } + + return LIBX52IO_ERROR_INVALID; +} + /* Error buffer used for building custom error strings */ static char error_buffer[256]; diff --git a/libx52io/libx52io.h b/libx52io/libx52io.h index 111e1cd..e23d000 100644 --- a/libx52io/libx52io.h +++ b/libx52io/libx52io.h @@ -434,6 +434,62 @@ LIBX52IO_API const char *libx52io_axis_to_str(libx52io_axis axis); */ LIBX52IO_API const char *libx52io_button_to_str(libx52io_button button); +/** + * @brief Parse the string representation of an axis. + * + * Accepts tokens as returned by \ref libx52io_axis_to_str (e.g. \c ABS_X). + * + * @param[in] str NUL-terminated axis name + * @param[out] axis Receives the axis ID on success + * + * @returns \ref LIBX52IO_SUCCESS if \a str was recognized, + * \ref LIBX52IO_ERROR_INVALID if \a str or \a axis is NULL or \a str is unknown. + */ +LIBX52IO_API int libx52io_axis_from_str(const char *str, libx52io_axis *axis); + +/** + * @brief Parse the string representation of a button. + * + * Accepts tokens as returned by \ref libx52io_button_to_str (e.g. \c BTN_FIRE). + * + * @param[in] str NUL-terminated button name + * @param[out] button Receives the button ID on success + * + * @returns \ref LIBX52IO_SUCCESS if \a str was recognized, + * \ref LIBX52IO_ERROR_INVALID if \a str or \a button is NULL or \a str is unknown. + */ +LIBX52IO_API int libx52io_button_from_str(const char *str, libx52io_button *button); + +/** + * @brief Parse an axis name with ASCII case-insensitive matching. + * + * Like \ref libx52io_axis_from_str, but treats ASCII letters \c A– \c Z the same + * as \c a– \c z. Digits, underscore, and NUL must match exactly. Matching is + * not locale-dependent. + * + * @param[in] str NUL-terminated axis name + * @param[out] axis Receives the axis ID on success + * + * @returns \ref LIBX52IO_SUCCESS if \a str was recognized, + * \ref LIBX52IO_ERROR_INVALID if \a str or \a axis is NULL or \a str is unknown. + */ +LIBX52IO_API int libx52io_axis_from_str_nocase(const char *str, libx52io_axis *axis); + +/** + * @brief Parse a button name with ASCII case-insensitive matching. + * + * Like \ref libx52io_button_from_str, but treats ASCII letters \c A– \c Z the same + * as \c a– \c z. Digits, underscore, and NUL must match exactly. Matching is + * not locale-dependent. + * + * @param[in] str NUL-terminated button name + * @param[out] button Receives the button ID on success + * + * @returns \ref LIBX52IO_SUCCESS if \a str was recognized, + * \ref LIBX52IO_ERROR_INVALID if \a str or \a button is NULL or \a str is unknown. + */ +LIBX52IO_API int libx52io_button_from_str_nocase(const char *str, libx52io_button *button); + /** * @brief Get the vendor ID of the connected X52 device. * diff --git a/libx52io/meson.build b/libx52io/meson.build index ae9da40..0b08377 100644 --- a/libx52io/meson.build +++ b/libx52io/meson.build @@ -1,4 +1,4 @@ -libx52io_version = '1.0.0' +libx52io_version = '1.1.0' libx52io_files = files( 'io_core.c', @@ -37,4 +37,11 @@ test_parser = executable('test-parser', 'test_parser.c', libx52io_files, ) test('test-parser', test_parser, protocol: 'tap') +test_io_strings = executable('test-io-strings', 'test_io_strings.c', libx52io_files, + build_by_default: false, + dependencies: [dep_cmocka, dep_hidapi, dep_intl], + include_directories: [includes], +) +test('test-io-strings', test_io_strings, protocol: 'tap') + diff --git a/libx52io/test_io_strings.c b/libx52io/test_io_strings.c new file mode 100644 index 0000000..128b1af --- /dev/null +++ b/libx52io/test_io_strings.c @@ -0,0 +1,191 @@ +/* + * Saitek X52 IO driver - io string parse tests + * + * Copyright (C) 2012-2020 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 + */ + +#include +#include +#include +#include + +#include "libx52io.h" + +static void lower_ascii_only(char *dst, const char *src) +{ + for (; *src; src++, dst++) { + unsigned char c = (unsigned char)*src; + + if (c >= 'A' && c <= 'Z') { + c = (unsigned char)(c - 'A' + 'a'); + } + *dst = (char)c; + } + *dst = '\0'; +} + +static void test_axis_round_trip(void **state) +{ + libx52io_axis a; + libx52io_axis out; + + (void)state; + + for (a = LIBX52IO_AXIS_X; a < LIBX52IO_AXIS_MAX; a++) { + const char *s = libx52io_axis_to_str(a); + + assert_non_null(s); + assert_int_equal(LIBX52IO_SUCCESS, libx52io_axis_from_str(s, &out)); + assert_int_equal(a, out); + assert_ptr_equal(s, libx52io_axis_to_str(out)); + } +} + +static void test_button_round_trip(void **state) +{ + libx52io_button b; + libx52io_button out; + + (void)state; + + for (b = LIBX52IO_BTN_TRIGGER; b < LIBX52IO_BUTTON_MAX; b++) { + const char *s = libx52io_button_to_str(b); + + assert_non_null(s); + assert_int_equal(LIBX52IO_SUCCESS, libx52io_button_from_str(s, &out)); + assert_int_equal(b, out); + assert_ptr_equal(s, libx52io_button_to_str(out)); + } +} + +static void test_axis_from_str_invalid(void **state) +{ + libx52io_axis out; + + (void)state; + + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_axis_from_str("", &out)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_axis_from_str("ABS_", &out)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_axis_from_str("ABS_XX", &out)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_axis_from_str("abs_x", &out)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_axis_from_str("BTN_FIRE", &out)); +} + +static void test_button_from_str_invalid(void **state) +{ + libx52io_button out; + + (void)state; + + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_button_from_str("", &out)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_button_from_str("BTN_", &out)); + assert_int_equal(LIBX52IO_ERROR_INVALID, + libx52io_button_from_str("BTN_FIRE_EXTRA", &out)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_button_from_str("btn_fire", &out)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_button_from_str("ABS_X", &out)); +} + +static void test_from_str_null_args(void **state) +{ + libx52io_axis ax; + libx52io_button btn; + + (void)state; + + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_axis_from_str(NULL, &ax)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_axis_from_str("ABS_X", NULL)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_button_from_str(NULL, &btn)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_button_from_str("BTN_A", NULL)); +} + +static void test_axis_from_str_nocase(void **state) +{ + libx52io_axis a; + libx52io_axis out; + char buf[64]; + + (void)state; + + for (a = LIBX52IO_AXIS_X; a < LIBX52IO_AXIS_MAX; a++) { + const char *s = libx52io_axis_to_str(a); + + assert_non_null(s); + lower_ascii_only(buf, s); + assert_int_equal(LIBX52IO_SUCCESS, libx52io_axis_from_str_nocase(buf, &out)); + assert_int_equal(a, out); + } + + assert_int_equal(LIBX52IO_SUCCESS, + libx52io_axis_from_str_nocase("aBs_hAtX", &out)); + assert_int_equal(LIBX52IO_AXIS_HATX, out); +} + +static void test_button_from_str_nocase(void **state) +{ + libx52io_button b; + libx52io_button out; + char buf[64]; + + (void)state; + + for (b = LIBX52IO_BTN_TRIGGER; b < LIBX52IO_BUTTON_MAX; b++) { + const char *s = libx52io_button_to_str(b); + + assert_non_null(s); + lower_ascii_only(buf, s); + assert_int_equal(LIBX52IO_SUCCESS, libx52io_button_from_str_nocase(buf, &out)); + assert_int_equal(b, out); + } + + assert_int_equal(LIBX52IO_SUCCESS, + libx52io_button_from_str_nocase("BtN_t1_dN", &out)); + assert_int_equal(LIBX52IO_BTN_T1_DN, out); +} + +static void test_from_str_nocase_invalid(void **state) +{ + libx52io_axis ax; + libx52io_button btn; + + (void)state; + + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_axis_from_str_nocase("", &ax)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_axis_from_str_nocase("abs_xx", &ax)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_button_from_str_nocase("", &btn)); + assert_int_equal(LIBX52IO_ERROR_INVALID, + libx52io_button_from_str_nocase("btn_fire_extra", &btn)); +} + +static void test_from_str_nocase_null_args(void **state) +{ + libx52io_axis ax; + libx52io_button btn; + + (void)state; + + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_axis_from_str_nocase(NULL, &ax)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_axis_from_str_nocase("abs_x", NULL)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_button_from_str_nocase(NULL, &btn)); + assert_int_equal(LIBX52IO_ERROR_INVALID, libx52io_button_from_str_nocase("btn_a", NULL)); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_axis_round_trip), + cmocka_unit_test(test_button_round_trip), + cmocka_unit_test(test_axis_from_str_invalid), + cmocka_unit_test(test_button_from_str_invalid), + cmocka_unit_test(test_from_str_null_args), + cmocka_unit_test(test_axis_from_str_nocase), + cmocka_unit_test(test_button_from_str_nocase), + cmocka_unit_test(test_from_str_nocase_invalid), + cmocka_unit_test(test_from_str_nocase_null_args), + }; + + cmocka_set_message_output(CM_OUTPUT_TAP); + cmocka_run_group_tests(tests, NULL, NULL); + return 0; +} diff --git a/po/libx52.pot b/po/libx52.pot index 8938a5c..dc2a67d 100644 --- a/po/libx52.pot +++ b/po/libx52.pot @@ -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-02 23:18-0700\n" +"POT-Creation-Date: 2026-04-02 23:32-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,11 +17,11 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: libx52/x52_strerror.c:23 libx52io/io_strings.c:101 vkm/vkm_common.c:25 +#: libx52/x52_strerror.c:23 libx52io/io_strings.c:196 vkm/vkm_common.c:25 msgid "Success" msgstr "" -#: libx52/x52_strerror.c:24 libx52io/io_strings.c:102 +#: libx52/x52_strerror.c:24 libx52io/io_strings.c:197 msgid "Initialization failure" msgstr "" @@ -85,7 +85,7 @@ msgstr "" msgid "System call interrupted" msgstr "" -#: libx52/x52_strerror.c:66 libx52io/io_strings.c:125 vkm/vkm_common.c:52 +#: libx52/x52_strerror.c:66 libx52io/io_strings.c:220 vkm/vkm_common.c:52 #, c-format msgid "Unknown error %d" msgstr "" @@ -213,23 +213,23 @@ msgstr "" msgid "Unknown LED ID %d" msgstr "" -#: libx52io/io_strings.c:103 +#: libx52io/io_strings.c:198 msgid "No device" msgstr "" -#: libx52io/io_strings.c:104 +#: libx52io/io_strings.c:199 msgid "Invalid arguments" msgstr "" -#: libx52io/io_strings.c:105 +#: libx52io/io_strings.c:200 msgid "Connection failure" msgstr "" -#: libx52io/io_strings.c:106 +#: libx52io/io_strings.c:201 msgid "I/O error" msgstr "" -#: libx52io/io_strings.c:107 +#: libx52io/io_strings.c:202 msgid "Read timeout" msgstr "" diff --git a/po/xx_PL.po b/po/xx_PL.po index 2258907..707d97b 100644 --- a/po/xx_PL.po +++ b/po/xx_PL.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: libx52 0.2.3\n" "Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n" -"POT-Creation-Date: 2026-04-02 23:18-0700\n" +"POT-Creation-Date: 2026-04-02 23:32-0700\n" "PO-Revision-Date: 2026-04-01 20:50-0700\n" "Last-Translator: Nirenjan Krishnan \n" "Language-Team: Dummy Language for testing i18n\n" @@ -17,11 +17,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 3.4.2\n" -#: libx52/x52_strerror.c:23 libx52io/io_strings.c:101 vkm/vkm_common.c:25 +#: libx52/x52_strerror.c:23 libx52io/io_strings.c:196 vkm/vkm_common.c:25 msgid "Success" msgstr "Uccesssay" -#: libx52/x52_strerror.c:24 libx52io/io_strings.c:102 +#: libx52/x52_strerror.c:24 libx52io/io_strings.c:197 msgid "Initialization failure" msgstr "Initializationay ailurefay" @@ -85,7 +85,7 @@ msgstr "Ipepay erroray" msgid "System call interrupted" msgstr "Ystemsay allcay interrupteday" -#: libx52/x52_strerror.c:66 libx52io/io_strings.c:125 vkm/vkm_common.c:52 +#: libx52/x52_strerror.c:66 libx52io/io_strings.c:220 vkm/vkm_common.c:52 #, c-format msgid "Unknown error %d" msgstr "Unknownay erroray %d" @@ -213,23 +213,23 @@ msgstr "Ottlethray" msgid "Unknown LED ID %d" msgstr "Unknownay EDLay IDay %d" -#: libx52io/io_strings.c:103 +#: libx52io/io_strings.c:198 msgid "No device" msgstr "Onay eviceday" -#: libx52io/io_strings.c:104 +#: libx52io/io_strings.c:199 msgid "Invalid arguments" msgstr "Invaliday argumentsay" -#: libx52io/io_strings.c:105 +#: libx52io/io_strings.c:200 msgid "Connection failure" msgstr "Onnectioncay ailurefay" -#: libx52io/io_strings.c:106 +#: libx52io/io_strings.c:201 msgid "I/O error" msgstr "I/O erroray" -#: libx52io/io_strings.c:107 +#: libx52io/io_strings.c:202 msgid "Read timeout" msgstr "Eadray imeouttay"