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; +}