Skip to content

Commit 7307c41

Browse files
committed
Better from_chars implementation (Fix #2475)
1 parent df0b7d2 commit 7307c41

2 files changed

Lines changed: 166 additions & 10 deletions

File tree

httplib.h

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -686,18 +686,70 @@ inline from_chars_result<T> from_chars(const char *first, const char *last,
686686
return {p, std::errc{}};
687687
}
688688

689-
// from_chars for double (simple wrapper for strtod)
689+
// from_chars for double (hand-written, locale-independent)
690+
//
691+
// The only double consumed by this library is the HTTP quality value, whose
692+
// grammar is (RFC 9110 12.4.2):
693+
// qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] )
694+
// i.e. a non-negative decimal with no sign, exponent, "inf"/"nan", or wide
695+
// magnitude. So this parser recognizes exactly 1*DIGIT [ "." *DIGIT ] with
696+
// '.' always the decimal separator (std::strtod would instead read it from the
697+
// global C locale, mis-parsing q-values once an embedder calls
698+
// setlocale(LC_ALL, "") into a comma-decimal locale). The caller range-checks
699+
// the result to [0, 1], so inputs outside that range need not be distinguished
700+
// here. Allocation-free, single pass, and free of the overflow/rounding edge
701+
// cases that exponent and wide-range handling would introduce.
690702
inline from_chars_result<double> from_chars(const char *first, const char *last,
691703
double &value) {
692-
std::string s(first, last);
693-
char *endptr = nullptr;
694-
errno = 0;
695-
value = std::strtod(s.c_str(), &endptr);
696-
if (endptr == s.c_str()) { return {first, std::errc::invalid_argument}; }
697-
if (errno == ERANGE) {
698-
return {first + (endptr - s.c_str()), std::errc::result_out_of_range};
699-
}
700-
return {first + (endptr - s.c_str()), std::errc{}};
704+
value = 0.0;
705+
const char *p = first;
706+
707+
// Each 1eN is exactly representable, so a single final division by the
708+
// matching entry yields a correctly-rounded result.
709+
static const double powers_of_ten[] = {
710+
1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9,
711+
1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18};
712+
const int max_frac_digits =
713+
static_cast<int>(sizeof(powers_of_ten) / sizeof(powers_of_ten[0])) - 1;
714+
715+
// Accumulate digits into a 64-bit integer and remember how many were
716+
// fractional. Two independent caps keep this bounded and safe:
717+
// * accumulation saturates before mantissa could overflow uint64_t, and
718+
// * frac_digits is capped at max_frac_digits so it is always a valid index
719+
// into powers_of_ten (without this an input like "0.000...0" would never
720+
// grow mantissa, so the saturation cap alone would not bound it).
721+
// Both caps only drop digits far beyond the precision a q-value needs; any
722+
// value they would change is well outside [0, 1] and rejected by the caller.
723+
uint64_t mantissa = 0;
724+
int frac_digits = 0;
725+
bool seen_digit = false;
726+
727+
const uint64_t limit = ((std::numeric_limits<uint64_t>::max)() - 9) / 10;
728+
auto accumulate = [&](char c) {
729+
if (mantissa <= limit) {
730+
mantissa = mantissa * 10 + static_cast<uint64_t>(c - '0');
731+
return true;
732+
}
733+
return false;
734+
};
735+
736+
for (; p != last && '0' <= *p && *p <= '9'; ++p) {
737+
seen_digit = true;
738+
accumulate(*p);
739+
}
740+
741+
if (p != last && *p == '.') {
742+
++p;
743+
for (; p != last && '0' <= *p && *p <= '9'; ++p) {
744+
seen_digit = true;
745+
if (frac_digits < max_frac_digits && accumulate(*p)) { ++frac_digits; }
746+
}
747+
}
748+
749+
if (!seen_digit) { return {first, std::errc::invalid_argument}; }
750+
751+
value = static_cast<double>(mantissa) / powers_of_ten[frac_digits];
752+
return {p, std::errc{}};
701753
}
702754

703755
inline bool parse_port(const char *s, size_t len, int &port) {

test/test.cc

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,75 @@ TEST(TrimTests, TrimStringTests) {
638638
EXPECT_TRUE(detail::trim_copy("").empty());
639639
}
640640

641+
TEST(FromCharsTest, Double) {
642+
// detail::from_chars(double) recognizes exactly the HTTP quality-value
643+
// grammar (RFC 9110 12.4.2): a non-negative decimal "1*DIGIT [ '.' *DIGIT ]"
644+
// with no sign, exponent, or "inf"/"nan", parsed locale-independently.
645+
auto parse = [](const std::string &s, double &v) {
646+
return detail::from_chars(s.data(), s.data() + s.size(), v);
647+
};
648+
649+
double v = -1.0;
650+
651+
// Representative quality values.
652+
EXPECT_EQ(parse("0", v).ec, std::errc{});
653+
EXPECT_DOUBLE_EQ(v, 0.0);
654+
EXPECT_EQ(parse("1", v).ec, std::errc{});
655+
EXPECT_DOUBLE_EQ(v, 1.0);
656+
EXPECT_EQ(parse("0.8", v).ec, std::errc{});
657+
EXPECT_DOUBLE_EQ(v, 0.8);
658+
EXPECT_EQ(parse("0.001", v).ec, std::errc{});
659+
EXPECT_DOUBLE_EQ(v, 0.001);
660+
EXPECT_EQ(parse("1.000", v).ec, std::errc{});
661+
EXPECT_DOUBLE_EQ(v, 1.0);
662+
663+
// A missing integer or fractional part is tolerated.
664+
EXPECT_EQ(parse(".5", v).ec, std::errc{});
665+
EXPECT_DOUBLE_EQ(v, 0.5);
666+
EXPECT_EQ(parse("5.", v).ec, std::errc{});
667+
EXPECT_DOUBLE_EQ(v, 5.0);
668+
669+
// Values outside [0, 1] still parse; the caller range-checks them.
670+
EXPECT_EQ(parse("123.456", v).ec, std::errc{});
671+
EXPECT_DOUBLE_EQ(v, 123.456);
672+
673+
// Stops at the first byte that is not part of the number and reports where.
674+
std::string trailing = "0.9, text/html";
675+
auto r = parse(trailing, v);
676+
EXPECT_EQ(r.ec, std::errc{});
677+
EXPECT_DOUBLE_EQ(v, 0.9);
678+
EXPECT_EQ(*r.ptr, ',');
679+
680+
// Sign and exponent are NOT part of the grammar: '+'/'-' are rejected
681+
// outright, and an 'e'/'E' simply ends the number.
682+
EXPECT_EQ(parse("-3.25", v).ec, std::errc::invalid_argument);
683+
EXPECT_EQ(parse("+2.5", v).ec, std::errc::invalid_argument);
684+
std::string exp_input = "1.5e3";
685+
r = parse(exp_input, v);
686+
EXPECT_EQ(r.ec, std::errc{});
687+
EXPECT_DOUBLE_EQ(v, 1.5);
688+
EXPECT_EQ(*r.ptr, 'e');
689+
690+
// Invalid inputs.
691+
EXPECT_EQ(parse("", v).ec, std::errc::invalid_argument);
692+
EXPECT_EQ(parse(".", v).ec, std::errc::invalid_argument);
693+
EXPECT_EQ(parse("abc", v).ec, std::errc::invalid_argument);
694+
EXPECT_EQ(parse("nan", v).ec, std::errc::invalid_argument);
695+
EXPECT_EQ(parse("inf", v).ec, std::errc::invalid_argument);
696+
697+
// Pathological but well-formed inputs must stay bounded (no overflow, no
698+
// out-of-bounds table access) and stay within [0, 1] for the caller.
699+
EXPECT_EQ(parse(std::string("0.") + std::string(500, '0'), v).ec,
700+
std::errc{});
701+
EXPECT_DOUBLE_EQ(v, 0.0);
702+
EXPECT_EQ(parse(std::string("0.") + std::string(500, '9'), v).ec,
703+
std::errc{});
704+
EXPECT_GE(v, 0.0);
705+
EXPECT_LE(v, 1.0);
706+
EXPECT_EQ(parse(std::string(500, '9'), v).ec, std::errc{});
707+
EXPECT_GT(v, 1.0); // huge integer: finite, > 1, so the caller rejects it
708+
}
709+
641710
TEST(ParseAcceptHeaderTest, BasicAcceptParsing) {
642711
// Simple case without quality values
643712
std::vector<std::string> result1;
@@ -745,6 +814,41 @@ TEST(ParseAcceptHeaderTest, SpecialCases) {
745814
EXPECT_EQ(no_space_result[2], "text/plain");
746815
}
747816

817+
TEST(ParseAcceptHeaderTest, QualityValueLocaleIndependence) {
818+
// Quality values always use '.' as the decimal separator, so parsing must
819+
// not depend on the process locale. An embedding application may have
820+
// switched to a locale that uses ',' via setlocale(LC_ALL, "").
821+
const char *cur = std::setlocale(LC_NUMERIC, nullptr);
822+
std::string saved = cur ? cur : "C";
823+
824+
const char *comma_locales[] = {"de_DE.UTF-8", "de_DE.utf8", "nl_NL.UTF-8",
825+
"fr_FR.UTF-8"};
826+
bool switched = false;
827+
for (const auto loc : comma_locales) {
828+
if (std::setlocale(LC_NUMERIC, loc) != nullptr &&
829+
std::localeconv()->decimal_point[0] == ',') {
830+
switched = true;
831+
break;
832+
}
833+
}
834+
if (!switched) {
835+
std::setlocale(LC_NUMERIC, saved.c_str());
836+
GTEST_SKIP() << "no comma-decimal locale available on this host";
837+
}
838+
839+
// The higher-weighted type appears later in the list, so a correct parse
840+
// must reorder it ahead of the earlier, lower-weighted one. A locale-
841+
// sensitive parse reads both weights as 0 and leaves the original order.
842+
std::vector<std::string> result;
843+
EXPECT_TRUE(detail::parse_accept_header(
844+
"application/json;q=0.1,text/html;q=0.9", result));
845+
ASSERT_EQ(result.size(), 2U);
846+
EXPECT_EQ(result[0], "text/html");
847+
EXPECT_EQ(result[1], "application/json");
848+
849+
std::setlocale(LC_NUMERIC, saved.c_str());
850+
}
851+
748852
TEST(ParseAcceptHeaderTest, InvalidCases) {
749853
std::vector<std::string> result;
750854

0 commit comments

Comments
 (0)