Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 168 additions & 2 deletions include/boost/archive/basic_text_iprimitive.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@
// in such cases. So we can't use basic_ostream<IStream::char_type> but rather
// use two template parameters

#include <ios>
#include <limits>
#include <locale>
#include <string>
#include <cstddef> // size_t
#include <streambuf>
#include <string>

#include <boost/config.hpp>
#if defined(BOOST_NO_STDC_NAMESPACE)
Expand All @@ -39,6 +43,7 @@ namespace std{
#endif

#include <boost/io/ios_state.hpp>
#include <boost/mpl/bool.hpp>
#include <boost/static_assert.hpp>

#include <boost/detail/workaround.hpp>
Expand Down Expand Up @@ -85,9 +90,103 @@ 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<class T>
void load(T & t)
{
struct has_non_finite {
typedef typename mpl::bool_<
std::numeric_limits<T>::has_infinity
|| std::numeric_limits<T>::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<char_type, traits_type> * 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;
}

// 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. Lowered as it goes, since it is
// ASCII either way.
std::string take_letters(){
typedef typename IStream::traits_type traits_type;
typedef typename IStream::char_type char_type;

std::basic_streambuf<char_type, traits_type> * 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);
sb->sbumpc();
}
return token;
}

// Before 2015, the Microsoft library wrote an infinity as "1.#INF", a NaN
// as "1.#QNAN" and an indeterminate as "-1.#IND". The extraction takes
// the leading "1." for the value and stops at the '#', so an archive
// written then loads as 1 and the rest is left to derail the next read.
// Nothing else puts a '#' after a number, so seeing one settles it.
template<class T>
void take_legacy_non_finite(T & t){
typedef typename IStream::traits_type traits_type;
typedef typename IStream::char_type char_type;

std::basic_streambuf<char_type, traits_type> * const sb = is.rdbuf();
if(NULL == sb
|| ! traits_type::eq_int_type(
sb->sgetc(), traits_type::to_int_type(char_type('#'))
)){
return;
}
sb->sbumpc();
const std::string token = take_letters();
if("inf" == token && std::numeric_limits<T>::has_infinity){
t = std::numeric_limits<T>::infinity();
}
else if(("qnan" == token || "snan" == token || "ind" == token)
&& std::numeric_limits<T>::has_quiet_NaN){
t = std::numeric_limits<T>::quiet_NaN();
}
else{
const std::string message =
detail::stream_position_message(is.rdbuf());
boost::serialization::throw_exception(
archive_exception(
archive_exception::input_stream_error,
message.c_str()
)
);
}
}

template<class T>
void load_impl(T & t, boost::mpl::bool_<false> &){
if(is >> t)
return;
const std::string message =
Expand All @@ -100,6 +199,73 @@ 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<class T>
void load_impl(T & t, boost::mpl::bool_<true> &){
typedef typename IStream::traits_type traits_type;
typedef typename IStream::char_type char_type;

const bool negative = take_minus_sign();
if(is >> t){
take_legacy_non_finite(t);
if(negative){
t = -t;
}
return;
}
is.clear();

std::basic_streambuf<char_type, traits_type> * const sb = is.rdbuf();
const std::string token = take_letters();
// 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<T>::has_infinity){
t = std::numeric_limits<T>::infinity();
}
else if("nan" == token && std::numeric_limits<T>::has_quiet_NaN){
t = std::numeric_limits<T>::quiet_NaN();
}
else{
const std::string message =
detail::stream_position_message(is.rdbuf());
boost::serialization::throw_exception(
archive_exception(
archive_exception::input_stream_error,
message.c_str()
)
);
}
if(negative){
t = -t;
}
}

template<class T>
void load(T & t)
{
typename has_non_finite<T>::type tag;
load_impl(t, tag);
}

void load(char & t)
{
short int i;
Expand Down
29 changes: 29 additions & 0 deletions include/boost/archive/basic_text_oprimitive.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
// use two template parameters

#include <iomanip>
#include <limits>
#include <locale>
#include <cstddef> // size_t

Expand Down Expand Up @@ -151,6 +152,31 @@ class BOOST_SYMBOL_VISIBLE basic_text_oprimitive
>::type type;
};

// An infinity and a NaN go out in a spelling the library fixes itself,
// rather than in whatever the stream would produce. The latter would
// differ between implementations; e.g. the Microsoft library wrote
// "1.#INF" and "1.#QNAN" before 2015. The read side accepts the older
// spellings too, so nothing written before this stops loading.
template<class T>
bool save_non_finite(const T & t){
if(std::numeric_limits<T>::has_infinity){
if(t == std::numeric_limits<T>::infinity()){
put("inf");
return true;
}
if(t == -std::numeric_limits<T>::infinity()){
put("-inf");
return true;
}
}
// Nothing equals a NaN, itself included.
if(std::numeric_limits<T>::has_quiet_NaN && t != t){
put("nan");
return true;
}
return false;
}

template<class T>
void save_impl(const T &t, boost::mpl::bool_<true> &){
// must be a user mistake - can't serialize un-initialized data
Expand All @@ -159,6 +185,9 @@ class BOOST_SYMBOL_VISIBLE basic_text_oprimitive
archive_exception(archive_exception::output_stream_error)
);
}
if(save_non_finite(t)){
return;
}
// The formulae for the number of decimal digits required is given in
// http://www2.open-std.org/JTC1/SC22/WG21/docs/papers/2005/n1822.pdf
// which is derived from Kahan's paper:
Expand Down
2 changes: 2 additions & 0 deletions test/Jamfile.v2
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
Expand Down Expand Up @@ -161,6 +162,7 @@ if ! $(BOOST_ARCHIVE_LIST) {
[ test-bsl-run test_duplicate_type_registration ]
[ test-bsl-run test_strong_typedef_move ]
[ test-bsl-run test_stream_error_position ]
[ test-bsl-run test_non_finite_format ]
[ test-bsl-run test_private_ctor ]
[ test-bsl-run test_reset_object_address : A ]
[ test-bsl-run test_void_cast ]
Expand Down
101 changes: 101 additions & 0 deletions test/test_non_finite_floats.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <cstdio>
#include <fstream>
#include <limits>

#include <boost/config.hpp>
#if defined(BOOST_NO_STDC_NAMESPACE)
namespace std{
using ::remove;
}
#endif

#include "test_tools.hpp"

#include <boost/serialization/nvp.hpp>

template<class T>
struct values {
T positive_infinity;
T negative_infinity;
T not_a_number;
T ordinary;

values() :
positive_infinity(std::numeric_limits<T>::infinity()),
negative_infinity(-std::numeric_limits<T>::infinity()),
not_a_number(std::numeric_limits<T>::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<class Archive>
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<class T>
void test_type(){
const char * testfile = boost::archive::tmpnam(NULL);
BOOST_REQUIRE(NULL != testfile);

const values<T> written;
{
test_ostream os(testfile, TEST_STREAM_FLAGS);
test_oarchive oa(os, TEST_ARCHIVE_FLAGS);
oa << boost::serialization::make_nvp("values", written);
}

values<T> 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<T>::infinity());
BOOST_CHECK(read.negative_infinity == -std::numeric_limits<T>::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<float>();
test_type<double>();
return EXIT_SUCCESS;
}
Loading
Loading