From f2960a197e868b73fc0d93e8df35fbd1fc6e1d9b Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Tue, 21 Jul 2026 13:21:09 +0300 Subject: [PATCH] utils: add ascii ctype helpers Add some constexpr ctype helpers that avoid any possible UB (i.e., handling casting to unsigned), and explicitly avoid any locale-specifics. --- lib/utils/mu-utils.cc | 2 +- lib/utils/mu-utils.hh | 127 ++++++++++++++++++++++++++++++++++ lib/utils/tests/test-utils.cc | 41 +++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) diff --git a/lib/utils/mu-utils.cc b/lib/utils/mu-utils.cc index cc565255..ccc1197a 100644 --- a/lib/utils/mu-utils.cc +++ b/lib/utils/mu-utils.cc @@ -269,7 +269,7 @@ Mu::remove_ctrl(const std::string& str) result.reserve(str.length()); for (auto&& c : str) { - if (::iscntrl(c) || c == ' ') { + if (is_ascii_cntrl(c) || c == ' ') { if (prev != ' ') result += prev = ' '; } else diff --git a/lib/utils/mu-utils.hh b/lib/utils/mu-utils.hh index ba5f5530..33488dfb 100644 --- a/lib/utils/mu-utils.hh +++ b/lib/utils/mu-utils.hh @@ -166,6 +166,133 @@ std::tm mu_time(T t={}, bool use_utc=false) { return time_tm; } +/* + * Constexpr, locale-independent ASCII versions of some functions. + * + * Unlike their libc counterparts, these are well-defined for _any_ + * character value; no need for casting to unsigned char at the + * call-site (which the libc versions require to avoid UB for negative + * char values). + */ + +/** + * Is this an ASCII character, i.e., in [0, 0x7f]? Constexpr version + * of ::isascii. + * + * @param c some character + * + * @return true or false + */ +template +constexpr bool is_ascii(Char c) noexcept { + return static_cast>(c) < 0x80; +} + +/** + * Is this an ASCII control character? ASCII version of ::iscntrl. + * + * @param c some character + * + * @return true or false + */ +template +constexpr bool is_ascii_cntrl(Char c) noexcept { + const auto uc{static_cast>(c)}; + return uc < 0x20 || uc == 0x7f; +} + +/** + * Is this an ASCII alphabetic character, i.e., in [a-zA-Z]? + * ASCII version of ::isalpha. + * + * @param c some character + * + * @return true or false + */ +template +constexpr bool is_ascii_alpha(Char c) noexcept { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +/** + * Is this an ASCII digit, i.e., in [0-9]? ASCII version of ::isdigit. + * + * @param c some character + * + * @return true or false + */ +template +constexpr bool is_ascii_digit(Char c) noexcept { + return c >= '0' && c <= '9'; +} + +/** + * Is this an ASCII alphanumeric character, i.e., in [a-zA-Z0-9]? + * ASCII version of ::isalnum. + * + * @param c some character + * + * @return true or false + */ +template +constexpr bool is_ascii_alnum(Char c) noexcept { + return is_ascii_alpha(c) || is_ascii_digit(c); +} + +/** + * Is this an ASCII blank character, i.e., SPC or TAB? ASCII version + * of ::isblank. + * + * @param c some character + * + * @return true or false + */ +template +constexpr bool is_ascii_blank(Char c) noexcept { + return c == ' ' || c == '\t'; +} + +/** + * Is this an ASCII white-space character, i.e., SPC, TAB, LF, VT, FF + * or CR? ASCII version of ::isspace. + * + * @param c some character + * + * @return true or false + */ +template +constexpr bool is_ascii_space(Char c) noexcept { + return c == ' ' || (c >= '\t' && c <= '\r'); +} + +/** + * Is this an ASCII punctuation character, i.e., a printable character + * that is neither alphanumeric nor SPC? ASCII version of ::ispunct. + * + * @param c some character + * + * @return true or false + */ +template +constexpr bool is_ascii_punct(Char c) noexcept { + const auto uc{static_cast>(c)}; + return uc > 0x20 && uc < 0x7f && !is_ascii_alnum(c); +} + +/** + * Get the lower-case version of an ASCII character in [A-Z]; any + * other character is returned unchanged. ASCII version of ::tolower. + * + * @param c some character + * + * @return the lower-cased character + */ +template +constexpr Char to_ascii_lower(Char c) noexcept { + return (c >= 'A' && c <= 'Z') ? + static_cast(c + ('a' - 'A')) : c; +} + using StringVec = std::vector; /** diff --git a/lib/utils/tests/test-utils.cc b/lib/utils/tests/test-utils.cc index 2e5aa47d..70a659d1 100644 --- a/lib/utils/tests/test-utils.cc +++ b/lib/utils/tests/test-utils.cc @@ -335,6 +335,46 @@ test_summarize() "fortification by the Bais raja Sathna. "); } +static void +test_ascii_ctype() +{ + /* the ascii ctype functions are constexpr and work for any + * integral character type... */ + static_assert(is_ascii('m')); + static_assert(!is_ascii(static_cast(0x80))); + static_assert(is_ascii_cntrl('\n')); + static_assert(!is_ascii_cntrl(' ')); + static_assert(is_ascii_blank('\t')); + static_assert(!is_ascii_blank('\n')); + static_assert(to_ascii_lower('Q') == 'q'); + static_assert(to_ascii_lower(U'Q') == U'q'); + static_assert(to_ascii_lower('8') == '8'); + static_assert(to_ascii_lower('\xc4'/* Ä */) == '\xc4'); + + /* ... and, unlike their libc cousins, are well-defined for + * negative chars; check the full range against glib's + * (locale-independent) g_ascii_* versions. */ + for (int i = -128; i != 128; ++i) { + const auto c{static_cast(i)}; + g_assert_cmpint(is_ascii_cntrl(c), ==, !!g_ascii_iscntrl(c)); + g_assert_cmpint(is_ascii_alpha(c), ==, !!g_ascii_isalpha(c)); + g_assert_cmpint(is_ascii_digit(c), ==, !!g_ascii_isdigit(c)); + g_assert_cmpint(is_ascii_alnum(c), ==, !!g_ascii_isalnum(c)); + /* NB: unlike isspace(3), g_ascii_isspace excludes VT */ + g_assert_cmpint(is_ascii_space(c), ==, + !!g_ascii_isspace(c) || c == '\v'); + g_assert_cmpint(is_ascii_punct(c), ==, !!g_ascii_ispunct(c)); + g_assert_cmpint(to_ascii_lower(c), ==, g_ascii_tolower(c)); + } + + /* values beyond the char range must not get truncated */ + const gunichar uc{0x2028/* line separator */}; + g_assert_false(is_ascii(uc)); + g_assert_false(is_ascii_cntrl(uc)); + g_assert_false(is_ascii_punct(uc)); + g_assert_true(to_ascii_lower(uc) == uc); +} + int main(int argc, char* argv[]) { @@ -355,6 +395,7 @@ main(int argc, char* argv[]) g_test_add_func("/utils/define-bitmap", test_define_bitmap); g_test_add_func("/utils/to-from-lexnum", test_to_from_lexnum); g_test_add_func("/utils/locale-workaround", test_locale_workaround); + g_test_add_func("/utils/ascii-ctype", test_ascii_ctype); return g_test_run(); }