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.
diff --git a/src/lib_json/json_reader.cpp b/src/lib_json/json_reader.cpp
index 39ebcc6..ce9ca1b 100644
--- a/src/lib_json/json_reader.cpp
+++ b/src/lib_json/json_reader.cpp
@@ -584,7 +584,13 @@
value = std::numeric_limits<double>::infinity();
else if (value == std::numeric_limits<double>::lowest())
value = -std::numeric_limits<double>::infinity();
- else if (!std::isinf(value))
+ // operator>> sets failbit for a subnormal result (underflow) even though
+ // it produced the correctly-rounded value, which made such numbers fail to
+ // parse back after jsoncpp serialized them. Keep a subnormal value instead
+ // of rejecting it. See issue #1427. Other failures -- malformed numbers
+ // like "0e" or "0e+", or non-numbers -- leave the value at zero/non-finite
+ // and are still rejected.
+ else if (!std::isinf(value) && std::fpclassify(value) != FP_SUBNORMAL)
return addError(
"'" + String(token.start_, token.end_) + "' is not a number.", token);
}
@@ -1637,7 +1643,13 @@
value = std::numeric_limits<double>::infinity();
else if (value == std::numeric_limits<double>::lowest())
value = -std::numeric_limits<double>::infinity();
- else if (!std::isinf(value))
+ // operator>> sets failbit for a subnormal result (underflow) even though
+ // it produced the correctly-rounded value, which made such numbers fail to
+ // parse back after jsoncpp serialized them. Keep a subnormal value instead
+ // of rejecting it. See issue #1427. Other failures -- malformed numbers
+ // like "0e" or "0e+", or non-numbers -- leave the value at zero/non-finite
+ // and are still rejected.
+ else if (!std::isinf(value) && std::fpclassify(value) != FP_SUBNORMAL)
return addError(
"'" + String(token.start_, token.end_) + "' is not a number.", token);
}
diff --git a/src/test_lib_json/main.cpp b/src/test_lib_json/main.cpp
index 6673867..495bbb5 100644
--- a/src/test_lib_json/main.cpp
+++ b/src/test_lib_json/main.cpp
@@ -3241,6 +3241,49 @@
}
}
+JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseSubnormal) {
+ // Regression test for #1427: subnormal doubles make operator>> set failbit
+ // even though it produced the correctly-rounded value, so they used to fail
+ // to parse -- meaning a value jsoncpp had just serialized could fail to read
+ // back. They should now parse to that value.
+ Json::CharReaderBuilder b;
+ CharReaderPtr reader(b.newCharReader());
+ Json::String errs;
+
+ const struct {
+ const char* doc;
+ double expected;
+ } cases[] = {
+ {"[3.2114e-312]", 3.2114e-312}, // subnormal
+ {"[-1e-320]", -1e-320}, // negative subnormal
+ {"[4.9e-324]", 4.9e-324}, // smallest positive subnormal
+ };
+ for (const auto& c : cases) {
+ Json::Value root;
+ bool ok = reader->parse(c.doc, c.doc + std::strlen(c.doc), &root, &errs);
+ JSONTEST_ASSERT(ok);
+ JSONTEST_ASSERT(errs.empty());
+ JSONTEST_ASSERT_EQUAL(c.expected, root[0].asDouble());
+ }
+
+ // A subnormal also round-trips through the writer.
+ {
+ const Json::String doc = Json::writeString(Json::StreamWriterBuilder(),
+ Json::Value(3.2114e-312));
+ Json::Value root;
+ bool ok = reader->parse(doc.data(), doc.data() + doc.size(), &root, &errs);
+ JSONTEST_ASSERT(ok);
+ JSONTEST_ASSERT_EQUAL(3.2114e-312, root.asDouble());
+ }
+
+ // Malformed numbers and non-numbers are still rejected (the failure path
+ // accepts a subnormal value but nothing that parses to zero or junk).
+ for (const char* doc : {"[1abc]", "[0e]", "[0e+]"}) {
+ Json::Value root;
+ JSONTEST_ASSERT(!reader->parse(doc, doc + std::strlen(doc), &root, &errs));
+ }
+}
+
JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseString) {
Json::CharReaderBuilder b;
CharReaderPtr reader(b.newCharReader());