mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-09-30 20:52:31 +07:00
Better from_chars implementation (Fix #2475)
This commit is contained in:
@@ -686,18 +686,70 @@ inline from_chars_result<T> from_chars(const char *first, const char *last,
|
|||||||
return {p, std::errc{}};
|
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<double> from_chars(const char *first, const char *last,
|
inline from_chars_result<double> from_chars(const char *first, const char *last,
|
||||||
double &value) {
|
double &value) {
|
||||||
std::string s(first, last);
|
value = 0.0;
|
||||||
char *endptr = nullptr;
|
const char *p = first;
|
||||||
errno = 0;
|
|
||||||
value = std::strtod(s.c_str(), &endptr);
|
// Each 1eN is exactly representable, so a single final division by the
|
||||||
if (endptr == s.c_str()) { return {first, std::errc::invalid_argument}; }
|
// matching entry yields a correctly-rounded result.
|
||||||
if (errno == ERANGE) {
|
static const double powers_of_ten[] = {
|
||||||
return {first + (endptr - s.c_str()), std::errc::result_out_of_range};
|
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<int>(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<uint64_t>::max)() - 9) / 10;
|
||||||
|
auto accumulate = [&](char c) {
|
||||||
|
if (mantissa <= limit) {
|
||||||
|
mantissa = mantissa * 10 + static_cast<uint64_t>(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<double>(mantissa) / powers_of_ten[frac_digits];
|
||||||
|
return {p, std::errc{}};
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool parse_port(const char *s, size_t len, int &port) {
|
inline bool parse_port(const char *s, size_t len, int &port) {
|
||||||
|
|||||||
+104
@@ -638,6 +638,75 @@ TEST(TrimTests, TrimStringTests) {
|
|||||||
EXPECT_TRUE(detail::trim_copy("").empty());
|
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) {
|
TEST(ParseAcceptHeaderTest, BasicAcceptParsing) {
|
||||||
// Simple case without quality values
|
// Simple case without quality values
|
||||||
std::vector<std::string> result1;
|
std::vector<std::string> result1;
|
||||||
@@ -745,6 +814,41 @@ TEST(ParseAcceptHeaderTest, SpecialCases) {
|
|||||||
EXPECT_EQ(no_space_result[2], "text/plain");
|
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) {
|
TEST(ParseAcceptHeaderTest, InvalidCases) {
|
||||||
std::vector<std::string> result;
|
std::vector<std::string> result;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user