Skip to content

Commit 9778f45

Browse files
committed
Read back infinities and NaNs from text archives
An infinity or a NaN were written in the form produced by the standard library (e.g. "inf" or "nan"), but extraction of a floating point number then refused both: it took 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. Fix that by serializing with special tags, so that we don't depend on what the standard library writes, and reading back both the tags and the formats produced by the various standard libraries (so old archives read correctly). Fixes #386.
1 parent b159f7a commit 9778f45

5 files changed

Lines changed: 431 additions & 4 deletions

File tree

‎include/boost/archive/basic_text_iprimitive.hpp‎

Lines changed: 174 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
// basic_text_iprimitive.hpp
1111

1212
// (C) Copyright 2002 Robert Ramey - http://www.rrsd.com .
13+
// Copyright 2026 Gennaro Prota.
1314
// Distributed under the Boost Software License, Version 1.0.
1415
// (See accompanying file LICENSE_1_0.txt or copy at
1516
// http://www.boost.org/LICENSE_1_0.txt)
@@ -24,9 +25,13 @@
2425
// in such cases. So we can't use basic_ostream<IStream::char_type> but rather
2526
// use two template parameters
2627

28+
#include <ios>
29+
#include <limits>
2730
#include <locale>
2831
#include <string>
2932
#include <cstddef> // size_t
33+
#include <streambuf>
34+
#include <string>
3035

3136
#include <boost/config.hpp>
3237
#if defined(BOOST_NO_STDC_NAMESPACE)
@@ -39,6 +44,7 @@ namespace std{
3944
#endif
4045

4146
#include <boost/io/ios_state.hpp>
47+
#include <boost/mpl/bool.hpp>
4248
#include <boost/static_assert.hpp>
4349

4450
#include <boost/detail/workaround.hpp>
@@ -85,11 +91,17 @@ class BOOST_SYMBOL_VISIBLE basic_text_iprimitive {
8591
> locale_saver;
8692
#endif
8793

94+
// Whether a value of T can be an infinity or a NaN, and so may reach
95+
// us written as letters rather than as digits.
8896
template<class T>
89-
void load(T & t)
90-
{
91-
if(is >> t)
92-
return;
97+
struct has_non_finite {
98+
typedef typename mpl::bool_<
99+
std::numeric_limits<T>::has_infinity
100+
|| std::numeric_limits<T>::has_quiet_NaN
101+
>::type type;
102+
};
103+
104+
void throw_input_stream_error(){
93105
const std::string message =
94106
detail::stream_position_message(is.rdbuf());
95107
boost::serialization::throw_exception(
@@ -100,6 +112,164 @@ class BOOST_SYMBOL_VISIBLE basic_text_iprimitive {
100112
);
101113
}
102114

115+
// Takes a leading sign, if there is one, and says whether it was a
116+
// minus.
117+
bool take_minus_sign(){
118+
typedef typename IStream::traits_type traits_type;
119+
typedef typename IStream::char_type char_type;
120+
121+
std::basic_streambuf<char_type, traits_type> * const sb = is.rdbuf();
122+
if(NULL == sb){
123+
return false;
124+
}
125+
is >> std::ws;
126+
const typename traits_type::int_type c = sb->sgetc();
127+
if(traits_type::eq_int_type(c, traits_type::to_int_type(char_type('-')))
128+
|| traits_type::eq_int_type(c, traits_type::to_int_type(char_type('+')))){
129+
return traits_type::eq_int_type(
130+
sb->sbumpc(), traits_type::to_int_type(char_type('-'))
131+
);
132+
}
133+
return false;
134+
}
135+
136+
// Only the letters are taken, and not everything up to the next space,
137+
// because an XML archive ends a value with a tag. The terminator is
138+
// left where it is for the same reason. Lowered as it goes, since it is
139+
// ASCII either way.
140+
std::string take_letters(){
141+
typedef typename IStream::traits_type traits_type;
142+
typedef typename IStream::char_type char_type;
143+
144+
std::basic_streambuf<char_type, traits_type> * const sb = is.rdbuf();
145+
std::string token;
146+
if(NULL == sb){
147+
return token;
148+
}
149+
for(;;){
150+
const typename traits_type::int_type c = sb->sgetc();
151+
if(traits_type::eq_int_type(c, traits_type::eof())){
152+
break;
153+
}
154+
const char_type letter = traits_type::to_char_type(c);
155+
if(! ((char_type('a') <= letter && letter <= char_type('z'))
156+
|| (char_type('A') <= letter && letter <= char_type('Z')))){
157+
break;
158+
}
159+
token += char(char(letter) | 0x20);
160+
sb->sbumpc();
161+
}
162+
return token;
163+
}
164+
165+
// A NaN may carry a parenthesised payload, as in the "nan(ind)" the
166+
// Microsoft library writes, which has to come away with it.
167+
void take_nan_payload(){
168+
typedef typename IStream::traits_type traits_type;
169+
typedef typename IStream::char_type char_type;
170+
171+
std::basic_streambuf<char_type, traits_type> * const sb = is.rdbuf();
172+
if(NULL == sb
173+
|| ! traits_type::eq_int_type(
174+
sb->sgetc(), traits_type::to_int_type(char_type('('))
175+
)){
176+
return;
177+
}
178+
for(;;){
179+
const typename traits_type::int_type c = sb->sbumpc();
180+
if(traits_type::eq_int_type(c, traits_type::eof())
181+
|| traits_type::eq_int_type(
182+
c, traits_type::to_int_type(char_type(')'))
183+
)){
184+
break;
185+
}
186+
}
187+
}
188+
189+
template<class T>
190+
void take_non_finite(T & t, const std::string & token){
191+
if(("inf" == token || "infinity" == token)
192+
&& std::numeric_limits<T>::has_infinity){
193+
t = std::numeric_limits<T>::infinity();
194+
return;
195+
}
196+
if("nan" == token && std::numeric_limits<T>::has_quiet_NaN){
197+
take_nan_payload();
198+
t = std::numeric_limits<T>::quiet_NaN();
199+
return;
200+
}
201+
throw_input_stream_error();
202+
}
203+
204+
// Before 2015, the Microsoft library wrote an infinity as "1.#INF", a NaN
205+
// as "1.#QNAN" and an indeterminate as "-1.#IND". The extraction takes
206+
// the leading "1." for the value and stops at the '#', so an archive
207+
// written then loads as 1 and the rest is left to derail the next read.
208+
// Nothing else puts a '#' after a number, so seeing one settles it.
209+
template<class T>
210+
void take_legacy_non_finite(T & t){
211+
typedef typename IStream::traits_type traits_type;
212+
typedef typename IStream::char_type char_type;
213+
214+
std::basic_streambuf<char_type, traits_type> * const sb = is.rdbuf();
215+
if(NULL == sb
216+
|| ! traits_type::eq_int_type(
217+
sb->sgetc(), traits_type::to_int_type(char_type('#'))
218+
)){
219+
return;
220+
}
221+
sb->sbumpc();
222+
const std::string token = take_letters();
223+
if("inf" == token && std::numeric_limits<T>::has_infinity){
224+
t = std::numeric_limits<T>::infinity();
225+
}
226+
else if(("qnan" == token || "snan" == token || "ind" == token)
227+
&& std::numeric_limits<T>::has_quiet_NaN){
228+
t = std::numeric_limits<T>::quiet_NaN();
229+
}
230+
else{
231+
throw_input_stream_error();
232+
}
233+
}
234+
235+
template<class T>
236+
void load_impl(T & t, boost::mpl::bool_<false> &){
237+
if(is >> t)
238+
return;
239+
throw_input_stream_error();
240+
}
241+
242+
// An infinity and a NaN arrive as letters, which the extraction of a
243+
// floating point number refuses (issue #386). We look for the letters
244+
// before the extraction, not after it fails, because a failed extraction
245+
// may already have consumed some of them: libc++ takes "infini" out of
246+
// "infinity" before giving up. A number cannot begin with a letter, so
247+
// finding none means the extraction can go ahead.
248+
template<class T>
249+
void load_impl(T & t, boost::mpl::bool_<true> &){
250+
const bool negative = take_minus_sign();
251+
const std::string token = take_letters();
252+
if(! token.empty()){
253+
take_non_finite(t, token);
254+
}
255+
else if(is >> t){
256+
take_legacy_non_finite(t);
257+
}
258+
else{
259+
throw_input_stream_error();
260+
}
261+
if(negative){
262+
t = -t;
263+
}
264+
}
265+
266+
template<class T>
267+
void load(T & t)
268+
{
269+
typename has_non_finite<T>::type tag;
270+
load_impl(t, tag);
271+
}
272+
103273
void load(char & t)
104274
{
105275
short int i;

‎include/boost/archive/basic_text_oprimitive.hpp‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
// basic_text_oprimitive.hpp
1111

1212
// (C) Copyright 2002 Robert Ramey - http://www.rrsd.com .
13+
// Copyright 2026 Gennaro Prota.
1314
// Distributed under the Boost Software License, Version 1.0.
1415
// (See accompanying file LICENSE_1_0.txt or copy at
1516
// http://www.boost.org/LICENSE_1_0.txt)
@@ -25,6 +26,7 @@
2526
// use two template parameters
2627

2728
#include <iomanip>
29+
#include <limits>
2830
#include <locale>
2931
#include <cstddef> // size_t
3032

@@ -151,6 +153,31 @@ class BOOST_SYMBOL_VISIBLE basic_text_oprimitive
151153
>::type type;
152154
};
153155

156+
// An infinity and a NaN go out in a spelling the library fixes itself,
157+
// rather than in whatever the stream would produce. The latter would
158+
// differ between implementations; e.g. the Microsoft library wrote
159+
// "1.#INF" and "1.#QNAN" before 2015. The read side accepts the older
160+
// spellings too, so nothing written before this stops loading.
161+
template<class T>
162+
bool save_non_finite(const T & t){
163+
if(std::numeric_limits<T>::has_infinity){
164+
if(t == std::numeric_limits<T>::infinity()){
165+
put("inf");
166+
return true;
167+
}
168+
if(t == -std::numeric_limits<T>::infinity()){
169+
put("-inf");
170+
return true;
171+
}
172+
}
173+
// Nothing equals a NaN, itself included.
174+
if(std::numeric_limits<T>::has_quiet_NaN && t != t){
175+
put("nan");
176+
return true;
177+
}
178+
return false;
179+
}
180+
154181
template<class T>
155182
void save_impl(const T &t, boost::mpl::bool_<true> &){
156183
// must be a user mistake - can't serialize un-initialized data
@@ -159,6 +186,9 @@ class BOOST_SYMBOL_VISIBLE basic_text_oprimitive
159186
archive_exception(archive_exception::output_stream_error)
160187
);
161188
}
189+
if(save_non_finite(t)){
190+
return;
191+
}
162192
// The formulae for the number of decimal digits required is given in
163193
// http://www2.open-std.org/JTC1/SC22/WG21/docs/papers/2005/n1822.pdf
164194
// which is derived from Kahan's paper:

‎test/Jamfile.v2‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ test-suite "serialization" :
9696
[ test-bsl-run_files test_non_default_ctor ]
9797
[ test-bsl-run_files test_non_default_ctor2 ]
9898
[ test-bsl-run_files test_null_ptr ]
99+
[ test-bsl-run_files test_non_finite_floats ]
99100
[ test-bsl-run_files test_nvp : A ]
100101
[ test-bsl-run_files test_object ]
101102
[ test-bsl-run_files test_primitive ]
@@ -161,6 +162,7 @@ if ! $(BOOST_ARCHIVE_LIST) {
161162
[ test-bsl-run test_duplicate_type_registration ]
162163
[ test-bsl-run test_strong_typedef_move ]
163164
[ test-bsl-run test_stream_error_position ]
165+
[ test-bsl-run test_non_finite_format ]
164166
[ test-bsl-run test_private_ctor ]
165167
[ test-bsl-run test_reset_object_address : A ]
166168
[ test-bsl-run test_void_cast ]

‎test/test_non_finite_floats.cpp‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
2+
// test_non_finite_floats.cpp
3+
4+
// Copyright 2026 Gennaro Prota.
5+
// Distributed under the Boost Software License, Version 1.0.
6+
// (See accompanying file LICENSE_1_0.txt or copy at
7+
// http://www.boost.org/LICENSE_1_0.txt)
8+
9+
// See http://www.boost.org for updates, documentation, and revision history.
10+
11+
// An infinity is written as "inf" and a NaN as "nan", because that is what
12+
// the stream writes, and extraction of a floating point number then refuses
13+
// both: it accepts digits and little else, in libstdc++ and in the Microsoft
14+
// library alike. So the library used to write text archives which it could
15+
// not read back.
16+
17+
// Reported by nim65s in
18+
// https://github.com/boostorg/serialization/issues/386. Thanks!
19+
20+
#include <cstddef>
21+
#include <cstdio>
22+
#include <fstream>
23+
#include <limits>
24+
25+
#include <boost/config.hpp>
26+
#if defined(BOOST_NO_STDC_NAMESPACE)
27+
namespace std{
28+
using ::remove;
29+
}
30+
#endif
31+
32+
#include "test_tools.hpp"
33+
34+
#include <boost/serialization/nvp.hpp>
35+
36+
template<class T>
37+
struct values {
38+
T positive_infinity;
39+
T negative_infinity;
40+
T not_a_number;
41+
T ordinary;
42+
43+
values() :
44+
positive_infinity(std::numeric_limits<T>::infinity()),
45+
negative_infinity(-std::numeric_limits<T>::infinity()),
46+
not_a_number(std::numeric_limits<T>::quiet_NaN()),
47+
ordinary(T(24.567))
48+
{}
49+
50+
// Deliberately not the constructor above: this one is what the load
51+
// fills in, and it must start from something finite so that a load which
52+
// quietly does nothing cannot pass.
53+
values(int) :
54+
positive_infinity(0),
55+
negative_infinity(0),
56+
not_a_number(0),
57+
ordinary(0)
58+
{}
59+
60+
template<class Archive>
61+
void serialize(Archive & ar, const unsigned int /* version */){
62+
ar & boost::serialization::make_nvp("pos_inf", positive_infinity);
63+
ar & boost::serialization::make_nvp("neg_inf", negative_infinity);
64+
ar & boost::serialization::make_nvp("nan", not_a_number);
65+
ar & boost::serialization::make_nvp("ordinary", ordinary);
66+
}
67+
};
68+
69+
template<class T>
70+
void test_type(){
71+
const char * testfile = boost::archive::tmpnam(NULL);
72+
BOOST_REQUIRE(NULL != testfile);
73+
74+
const values<T> written;
75+
{
76+
test_ostream os(testfile, TEST_STREAM_FLAGS);
77+
test_oarchive oa(os, TEST_ARCHIVE_FLAGS);
78+
oa << boost::serialization::make_nvp("values", written);
79+
}
80+
81+
values<T> read(0);
82+
{
83+
test_istream is(testfile, TEST_STREAM_FLAGS);
84+
test_iarchive ia(is, TEST_ARCHIVE_FLAGS);
85+
ia >> boost::serialization::make_nvp("values", read);
86+
}
87+
88+
BOOST_CHECK(read.positive_infinity == std::numeric_limits<T>::infinity());
89+
BOOST_CHECK(read.negative_infinity == -std::numeric_limits<T>::infinity());
90+
// A NaN is equal to nothing, itself included, so that is the test.
91+
BOOST_CHECK(read.not_a_number != read.not_a_number);
92+
BOOST_CHECK(read.ordinary == written.ordinary);
93+
94+
std::remove(testfile);
95+
}
96+
97+
int test_main(int /* argc */, char * /* argv */ []){
98+
test_type<float>();
99+
test_type<double>();
100+
return EXIT_SUCCESS;
101+
}

0 commit comments

Comments
 (0)