Better from_chars implementation (Fix #2475)

This commit is contained in:
yhirose
2026-06-17 23:21:36 -04:00
parent df0b7d243b
commit 7307c41aa8
2 changed files with 165 additions and 9 deletions
+104
View File
@@ -638,6 +638,75 @@ TEST(TrimTests, TrimStringTests) {
EXPECT_TRUE(detail::trim_copy("").empty());
}
TEST(FromCharsTest, Double) {
// detail::from_chars(double) recognizes exactly the HTTP quality-value
// grammar (RFC 9110 12.4.2): a non-negative decimal "1*DIGIT [ '.' *DIGIT ]"
// with no sign, exponent, or "inf"/"nan", parsed locale-independently.
auto parse = [](const std::string &s, double &v) {
return detail::from_chars(s.data(), s.data() + s.size(), v);
};
double v = -1.0;
// Representative quality values.
EXPECT_EQ(parse("0", v).ec, std::errc{});
EXPECT_DOUBLE_EQ(v, 0.0);
EXPECT_EQ(parse("1", v).ec, std::errc{});
EXPECT_DOUBLE_EQ(v, 1.0);
EXPECT_EQ(parse("0.8", v).ec, std::errc{});
EXPECT_DOUBLE_EQ(v, 0.8);
EXPECT_EQ(parse("0.001", v).ec, std::errc{});
EXPECT_DOUBLE_EQ(v, 0.001);
EXPECT_EQ(parse("1.000", v).ec, std::errc{});
EXPECT_DOUBLE_EQ(v, 1.0);
// A missing integer or fractional part is tolerated.
EXPECT_EQ(parse(".5", v).ec, std::errc{});
EXPECT_DOUBLE_EQ(v, 0.5);
EXPECT_EQ(parse("5.", v).ec, std::errc{});
EXPECT_DOUBLE_EQ(v, 5.0);
// Values outside [0, 1] still parse; the caller range-checks them.
EXPECT_EQ(parse("123.456", v).ec, std::errc{});
EXPECT_DOUBLE_EQ(v, 123.456);
// Stops at the first byte that is not part of the number and reports where.
std::string trailing = "0.9, text/html";
auto r = parse(trailing, v);
EXPECT_EQ(r.ec, std::errc{});
EXPECT_DOUBLE_EQ(v, 0.9);
EXPECT_EQ(*r.ptr, ',');
// Sign and exponent are NOT part of the grammar: '+'/'-' are rejected
// outright, and an 'e'/'E' simply ends the number.
EXPECT_EQ(parse("-3.25", v).ec, std::errc::invalid_argument);
EXPECT_EQ(parse("+2.5", v).ec, std::errc::invalid_argument);
std::string exp_input = "1.5e3";
r = parse(exp_input, v);
EXPECT_EQ(r.ec, std::errc{});
EXPECT_DOUBLE_EQ(v, 1.5);
EXPECT_EQ(*r.ptr, 'e');
// Invalid inputs.
EXPECT_EQ(parse("", v).ec, std::errc::invalid_argument);
EXPECT_EQ(parse(".", v).ec, std::errc::invalid_argument);
EXPECT_EQ(parse("abc", v).ec, std::errc::invalid_argument);
EXPECT_EQ(parse("nan", v).ec, std::errc::invalid_argument);
EXPECT_EQ(parse("inf", v).ec, std::errc::invalid_argument);
// Pathological but well-formed inputs must stay bounded (no overflow, no
// out-of-bounds table access) and stay within [0, 1] for the caller.
EXPECT_EQ(parse(std::string("0.") + std::string(500, '0'), v).ec,
std::errc{});
EXPECT_DOUBLE_EQ(v, 0.0);
EXPECT_EQ(parse(std::string("0.") + std::string(500, '9'), v).ec,
std::errc{});
EXPECT_GE(v, 0.0);
EXPECT_LE(v, 1.0);
EXPECT_EQ(parse(std::string(500, '9'), v).ec, std::errc{});
EXPECT_GT(v, 1.0); // huge integer: finite, > 1, so the caller rejects it
}
TEST(ParseAcceptHeaderTest, BasicAcceptParsing) {
// Simple case without quality values
std::vector<std::string> result1;
@@ -745,6 +814,41 @@ TEST(ParseAcceptHeaderTest, SpecialCases) {
EXPECT_EQ(no_space_result[2], "text/plain");
}
TEST(ParseAcceptHeaderTest, QualityValueLocaleIndependence) {
// Quality values always use '.' as the decimal separator, so parsing must
// not depend on the process locale. An embedding application may have
// switched to a locale that uses ',' via setlocale(LC_ALL, "").
const char *cur = std::setlocale(LC_NUMERIC, nullptr);
std::string saved = cur ? cur : "C";
const char *comma_locales[] = {"de_DE.UTF-8", "de_DE.utf8", "nl_NL.UTF-8",
"fr_FR.UTF-8"};
bool switched = false;
for (const auto loc : comma_locales) {
if (std::setlocale(LC_NUMERIC, loc) != nullptr &&
std::localeconv()->decimal_point[0] == ',') {
switched = true;
break;
}
}
if (!switched) {
std::setlocale(LC_NUMERIC, saved.c_str());
GTEST_SKIP() << "no comma-decimal locale available on this host";
}
// The higher-weighted type appears later in the list, so a correct parse
// must reorder it ahead of the earlier, lower-weighted one. A locale-
// sensitive parse reads both weights as 0 and leaves the original order.
std::vector<std::string> result;
EXPECT_TRUE(detail::parse_accept_header(
"application/json;q=0.1,text/html;q=0.9", result));
ASSERT_EQ(result.size(), 2U);
EXPECT_EQ(result[0], "text/html");
EXPECT_EQ(result[1], "application/json");
std::setlocale(LC_NUMERIC, saved.c_str());
}
TEST(ParseAcceptHeaderTest, InvalidCases) {
std::vector<std::string> result;