Skip to content

Commit d6a5e5d

Browse files
committed
fix: accept subnormal and underflowing doubles when parsing (#1427)
decodeDouble parsed numbers via `istringstream >> double`. For a subnormal value such as `3.2114e-312`, operator>> sets failbit even though it consumed the whole token and produced the correctly-rounded value. The failure path only special-cased overflow (value == max/lowest), so subnormals were rejected as "not a number" -- meaning a value jsoncpp had just serialized could fail to parse back. In the failure path, distinguish a complete-but-out-of-range parse from genuine junk by whether the entire token was consumed (is.eof()): on a full parse keep the value operator>> produced (the correctly-rounded subnormal, zero on underflow, or infinity on overflow); only a partial parse (e.g. "1abc") is still rejected. eof() is used instead of errno because it is a fundamental streambuf signal that behaves consistently across libstdc++, libc++, and MSVC. Applied to both Reader and OurReader. Adds CharReaderTest/parseSubnormalAndUnderflow covering subnormals, a writer round-trip, underflow-to-zero, and continued rejection of junk.
1 parent 5f1f240 commit d6a5e5d

2 files changed

Lines changed: 55 additions & 2 deletions

File tree

src/lib_json/json_reader.cpp

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,12 @@ 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 value that overflows to infinity or
588+
// underflows to a subnormal/zero, even though it consumed the entire token
589+
// and produced the correctly-rounded result. Distinguish that from genuine
590+
// junk by whether the whole token was consumed: on a complete parse (eof)
591+
// keep the value, only reject a partial one. See issue #1427.
592+
else if (!is.eof())
588593
return addError(
589594
"'" + String(token.start_, token.end_) + "' is not a number.", token);
590595
}
@@ -1637,7 +1642,12 @@ bool OurReader::decodeDouble(Token& token, Value& decoded) {
16371642
value = std::numeric_limits<double>::infinity();
16381643
else if (value == std::numeric_limits<double>::lowest())
16391644
value = -std::numeric_limits<double>::infinity();
1640-
else if (!std::isinf(value))
1645+
// operator>> sets failbit for a value that overflows to infinity or
1646+
// underflows to a subnormal/zero, even though it consumed the entire token
1647+
// and produced the correctly-rounded result. Distinguish that from genuine
1648+
// junk by whether the whole token was consumed: on a complete parse (eof)
1649+
// keep the value, only reject a partial one. See issue #1427.
1650+
else if (!is.eof())
16411651
return addError(
16421652
"'" + String(token.start_, token.end_) + "' is not a number.", token);
16431653
}

src/test_lib_json/main.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3212,6 +3212,49 @@ JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseNumber) {
32123212
}
32133213
}
32143214

3215+
JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseSubnormalAndUnderflow) {
3216+
// Regression test for #1427: subnormal doubles make operator>> set failbit
3217+
// (errno == ERANGE) even though it produced the correctly-rounded value, so
3218+
// they used to fail to parse. They should now parse to that value.
3219+
Json::CharReaderBuilder b;
3220+
CharReaderPtr reader(b.newCharReader());
3221+
Json::String errs;
3222+
3223+
const struct {
3224+
const char* doc;
3225+
double expected;
3226+
} cases[] = {
3227+
{"[3.2114e-312]", 3.2114e-312}, // subnormal
3228+
{"[-1e-320]", -1e-320}, // negative subnormal
3229+
{"[4.9e-324]", 4.9e-324}, // smallest positive subnormal
3230+
{"[1e-400]", 0.0}, // underflow all the way to zero
3231+
};
3232+
for (const auto& c : cases) {
3233+
Json::Value root;
3234+
bool ok = reader->parse(c.doc, c.doc + std::strlen(c.doc), &root, &errs);
3235+
JSONTEST_ASSERT(ok);
3236+
JSONTEST_ASSERT(errs.empty());
3237+
JSONTEST_ASSERT_EQUAL(c.expected, root[0].asDouble());
3238+
}
3239+
3240+
// A subnormal also round-trips through the writer.
3241+
{
3242+
const Json::String doc = Json::writeString(Json::StreamWriterBuilder(),
3243+
Json::Value(3.2114e-312));
3244+
Json::Value root;
3245+
bool ok = reader->parse(doc.data(), doc.data() + doc.size(), &root, &errs);
3246+
JSONTEST_ASSERT(ok);
3247+
JSONTEST_ASSERT_EQUAL(3.2114e-312, root.asDouble());
3248+
}
3249+
3250+
// Genuine non-numbers are still rejected.
3251+
{
3252+
char const doc[] = "[1abc]";
3253+
Json::Value root;
3254+
JSONTEST_ASSERT(!reader->parse(doc, doc + std::strlen(doc), &root, &errs));
3255+
}
3256+
}
3257+
32153258
JSONTEST_FIXTURE_LOCAL(CharReaderTest, parseString) {
32163259
Json::CharReaderBuilder b;
32173260
CharReaderPtr reader(b.newCharReader());

0 commit comments

Comments
 (0)