From 55d49b433af4cb7fef4b08802e1bc589c9cfe435 Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Mon, 31 Aug 2026 16:00:20 +0200 Subject: [PATCH] Read back infinities and NaNs from text archives An infinity is written as "inf" and a NaN as "nan", because that is what the stream writes, and extraction of a floating point number then refuses both: it takes digits and little else, in libstdc++ and in the Microsoft library alike. So the library wrote text and XML archives which it could not read back. Recognize the two forms when reading, rather than change what is written, so that archives already in existence start loading instead of staying unreadable. Fixes #386. --- .../boost/archive/basic_text_iprimitive.hpp | 116 +++++++++++++++++- test/Jamfile.v2 | 1 + test/test_non_finite_floats.cpp | 101 +++++++++++++++ 3 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 test/test_non_finite_floats.cpp diff --git a/include/boost/archive/basic_text_iprimitive.hpp b/include/boost/archive/basic_text_iprimitive.hpp index 4b9f1a8c3..d1dd88565 100644 --- a/include/boost/archive/basic_text_iprimitive.hpp +++ b/include/boost/archive/basic_text_iprimitive.hpp @@ -24,8 +24,12 @@ // in such cases. So we can't use basic_ostream but rather // use two template parameters +#include +#include #include #include // size_t +#include +#include #include #if defined(BOOST_NO_STDC_NAMESPACE) @@ -38,6 +42,7 @@ namespace std{ #endif #include +#include #include #include @@ -83,9 +88,39 @@ class BOOST_SYMBOL_VISIBLE basic_text_iprimitive { > locale_saver; #endif + // Whether a value of T can be an infinity or a NaN, and so may reach + // us written as letters rather than as digits. template - void load(T & t) - { + struct has_non_finite { + typedef typename mpl::bool_< + std::numeric_limits::has_infinity + || std::numeric_limits::has_quiet_NaN + >::type type; + }; + + // Takes a leading sign, if there is one, and says whether it was a + // minus. + bool take_minus_sign(){ + typedef typename IStream::traits_type traits_type; + typedef typename IStream::char_type char_type; + + std::basic_streambuf * const sb = is.rdbuf(); + if(NULL == sb){ + return false; + } + is >> std::ws; + const typename traits_type::int_type c = sb->sgetc(); + if(traits_type::eq_int_type(c, traits_type::to_int_type(char_type('-'))) + || traits_type::eq_int_type(c, traits_type::to_int_type(char_type('+')))){ + return traits_type::eq_int_type( + sb->sbumpc(), traits_type::to_int_type(char_type('-')) + ); + } + return false; + } + + template + void load_impl(T & t, boost::mpl::bool_ &){ if(is >> t) return; boost::serialization::throw_exception( @@ -93,6 +128,83 @@ class BOOST_SYMBOL_VISIBLE basic_text_iprimitive { ); } + // An infinity is written as "inf" and a NaN as "nan", because that is + // what the stream writes, and the extraction of a floating point number + // then refuses both, so an archive the library wrote itself would not + // load. Read the letters here rather than change what is written, so + // that archives already in existence start loading (issue #386). + template + void load_impl(T & t, boost::mpl::bool_ &){ + typedef typename IStream::traits_type traits_type; + typedef typename IStream::char_type char_type; + + const bool negative = take_minus_sign(); + if(is >> t){ + if(negative){ + t = -t; + } + return; + } + is.clear(); + + // Only the letters are taken, and not everything up to the next + // space, because an XML archive ends a value with a tag. The + // terminator is left where it is for the same reason. + std::basic_streambuf * const sb = is.rdbuf(); + std::string token; + for(;;){ + const typename traits_type::int_type c = sb->sgetc(); + if(traits_type::eq_int_type(c, traits_type::eof())){ + break; + } + const char_type letter = traits_type::to_char_type(c); + if(! ((char_type('a') <= letter && letter <= char_type('z')) + || (char_type('A') <= letter && letter <= char_type('Z')))){ + break; + } + token += char(char(letter) | 0x20); // ASCII, so this lowers it + sb->sbumpc(); + } + // A NaN may carry a parenthesised payload, as in the "nan(ind)" the + // Microsoft library writes, which has to come away with it. + if("nan" == token + && traits_type::eq_int_type( + sb->sgetc(), traits_type::to_int_type(char_type('(')) + ) + ){ + while(! traits_type::eq_int_type( + sb->sbumpc(), traits_type::to_int_type(char_type(')')) + )){ + if(traits_type::eq_int_type(sb->sgetc(), traits_type::eof())){ + break; + } + } + } + + if(("inf" == token || "infinity" == token) + && std::numeric_limits::has_infinity){ + t = std::numeric_limits::infinity(); + } + else if("nan" == token && std::numeric_limits::has_quiet_NaN){ + t = std::numeric_limits::quiet_NaN(); + } + else{ + boost::serialization::throw_exception( + archive_exception(archive_exception::input_stream_error) + ); + } + if(negative){ + t = -t; + } + } + + template + void load(T & t) + { + typename has_non_finite::type tag; + load_impl(t, tag); + } + void load(char & t) { short int i; diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 02f55e681..ecac5989c 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -96,6 +96,7 @@ test-suite "serialization" : [ test-bsl-run_files test_non_default_ctor ] [ test-bsl-run_files test_non_default_ctor2 ] [ test-bsl-run_files test_null_ptr ] + [ test-bsl-run_files test_non_finite_floats ] [ test-bsl-run_files test_nvp : A ] [ test-bsl-run_files test_object ] [ test-bsl-run_files test_primitive ] diff --git a/test/test_non_finite_floats.cpp b/test/test_non_finite_floats.cpp new file mode 100644 index 000000000..1720801b4 --- /dev/null +++ b/test/test_non_finite_floats.cpp @@ -0,0 +1,101 @@ +/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8 +// test_non_finite_floats.cpp + +// Copyright 2026 Gennaro Prota. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +// See http://www.boost.org for updates, documentation, and revision history. + +// An infinity is written as "inf" and a NaN as "nan", because that is what +// the stream writes, and extraction of a floating point number then refuses +// both: it accepts digits and little else, in libstdc++ and in the Microsoft +// library alike. So the library used to write text archives which it could +// not read back. + +// Reported by nim65s in +// https://github.com/boostorg/serialization/issues/386. Thanks! + +#include +#include +#include +#include + +#include +#if defined(BOOST_NO_STDC_NAMESPACE) +namespace std{ + using ::remove; +} +#endif + +#include "test_tools.hpp" + +#include + +template +struct values { + T positive_infinity; + T negative_infinity; + T not_a_number; + T ordinary; + + values() : + positive_infinity(std::numeric_limits::infinity()), + negative_infinity(-std::numeric_limits::infinity()), + not_a_number(std::numeric_limits::quiet_NaN()), + ordinary(T(24.567)) + {} + + // Deliberately not the constructor above: this one is what the load + // fills in, and it must start from something finite so that a load which + // quietly does nothing cannot pass. + values(int) : + positive_infinity(0), + negative_infinity(0), + not_a_number(0), + ordinary(0) + {} + + template + void serialize(Archive & ar, const unsigned int /* version */){ + ar & boost::serialization::make_nvp("pos_inf", positive_infinity); + ar & boost::serialization::make_nvp("neg_inf", negative_infinity); + ar & boost::serialization::make_nvp("nan", not_a_number); + ar & boost::serialization::make_nvp("ordinary", ordinary); + } +}; + +template +void test_type(){ + const char * testfile = boost::archive::tmpnam(NULL); + BOOST_REQUIRE(NULL != testfile); + + const values written; + { + test_ostream os(testfile, TEST_STREAM_FLAGS); + test_oarchive oa(os, TEST_ARCHIVE_FLAGS); + oa << boost::serialization::make_nvp("values", written); + } + + values read(0); + { + test_istream is(testfile, TEST_STREAM_FLAGS); + test_iarchive ia(is, TEST_ARCHIVE_FLAGS); + ia >> boost::serialization::make_nvp("values", read); + } + + BOOST_CHECK(read.positive_infinity == std::numeric_limits::infinity()); + BOOST_CHECK(read.negative_infinity == -std::numeric_limits::infinity()); + // A NaN is equal to nothing, itself included, so that is the test. + BOOST_CHECK(read.not_a_number != read.not_a_number); + BOOST_CHECK(read.ordinary == written.ordinary); + + std::remove(testfile); +} + +int test_main(int /* argc */, char * /* argv */ []){ + test_type(); + test_type(); + return EXIT_SUCCESS; +}