feat(libx52io): Add _from_str and from_str_nocase APIs

This change add APIs to convert the string forms of the axis and button
names back to their corresponding enum identifiers. This is effectively
built such that a roundtrip of _to_str and _from_str will return the
same input. The _nocase variants handle case insensitive matching of the
names by folding of the ASCII alphabets A-Z and a-z only, so it doesn't
depend on localization.
profile-support
nirenjan 2026-04-02 23:42:29 -07:00
parent a3d9708d1e
commit d8cc745d2d
6 changed files with 368 additions and 19 deletions

View File

@ -8,6 +8,7 @@
#include "config.h" #include "config.h"
#include <stdio.h> #include <stdio.h>
#include <string.h>
#include "libx52io.h" #include "libx52io.h"
#include "gettext.h" #include "gettext.h"
@ -16,6 +17,28 @@
* on one to the enumeration definitions. * on one to the enumeration definitions.
*/ */
/* Locale-independent: only ASCII AZ fold with az; 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 */ /* String mapping for axis */
static const char * _x52io_axis_str[LIBX52IO_AXIS_MAX] = { static const char * _x52io_axis_str[LIBX52IO_AXIS_MAX] = {
[LIBX52IO_AXIS_X] = "ABS_X", [LIBX52IO_AXIS_X] = "ABS_X",
@ -92,6 +115,78 @@ const char * libx52io_button_to_str(libx52io_button button)
return NULL; 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 */ /* Error buffer used for building custom error strings */
static char error_buffer[256]; static char error_buffer[256];

View File

@ -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); 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. * @brief Get the vendor ID of the connected X52 device.
* *

View File

@ -1,4 +1,4 @@
libx52io_version = '1.0.0' libx52io_version = '1.1.0'
libx52io_files = files( libx52io_files = files(
'io_core.c', '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('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')

View File

@ -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 <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#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;
}

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: libx52 0.3.3\n" "Project-Id-Version: libx52 0.3.3\n"
"Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,11 +17,11 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n" "Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\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" msgid "Success"
msgstr "" msgstr ""
#: libx52/x52_strerror.c:24 libx52io/io_strings.c:102 #: libx52/x52_strerror.c:24 libx52io/io_strings.c:197
msgid "Initialization failure" msgid "Initialization failure"
msgstr "" msgstr ""
@ -85,7 +85,7 @@ msgstr ""
msgid "System call interrupted" msgid "System call interrupted"
msgstr "" 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 #, c-format
msgid "Unknown error %d" msgid "Unknown error %d"
msgstr "" msgstr ""
@ -213,23 +213,23 @@ msgstr ""
msgid "Unknown LED ID %d" msgid "Unknown LED ID %d"
msgstr "" msgstr ""
#: libx52io/io_strings.c:103 #: libx52io/io_strings.c:198
msgid "No device" msgid "No device"
msgstr "" msgstr ""
#: libx52io/io_strings.c:104 #: libx52io/io_strings.c:199
msgid "Invalid arguments" msgid "Invalid arguments"
msgstr "" msgstr ""
#: libx52io/io_strings.c:105 #: libx52io/io_strings.c:200
msgid "Connection failure" msgid "Connection failure"
msgstr "" msgstr ""
#: libx52io/io_strings.c:106 #: libx52io/io_strings.c:201
msgid "I/O error" msgid "I/O error"
msgstr "" msgstr ""
#: libx52io/io_strings.c:107 #: libx52io/io_strings.c:202
msgid "Read timeout" msgid "Read timeout"
msgstr "" msgstr ""

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: libx52 0.2.3\n" "Project-Id-Version: libx52 0.2.3\n"
"Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\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" "PO-Revision-Date: 2026-04-01 20:50-0700\n"
"Last-Translator: Nirenjan Krishnan <nirenjan@gmail.com>\n" "Last-Translator: Nirenjan Krishnan <nirenjan@gmail.com>\n"
"Language-Team: Dummy Language for testing i18n\n" "Language-Team: Dummy Language for testing i18n\n"
@ -17,11 +17,11 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.4.2\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" msgid "Success"
msgstr "Uccesssay" 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" msgid "Initialization failure"
msgstr "Initializationay ailurefay" msgstr "Initializationay ailurefay"
@ -85,7 +85,7 @@ msgstr "Ipepay erroray"
msgid "System call interrupted" msgid "System call interrupted"
msgstr "Ystemsay allcay interrupteday" 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 #, c-format
msgid "Unknown error %d" msgid "Unknown error %d"
msgstr "Unknownay erroray %d" msgstr "Unknownay erroray %d"
@ -213,23 +213,23 @@ msgstr "Ottlethray"
msgid "Unknown LED ID %d" msgid "Unknown LED ID %d"
msgstr "Unknownay EDLay IDay %d" msgstr "Unknownay EDLay IDay %d"
#: libx52io/io_strings.c:103 #: libx52io/io_strings.c:198
msgid "No device" msgid "No device"
msgstr "Onay eviceday" msgstr "Onay eviceday"
#: libx52io/io_strings.c:104 #: libx52io/io_strings.c:199
msgid "Invalid arguments" msgid "Invalid arguments"
msgstr "Invaliday argumentsay" msgstr "Invaliday argumentsay"
#: libx52io/io_strings.c:105 #: libx52io/io_strings.c:200
msgid "Connection failure" msgid "Connection failure"
msgstr "Onnectioncay ailurefay" msgstr "Onnectioncay ailurefay"
#: libx52io/io_strings.c:106 #: libx52io/io_strings.c:201
msgid "I/O error" msgid "I/O error"
msgstr "I/O erroray" msgstr "I/O erroray"
#: libx52io/io_strings.c:107 #: libx52io/io_strings.c:202
msgid "Read timeout" msgid "Read timeout"
msgstr "Eadray imeouttay" msgstr "Eadray imeouttay"