Skip to content

Commit bd455f8

Browse files
committed
Add locale-independent ASCII classification helpers (Fix #2482, Fix #2483)
std::isalnum and std::isdigit consult the global C locale, so a byte like 0xC5 can classify as alphanumeric once an embedder calls setlocale() (observed on macOS). HTTP grammars are defined over ASCII, so raw bytes must be classified without regard to the locale. Add detail::is_ascii_digit/is_ascii_alpha/is_ascii_alnum and use them at every classification site: multipart boundary validation, token checks, URI encoding, range header parsing, is_numeric, is_hex, and IPv4 host detection. Also unify the hand-written digit range checks in from_chars, URL parsing, and parse_ipv4 onto the same helpers. With these in place nothing uses <cctype> anymore, so the include is dropped.
1 parent 9a5321a commit bd455f8

2 files changed

Lines changed: 64 additions & 28 deletions

File tree

httplib.h

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,6 @@ using socket_t = int;
309309
#include <array>
310310
#include <atomic>
311311
#include <cassert>
312-
#include <cctype>
313312
#include <chrono>
314313
#include <climits>
315314
#include <condition_variable>
@@ -540,6 +539,21 @@ make_unique(std::size_t n) {
540539
return std::unique_ptr<T>(new RT[n]);
541540
}
542541

542+
// Locale-independent ASCII character classification. The <cctype>
543+
// counterparts (std::isalnum, std::isdigit, ...) consult the global C locale,
544+
// so e.g. std::isalnum(0xC5) can return true once an embedder calls
545+
// setlocale(). HTTP grammars are defined over ASCII, so raw bytes must be
546+
// classified without regard to the locale.
547+
inline bool is_ascii_digit(char c) { return '0' <= c && c <= '9'; }
548+
549+
inline bool is_ascii_alpha(char c) {
550+
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z');
551+
}
552+
553+
inline bool is_ascii_alnum(char c) {
554+
return is_ascii_digit(c) || is_ascii_alpha(c);
555+
}
556+
543557
namespace case_ignore {
544558

545559
inline unsigned char to_lower(int c) {
@@ -661,7 +675,7 @@ inline from_chars_result<T> from_chars(const char *first, const char *last,
661675
for (; p != last; ++p) {
662676
char c = *p;
663677
int digit = -1;
664-
if ('0' <= c && c <= '9') {
678+
if (is_ascii_digit(c)) {
665679
digit = c - '0';
666680
} else if ('a' <= c && c <= 'z') {
667681
digit = c - 'a' + 10;
@@ -733,14 +747,14 @@ inline from_chars_result<double> from_chars(const char *first, const char *last,
733747
return false;
734748
};
735749

736-
for (; p != last && '0' <= *p && *p <= '9'; ++p) {
750+
for (; p != last && is_ascii_digit(*p); ++p) {
737751
seen_digit = true;
738752
accumulate(*p);
739753
}
740754

741755
if (p != last && *p == '.') {
742756
++p;
743-
for (; p != last && '0' <= *p && *p <= '9'; ++p) {
757+
for (; p != last && is_ascii_digit(*p); ++p) {
744758
seen_digit = true;
745759
if (frac_digits < max_frac_digits && accumulate(*p)) { ++frac_digits; }
746760
}
@@ -803,8 +817,8 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
803817
// IPv6 host must be [a-fA-F0-9:]+ only
804818
if (uc.host.empty()) { return false; }
805819
for (auto c : uc.host) {
806-
if (!((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') ||
807-
(c >= '0' && c <= '9') || c == ':')) {
820+
if (!(is_ascii_digit(c) || (c >= 'a' && c <= 'f') ||
821+
(c >= 'A' && c <= 'F') || c == ':')) {
808822
return false;
809823
}
810824
}
@@ -2929,9 +2943,7 @@ template <size_t N> inline constexpr size_t str_len(const char (&)[N]) {
29292943
}
29302944

29312945
inline bool is_numeric(const std::string &str) {
2932-
return !str.empty() &&
2933-
std::all_of(str.cbegin(), str.cend(),
2934-
[](unsigned char c) { return std::isdigit(c); });
2946+
return !str.empty() && std::all_of(str.cbegin(), str.cend(), is_ascii_digit);
29352947
}
29362948

29372949
inline size_t get_header_value_u64(const Headers &headers,
@@ -4480,7 +4492,7 @@ inline bool set_socket_opt_time(socket_t sock, int level, int optname,
44804492
}
44814493

44824494
inline bool is_hex(char c, int &v) {
4483-
if (isdigit(static_cast<unsigned char>(c))) {
4495+
if (is_ascii_digit(c)) {
44844496
v = c - '0';
44854497
return true;
44864498
} else if ('A' <= c && c <= 'F') {
@@ -7893,8 +7905,7 @@ inline bool parse_range_header(const std::string &s, Ranges &ranges) {
78937905
inline bool parse_range_header(const std::string &s, Ranges &ranges) try {
78947906
#endif
78957907
auto is_valid = [](const std::string &str) {
7896-
return std::all_of(str.cbegin(), str.cend(),
7897-
[](unsigned char c) { return std::isdigit(c); });
7908+
return std::all_of(str.cbegin(), str.cend(), is_ascii_digit);
78987909
};
78997910

79007911
if (s.size() > 7 && s.compare(0, 6, "bytes=") == 0) {
@@ -8342,7 +8353,7 @@ inline bool is_multipart_boundary_chars_valid(const std::string &boundary) {
83428353
auto valid = true;
83438354
for (size_t i = 0; i < boundary.size(); i++) {
83448355
auto c = boundary[i];
8345-
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '-' && c != '_') {
8356+
if (!is_ascii_alnum(c) && c != '-' && c != '_') {
83468357
valid = false;
83478358
break;
83488359
}
@@ -8856,10 +8867,9 @@ class ContentProviderAdapter {
88568867
namespace fields {
88578868

88588869
inline bool is_token_char(char c) {
8859-
return std::isalnum(static_cast<unsigned char>(c)) || c == '!' || c == '#' ||
8860-
c == '$' || c == '%' || c == '&' || c == '\'' || c == '*' ||
8861-
c == '+' || c == '-' || c == '.' || c == '^' || c == '_' || c == '`' ||
8862-
c == '|' || c == '~';
8870+
return is_ascii_alnum(c) || c == '!' || c == '#' || c == '$' || c == '%' ||
8871+
c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' ||
8872+
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
88638873
}
88648874

88658875
inline bool is_token(const std::string &s) {
@@ -9635,9 +9645,8 @@ inline std::string encode_uri_component(const std::string &value) {
96359645
escaped << std::hex;
96369646

96379647
for (auto c : value) {
9638-
if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||
9639-
c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' ||
9640-
c == ')') {
9648+
if (detail::is_ascii_alnum(c) || c == '-' || c == '_' || c == '.' ||
9649+
c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')') {
96419650
escaped << c;
96429651
} else {
96439652
escaped << std::uppercase;
@@ -9656,10 +9665,10 @@ inline std::string encode_uri(const std::string &value) {
96569665
escaped << std::hex;
96579666

96589667
for (auto c : value) {
9659-
if (std::isalnum(static_cast<uint8_t>(c)) || c == '-' || c == '_' ||
9660-
c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' ||
9661-
c == ')' || c == ';' || c == '/' || c == '?' || c == ':' || c == '@' ||
9662-
c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || c == '#') {
9668+
if (detail::is_ascii_alnum(c) || c == '-' || c == '_' || c == '.' ||
9669+
c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')' ||
9670+
c == ';' || c == '/' || c == '?' || c == ':' || c == '@' || c == '&' ||
9671+
c == '=' || c == '+' || c == '$' || c == ',' || c == '#') {
96639672
escaped << c;
96649673
} else {
96659674
escaped << std::uppercase;
@@ -9720,7 +9729,8 @@ inline std::string encode_path_component(const std::string &component) {
97209729
auto c = static_cast<unsigned char>(component[i]);
97219730

97229731
// Unreserved characters per RFC 3986: ALPHA / DIGIT / "-" / "." / "_" / "~"
9723-
if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
9732+
if (detail::is_ascii_alnum(static_cast<char>(c)) || c == '-' || c == '.' ||
9733+
c == '_' || c == '~') {
97249734
result += static_cast<char>(c);
97259735
}
97269736
// Path-safe sub-delimiters: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" /
@@ -9793,7 +9803,8 @@ inline std::string encode_query_component(const std::string &component,
97939803
auto c = static_cast<unsigned char>(component[i]);
97949804

97959805
// Unreserved characters per RFC 3986
9796-
if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
9806+
if (detail::is_ascii_alnum(static_cast<char>(c)) || c == '-' || c == '.' ||
9807+
c == '_' || c == '~') {
97979808
result += static_cast<char>(c);
97989809
}
97999810
// Space handling
@@ -16607,7 +16618,7 @@ inline bool is_ipv4_address(const std::string &str) {
1660716618
for (char c : str) {
1660816619
if (c == '.') {
1660916620
dots++;
16610-
} else if (!isdigit(static_cast<unsigned char>(c))) {
16621+
} else if (!detail::is_ascii_digit(c)) {
1661116622
return false;
1661216623
}
1661316624
}
@@ -16624,7 +16635,7 @@ inline bool parse_ipv4(const std::string &str, unsigned char *out) {
1662416635
}
1662516636
int val = 0;
1662616637
int digits = 0;
16627-
while (*p >= '0' && *p <= '9') {
16638+
while (detail::is_ascii_digit(*p)) {
1662816639
val = val * 10 + (*p - '0');
1662916640
if (val > 255) { return false; }
1663016641
p++;

test/test.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,31 @@ TEST(TrimTests, TrimStringTests) {
666666
EXPECT_TRUE(detail::trim_copy("").empty());
667667
}
668668

669+
TEST(AsciiTest, LocaleIndependentClassification) {
670+
// detail::is_ascii_digit/alpha/alnum replace the <cctype> classifiers,
671+
// which consult the global C locale: std::isalnum(0xC5) can return true
672+
// once an embedder calls setlocale() (observed on macOS). The is_ascii_*
673+
// helpers must classify every byte by the ASCII grammar alone.
674+
for (int i = 0; i < 256; i++) {
675+
auto c = static_cast<char>(i);
676+
auto is_digit = i >= '0' && i <= '9';
677+
auto is_upper = i >= 'A' && i <= 'Z';
678+
auto is_lower = i >= 'a' && i <= 'z';
679+
680+
EXPECT_EQ(is_digit, detail::is_ascii_digit(c)) << "byte " << i;
681+
EXPECT_EQ(is_upper || is_lower, detail::is_ascii_alpha(c)) << "byte " << i;
682+
EXPECT_EQ(is_digit || is_upper || is_lower, detail::is_ascii_alnum(c))
683+
<< "byte " << i;
684+
}
685+
686+
// Regression check for the reported byte: 0xC5 is not alphanumeric.
687+
EXPECT_FALSE(detail::is_ascii_alnum(static_cast<char>(0xC5)));
688+
689+
// Non-ASCII bytes must be percent-encoded, never passed through as
690+
// alphanumeric.
691+
EXPECT_EQ("%C5", httplib::encode_uri_component("\xC5"));
692+
}
693+
669694
TEST(FromCharsTest, Double) {
670695
// detail::from_chars(double) recognizes exactly the HTTP quality-value
671696
// grammar (RFC 9110 12.4.2): a non-negative decimal "1*DIGIT [ '.' *DIGIT ]"

0 commit comments

Comments
 (0)