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());