Skip to content

Commit 1127961

Browse files
authored
fix: accept subnormal doubles when parsing (#1427) (#1695)
decodeDouble parsed numbers via `istringstream >> double`. For a subnormal value such as `3.2114e-312`, operator>> sets failbit (the result underflowed) even though it produced the correctly-rounded value. The failure path only special-cased overflow, so subnormals were rejected as "not a number" -- meaning a value jsoncpp had just serialized could fail to parse back. In the failure path, accept the value when it is a subnormal (std::fpclassify(value) == FP_SUBNORMAL). This keys off the value operator>> produces, which is the correctly-rounded subnormal on libstdc++, libc++, and MSVC, so it needs no errno/eof heuristics. It deliberately does not accept results that round to zero, so malformed numbers like "0e" / "0e+" (jsonchecker fail29/fail30) and other junk are still rejected. Applied to both Reader and OurReader. Adds CharReaderTest/parseSubnormal covering subnormals, a writer round-trip, and continued rejection of malformed numbers.
1 parent 43f3834 commit 1127961

2 files changed

Lines changed: 57 additions & 2 deletions

File tree

src/lib_json/json_reader.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,13 @@ bool Reader::decodeDouble(Token& token, Value& decoded) {
584584
value = std::numeric_limits<double>::infinity();
585585
else if (value == std::numeric_limits<double>::lowest())
586586
value = -std::numeric_limits<double>::infinity();
587-
else if (!std::isinf(value))
587+
// operator>> sets failbit for a subnormal result (underflow) even though
588+
// it produced the correctly-rounded value, which made such numbers fail to
589+
// parse back after jsoncpp serialized them. Keep a subnormal value instead
590+
// of rejecting it. See issue #1427. Other failures -- malformed numbers
591+
// like "0e" or "0e+", or non-numbers -- leave the value at zero/non-finite
592+
// and are still rejected.
593+
else if (!std::isinf(value) && std::fpclassify(value) != FP_SUBNORMAL)
588594
return addError(
589595
"'" + String(token.start_, token.end_) + "' is not a number.", token);
590596
}
@@ -1637,7 +1643,13 @@ bool OurReader::decodeDouble(Token& token, Value& decoded) {
16371643
value = std::numeric_limits<double>::infinity();
16381644
else if (value == std::numeric_limits<double>::lowest())
16391645
value = -std::numeric_limits<double>::infinity();
1640-
else if (!std::isinf(value))
1646+
// operator>> sets failbit for a subnormal result (underflow) even though
1647+
// it produced the correctly-rounded value, which made such numbers fail to
1648+
// parse back after jsoncpp serialized them. Keep a subnormal value instead
1649+
// of rejecting it. See issue #1427. Other failures -- malformed numbers
1650+
// like "0e" or "0e+", or non-numbers -- leave the value at zero/non-finite
1651+
// and are still rejected.
1652+
else if (!std::isinf(value) && std::fpclassify(value) != FP_SUBNORMAL)
16411653
return addError(
16421654
"'" + String(token.start_, token.end_) + "' is not a number.", token);
16431655
}

src/test_lib_json/main.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3241,6 +3241,49 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseNumber) {
32413241
}
32423242
}
32433243

3244+
JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseSubnormal) {
3245+
// Regression test for #1427: subnormal doubles make operator>> set failbit
3246+
// even though it produced the correctly-rounded value, so they used to fail
3247+
// to parse -- meaning a value jsoncpp had just serialized could fail to read
3248+
// back. They should now parse to that value.
3249+
Json::CharReaderBuilder b;
3250+
CharReaderPtr reader(b.newCharReader());
3251+
Json::String errs;
3252+
3253+
const struct {
3254+
const char* doc;
3255+
double expected;
3256+
} cases[] = {
3257+
{"[3.2114e-312]", 3.2114e-312}, // subnormal
3258+
{"[-1e-320]", -1e-320}, // negative subnormal
3259+
{"[4.9e-324]", 4.9e-324}, // smallest positive subnormal
3260+
};
3261+
for (const auto& c : cases) {
3262+
Json::Value root;
3263+
bool ok = reader->parse(c.doc, c.doc + std::strlen(c.doc), &root, &errs);
3264+
JSONTEST_ASSERT(ok);
3265+
JSONTEST_ASSERT(errs.empty());
3266+
JSONTEST_ASSERT_EQUAL(c.expected, root[0].asDouble());
3267+
}
3268+
3269+
// A subnormal also round-trips through the writer.
3270+
{
3271+
const Json::String doc = Json::writeString(Json::StreamWriterBuilder(),
3272+
Json::Value(3.2114e-312));
3273+
Json::Value root;
3274+
bool ok = reader->parse(doc.data(), doc.data() + doc.size(), &root, &errs);
3275+
JSONTEST_ASSERT(ok);
3276+
JSONTEST_ASSERT_EQUAL(3.2114e-312, root.asDouble());
3277+
}
3278+
3279+
// Malformed numbers and non-numbers are still rejected (the failure path
3280+
// accepts a subnormal value but nothing that parses to zero or junk).
3281+
for (const char* doc : {"[1abc]", "[0e]", "[0e+]"}) {
3282+
Json::Value root;
3283+
JSONTEST_ASSERT(!reader->parse(doc, doc + std::strlen(doc), &root, &errs));
3284+
}
3285+
}
3286+
32443287
JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseString) {
32453288
Json::CharReaderBuilder b;
32463289
CharReaderPtr reader(b.newCharReader());

0 commit comments

Comments
 (0)