diff --git a/httplib.h b/httplib.h index b7e0219..8676668 100644 --- a/httplib.h +++ b/httplib.h @@ -686,18 +686,70 @@ inline from_chars_result from_chars(const char *first, const char *last, return {p, std::errc{}}; } -// from_chars for double (simple wrapper for strtod) +// from_chars for double (hand-written, locale-independent) +// +// The only double consumed by this library is the HTTP quality value, whose +// grammar is (RFC 9110 12.4.2): +// qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ) +// i.e. a non-negative decimal with no sign, exponent, "inf"/"nan", or wide +// magnitude. So this parser recognizes exactly 1*DIGIT [ "." *DIGIT ] with +// '.' always the decimal separator (std::strtod would instead read it from the +// global C locale, mis-parsing q-values once an embedder calls +// setlocale(LC_ALL, "") into a comma-decimal locale). The caller range-checks +// the result to [0, 1], so inputs outside that range need not be distinguished +// here. Allocation-free, single pass, and free of the overflow/rounding edge +// cases that exponent and wide-range handling would introduce. inline from_chars_result from_chars(const char *first, const char *last, double &value) { - std::string s(first, last); - char *endptr = nullptr; - errno = 0; - value = std::strtod(s.c_str(), &endptr); - if (endptr == s.c_str()) { return {first, std::errc::invalid_argument}; } - if (errno == ERANGE) { - return {first + (endptr - s.c_str()), std::errc::result_out_of_range}; + value = 0.0; + const char *p = first; + + // Each 1eN is exactly representable, so a single final division by the + // matching entry yields a correctly-rounded result. + static const double powers_of_ten[] = { + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, + 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18}; + const int max_frac_digits = + static_cast(sizeof(powers_of_ten) / sizeof(powers_of_ten[0])) - 1; + + // Accumulate digits into a 64-bit integer and remember how many were + // fractional. Two independent caps keep this bounded and safe: + // * accumulation saturates before mantissa could overflow uint64_t, and + // * frac_digits is capped at max_frac_digits so it is always a valid index + // into powers_of_ten (without this an input like "0.000...0" would never + // grow mantissa, so the saturation cap alone would not bound it). + // Both caps only drop digits far beyond the precision a q-value needs; any + // value they would change is well outside [0, 1] and rejected by the caller. + uint64_t mantissa = 0; + int frac_digits = 0; + bool seen_digit = false; + + const uint64_t limit = ((std::numeric_limits::max)() - 9) / 10; + auto accumulate = [&](char c) { + if (mantissa <= limit) { + mantissa = mantissa * 10 + static_cast(c - '0'); + return true; + } + return false; + }; + + for (; p != last && '0' <= *p && *p <= '9'; ++p) { + seen_digit = true; + accumulate(*p); } - return {first + (endptr - s.c_str()), std::errc{}}; + + if (p != last && *p == '.') { + ++p; + for (; p != last && '0' <= *p && *p <= '9'; ++p) { + seen_digit = true; + if (frac_digits < max_frac_digits && accumulate(*p)) { ++frac_digits; } + } + } + + if (!seen_digit) { return {first, std::errc::invalid_argument}; } + + value = static_cast(mantissa) / powers_of_ten[frac_digits]; + return {p, std::errc{}}; } inline bool parse_port(const char *s, size_t len, int &port) { diff --git a/test/test.cc b/test/test.cc index d682e86..28e84b4 100644 --- a/test/test.cc +++ b/test/test.cc @@ -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 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 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 result;