Compare commits

...

11 Commits

Author SHA1 Message Date
Gennaro Prota bb67164668 Document that a derived archive must call init() itself
Since Boost 1.73, the CRTP base archive classes no longer call `init()`
from their constructors: doing so downcast this to the most derived
class while it was still being constructed, which is undefined behavior
and tripped the sanitizers. The derivation guide still showed the old
pattern, in which a class derived from one of the `xxx_oarchive_impl`
templates relied on the base to write the archive header. Following it
now yields an archive that never writes its header.

The guide now states that the most derived class is responsible for
calling `init()` in its own constructor body, honoring `no_header`, and
shows how, noting that the log_archive example is empty only because it
suppresses the header.

This also corrects the curiously recurring template argument in the
further-derivation sketch, which named `xml_oarchive` instead of
`log_archive`.

Refs issue #182.
2026-07-22 10:56:10 +02:00
Gennaro Prota 7132a62f52 Serialize optional<T> for T's that have no default constructor
The value of an `optional<T>` is now handled like a single element of a
standard library container: when `T` is default constructible, it is
serialized in place, exactly as before; otherwise, construction is
routed through `save_construct_data`/`load_construct_data` so that `T`
can be reconstructed on load. This lets `optional<T>` be serialized for
types that cannot or should not provide a default constructor
(const-qualified members, third party classes, and so on).

The default-constructible path is left byte for byte unchanged, so
archives written by earlier versions keep the same layout. The
construct-data path writes an extra `item_version` plus whatever
`save_construct_data` emits, which is new wire content only for types
that previously could not be serialized at all, so no existing archive
is affected.

Loading reconstructs the value with `detail::stack_construct`, which
placement-constructs the object through `load_construct_data` before the
value is read. This is unlike the pre-2017 implementation, which
serialized into unconstructed storage (`detail::stack_allocate`), so the
reliability concern that motivated the earlier restriction does not
apply here.

Closes issue #121.
2026-07-22 06:34:56 +00:00
Gennaro Prota f689b360b1 Add a regression test for cross-library base pointer deserialization
A derived class defined in a shared library is round-tripped through a
`unique_ptr` to its abstract base, across the library boundary, using a
binary-backed polymorphic archive, from an executable built with hidden
symbol visibility. The test reuses the existing `dll_polymorphic_base`
and `dll_polymorphic_derived2` libraries.

On GCC/ELF the per-type serializer singletons were duplicated across the
boundary, so the reader could not map the class id back to the derived
type and recovered the wrong object.

Refs issue #117.
2026-07-22 06:34:45 +00:00
Gennaro Prota 2780686e31 Record the first-registered serializer's tracking flag on save
Since 1.85, a Derived object serialized through a Base pointer across
module boundaries loses its data on load: the base subobject reads back
empty. PR #287 changed save to decide whether to emit an object id from
`co.m_bos_ptr` (the first serializer instance registered for a type),
but left the tracking flag written into the class preamble taken from
`bos`, the instance the current call happens to use.

Those can differ. With `track_selectively`, two module-local serializer
singletons for one type report different `tracking()`: the one whose
pointer serializer has been registered has `serialized_as_pointer()`
true, the other false. The writer then records one flag but lays the
object out per the other, so the reader desyncs and the subobject is
misread.

Record `co.m_bos_ptr`'s flag, the same instance the id decision uses, so
save is self-consistent again, as it was before PR #287 when both sides
read `bos`. The output is byte-identical whenever `co.m_bos_ptr == &bos`
(every single-module archive); only the already-broken cross-module case
changes.

Fixes issue #329.
2026-07-21 14:25:26 +00:00
Gennaro Prota 432ef2ee5a Add a regression test for reading a missing nvp (#109)
Issue #109 is the same Boost 1.66 `my_parse` regression as #82 and #99,
reached by a different path: reading an nvp that is not present fails
and consumes the closing tag, so at destruction `windup()` meets end of
input and (before the fix) threw out of the noexcept destructor,
terminating the process.

The `my_parse` fix already resolves it; this adds the guarding test. It
reads a value, then a missing nvp (catching the exception), and lets the
archive destruct: it aborts before the fix and passes after.

Refs issue #109.
2026-07-21 08:46:29 +00:00
Gennaro Prota db2610e1e6 Don't let the XML input archive destructor throw at end of input
The xml input archive destructor calls `windup()` to consume the
trailing tag; `windup()` calls `my_parse()`. Since Boost 1.66 (commit
64dc620), `my_parse()` threw `input_stream_error` whenever `get()` set
`failbit`. But `get()` sets both `failbit` and `eofbit` at end of input,
and the check tested `fail()` before `eof()`---so simply reaching the
end while scanning for a tag threw, escaping the implicitly noexcept
destructor and terminating the process. No closing tag is left to find
in several cases: a truncated stream, an archive whose writer was never
flushed, or a well-formed one whose closing tag an earlier failed read
had already consumed.

So, test `eof()` before `fail()` and return `false` for end of
input---as it did before 1.66 (a genuine, non-EOF stream error still
throws).

As defense in depth, this also wraps the `windup()` call in both XML
input destructors so no exception can escape them regardless.

Fixes issue #99, the same defect as the already-closed #82.

It also removes the terminate reported in #109, though that issue's
broader broken-state-after-exception concern is a by-design limitation
of the forward-only parse and isn't fixed.

Refs #109.
2026-07-21 08:46:29 +00:00
Gennaro Prota 64c2f11a66 Move, don't copy, the loaded value into the optional
`load_impl` read the value into a local and copy-assigned it to the
optional. A move-only type then failed to compile: `optional`'s
perfect-forwarding assignment is removed by SFINAE when the value is not
copy-assignable, leaving no viable `operator=`.

So, move the local into the optional instead.

The one-line fix was proposed by Venkat Murty in PR #330; this extends
it with `boost::move` and a regression test.
2026-07-20 15:39:56 +00:00
Gennaro Prota f45573ed35 Run the examples serially in CI
The example programs write to fixed temp filenames---demo and
demo_exception even share tmpdir()/demofile.txt---so running them under
`b2 -jN` with N > 1 races: concurrent processes clobber each other's
archive files and a reader picks up a half-written one, throwing
`archive_exception`. Therefore, run the examples under `-j1` (the tests
keep running in parallel).
2026-07-20 07:09:47 +00:00
Gennaro Prota c4ae6ec9d9 Skip the non-ASCII path round trip only on the wide-text archive
The wide *text* archive transcodes a narrow `std::string` through the
stream locale's `codecvt_null`, which cannot represent bytes outside the
ASCII range; it is the only archive in the test list that fails to
round-trip non-ASCII content. The others preserve it: narrow text and
XML store the bytes verbatim, binary stores them raw, and wide XML
installs a UTF-8 codecvt.

Rather than dropping the non-ASCII path from the test entirely, exercise
it against every archive but the wide-text one.
2026-07-20 07:09:47 +00:00
Gennaro Prota 28f606fa7d Disable std::filesystem serialization on GCC before 9
GCC's initial `<filesystem>` (before version 9) shipped in a separate
libstdc++fs and is unreliable: serializing a `std::filesystem::path`
segfaults at runtime in CI. Exclude those versions from the availability
guard so the feature is inert.

Refs issue #337.
2026-07-20 07:09:47 +00:00
Gennaro Prota 22ae0cd043 Run CI on merge-queue candidates 2026-07-17 18:01:05 +02:00
15 changed files with 514 additions and 39 deletions
+19 -2
View File
@@ -7,6 +7,7 @@ on:
- master
- develop
- feature/**
merge_group:
env:
UBSAN_OPTIONS: print_stacktrace=1
@@ -243,7 +244,15 @@ jobs:
run: |
cd ../boost-root
export ADDRMD=${{matrix.address-model}}
./b2 -j3 libs/$LIBRARY/test libs/$LIBRARY/example toolset=${{matrix.toolset}} cxxstd=${{matrix.cxxstd}} ${ADDRMD:+address-model=$ADDRMD} variant=debug,release
./b2 -j3 libs/$LIBRARY/test toolset=${{matrix.toolset}} cxxstd=${{matrix.cxxstd}} ${ADDRMD:+address-model=$ADDRMD} variant=debug,release
- name: Run examples
run: |
cd ../boost-root
export ADDRMD=${{matrix.address-model}}
# The demos use fixed temp filenames, so run them serially (-j1)
# to avoid concurrent runs clobbering each other's archive files.
./b2 -j1 libs/$LIBRARY/example toolset=${{matrix.toolset}} cxxstd=${{matrix.cxxstd}} ${ADDRMD:+address-model=$ADDRMD} variant=debug,release
windows:
strategy:
@@ -298,7 +307,15 @@ jobs:
shell: cmd
run: |
cd ../boost-root
b2 -j3 libs/%LIBRARY%/test libs/%LIBRARY%/example toolset=${{matrix.toolset}} cxxstd=${{matrix.cxxstd}} address-model=${{matrix.addrmd}} variant=debug,release embed-manifest-via=linker
b2 -j3 libs/%LIBRARY%/test toolset=${{matrix.toolset}} cxxstd=${{matrix.cxxstd}} address-model=${{matrix.addrmd}} variant=debug,release embed-manifest-via=linker
- name: Run examples
shell: cmd
run: |
cd ../boost-root
rem The demos use fixed temp filenames, so run them serially (-j1)
rem to avoid concurrent runs clobbering each other's archive files.
b2 -j1 libs/%LIBRARY%/example toolset=${{matrix.toolset}} cxxstd=${{matrix.cxxstd}} address-model=${{matrix.addrmd}} variant=debug,release embed-manifest-via=linker
posix-cmake-subdir:
strategy:
+22 -2
View File
@@ -70,8 +70,28 @@ class log_archive :
<li><i>Note the</i> <code style="white-space: normal">log_archive</code> <i>between the</i> &lt;&gt;
This is required so that base classes can downcast their <code style="white-space: normal">this</code> pointer
to the most derived class. This is referred to as <b>C</b>uriously <b>R</b>ecurring
<b>T</b>emplate <b>P</b>attern (<b>CRTP</b>) <a href="bibliography.html#11">[11]</a>.
<b>T</b>emplate <b>P</b>attern (<b>CRTP</b>) <a href="bibliography.html#11">[11]</a>.
It is used to implement static polymorphism.
<li><i>The most derived class is responsible for calling</i> <code style="white-space: normal">init()</code>.
The archive header (signature and version) is written by <code style="white-space: normal">init()</code>.
Before Boost 1.73, the <b>CRTP</b> base called <code style="white-space: normal">init()</code> from its own
constructor, but that downcast <code style="white-space: normal">this</code> to the most derived class
while that class was still being constructed, which is undefined behavior. As of 1.73, the base
classes no longer do this, so a derived archive that wants the standard header must call
<code style="white-space: normal">init()</code> itself, in its own constructor body, after the base
sub-objects have been constructed, honoring the <code style="white-space: normal">no_header</code> flag:
<pre><code>
log_archive(std::ostream &amp; os, unsigned int flags = 0) :
xml_oarchive_impl&lt;log_archive&gt;(os, flags)
{
if(0 == (flags &amp; boost::archive::no_header))
init();
}
</code></pre>
The <code style="white-space: normal">log_archive</code> shown in this example passes
<code style="white-space: normal">no_header</code>, so it has no header to write and its constructor
body is empty; see <code style="white-space: normal">xml_oarchive</code>, or the
<code style="white-space: normal">portable_binary_oarchive</code> example, for archives that do write one.
<li><i>Base classes need to be explicitly given access to the derived class.</i>
This can be done by making members public or by including friend declarations for
the base classes.
@@ -135,7 +155,7 @@ class log_archive :
{
public:
log_archive(std::ostream &amp; os, unsigned int flags = 0) :
log_archive_impl&lt;xml_oarchive&gt;(os, flags)
log_archive_impl&lt;log_archive&gt;(os, flags)
{}
};
</code></pre>
+2
View File
@@ -11,6 +11,8 @@ project libs/serialization/example
: requirements <library>../build//boost_serialization
;
import testing ;
import ../util/test :
run-template
run-invoke
@@ -189,7 +189,14 @@ xml_iarchive_impl<Archive>::~xml_iarchive_impl(){
if(boost::core::uncaught_exceptions() > 0)
return;
if(0 == (this->get_flags() & no_header)){
gimpl->windup(is);
// windup() parses the trailing end tag; an exception must not escape
// this (implicitly noexcept) destructor and terminate the process.
// A stream error while consuming the trailer is not worth that. #99
BOOST_TRY {
gimpl->windup(is);
}
BOOST_CATCH(...) {}
BOOST_CATCH_END
}
}
} // namespace archive
@@ -177,7 +177,14 @@ xml_wiarchive_impl<Archive>::~xml_wiarchive_impl(){
if(boost::core::uncaught_exceptions() > 0)
return;
if(0 == (this->get_flags() & no_header)){
gimpl->windup(is);
// windup() parses the trailing end tag; an exception must not escape
// this (implicitly noexcept) destructor and terminate the process.
// A stream error while consuming the trailer is not worth that. #99
BOOST_TRY {
gimpl->windup(is);
}
BOOST_CATCH(...) {}
BOOST_CATCH_END
}
}
+5 -1
View File
@@ -18,8 +18,12 @@
#include <boost/config.hpp>
// GCC's <filesystem> before version 9 is the initial, incomplete
// implementation (shipped in a separate libstdc++fs and buggy at runtime),
// so the feature is disabled there.
#if defined(__has_include)
# if __has_include(<filesystem>) && (BOOST_CXX_VERSION >= 201703L)
# if __has_include(<filesystem>) && (BOOST_CXX_VERSION >= 201703L) \
&& ! (defined(__GNUC__) && ! defined(__clang__) && __GNUC__ < 9)
# define BOOST_SERIALIZATION_HAS_STD_FILESYSTEM
# endif
#endif
+83 -26
View File
@@ -1,6 +1,7 @@
/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
// (C) Copyright 2002-4 Pavel Vozenilek .
// 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)
@@ -20,13 +21,17 @@
#include <optional>
#endif
#include <boost/move/utility_core.hpp>
#include <boost/core/addressof.hpp>
#include <boost/utility/enable_if.hpp>
#include <boost/serialization/item_version_type.hpp>
#include <boost/serialization/library_version_type.hpp>
#include <boost/serialization/version.hpp>
#include <boost/serialization/split_free.hpp>
#include <boost/serialization/nvp.hpp>
#include <boost/type_traits/is_pointer.hpp>
#include <boost/serialization/serialization.hpp>
#include <boost/serialization/detail/is_default_constructible.hpp>
#include <boost/serialization/detail/stack_constructor.hpp>
// function specializations must be defined in the appropriate
// namespace - boost::serialization
@@ -34,27 +39,91 @@ namespace boost {
namespace serialization {
namespace detail {
// The value of an optional<T> is serialized like a single element of an
// STL container: when T is default constructible we serialize it in place,
// otherwise we route construction through save/load_construct_data so that
// types without a default constructor can be reconstructed on load. The
// default-constructible path is left untouched so that archives written by
// earlier versions of the library keep the same layout.
// save the value: T is default constructible
template<class Archive, class OT>
typename boost::enable_if<
typename detail::is_default_constructible<typename OT::value_type>,
void
>::type
save_value(Archive & ar, const OT & ot){
ar << boost::serialization::make_nvp("value", *ot);
}
// save the value: T is not default constructible
template<class Archive, class OT>
typename boost::disable_if<
typename detail::is_default_constructible<typename OT::value_type>,
void
>::type
save_value(Archive & ar, const OT & ot){
typedef typename OT::value_type value_type;
const value_type & v = *ot;
const boost::serialization::item_version_type item_version(
boost::serialization::version<value_type>::value
);
ar << BOOST_SERIALIZATION_NVP(item_version);
boost::serialization::save_construct_data_adl(
ar,
boost::addressof(v),
item_version
);
ar << boost::serialization::make_nvp("value", v);
}
// load the value: T is default constructible
template<class Archive, class OT>
typename boost::enable_if<
typename detail::is_default_constructible<typename OT::value_type>,
void
>::type
load_value(Archive & ar, OT & ot, const unsigned int version){
if(0 == version){
boost::serialization::item_version_type item_version(0);
boost::serialization::library_version_type library_version(
ar.get_library_version()
);
if(boost::serialization::library_version_type(3) < library_version){
ar >> BOOST_SERIALIZATION_NVP(item_version);
}
}
typename OT::value_type t;
ar >> boost::serialization::make_nvp("value", t);
ot = boost::move(t);
}
// load the value: T is not default constructible
template<class Archive, class OT>
typename boost::disable_if<
typename detail::is_default_constructible<typename OT::value_type>,
void
>::type
load_value(Archive & ar, OT & ot, const unsigned int /* version */){
typedef typename OT::value_type value_type;
boost::serialization::item_version_type item_version(0);
ar >> BOOST_SERIALIZATION_NVP(item_version);
detail::stack_construct<Archive, value_type> aux(ar, item_version);
ar >> boost::serialization::make_nvp("value", aux.reference());
ot = boost::move(aux.reference());
ar.reset_object_address(boost::addressof(*ot), aux.address());
}
// OT is of the form optional<T>
template<class Archive, class OT>
void save_impl(
Archive & ar,
const OT & ot
){
// It is an inherent limitation to the serialization of optional.hpp
// that the underlying type must be either a pointer or must have a
// default constructor. It's possible that this could change sometime
// in the future, but for now, one will have to work around it. This can
// be done by serialization the optional<T> as optional<T *>
#ifndef BOOST_NO_CXX11_HDR_TYPE_TRAITS
BOOST_STATIC_ASSERT(
boost::serialization::detail::is_default_constructible<typename OT::value_type>::value
|| boost::is_pointer<typename OT::value_type>::value
);
#endif
const bool tflag(ot);
ar << boost::serialization::make_nvp("initialized", tflag);
if (tflag){
ar << boost::serialization::make_nvp("value", *ot);
save_value(ar, ot);
}
}
@@ -71,19 +140,7 @@ void load_impl(
ot.reset();
return;
}
if(0 == version){
boost::serialization::item_version_type item_version(0);
boost::serialization::library_version_type library_version(
ar.get_library_version()
);
if(boost::serialization::library_version_type(3) < library_version){
ar >> BOOST_SERIALIZATION_NVP(item_version);
}
}
typename OT::value_type t;
ar >> boost::serialization::make_nvp("value",t);
ot = t;
load_value(ar, ot, version);
}
} // detail
+2 -2
View File
@@ -255,7 +255,7 @@ basic_oarchive_impl::save_object(
if(bos.class_info()){
if( ! co.m_initialized){
ar.vsave(class_id_optional_type(co.m_class_id));
ar.vsave(tracking_type(bos.tracking(m_flags)));
ar.vsave(tracking_type(co.m_bos_ptr->tracking(m_flags)));
ar.vsave(version_type(bos.version()));
(const_cast<cobject_type &>(co)).m_initialized = true;
}
@@ -343,7 +343,7 @@ basic_oarchive_impl::save_pointer(
}
}
if(bos.class_info()){
ar.vsave(tracking_type(bos.tracking(m_flags)));
ar.vsave(tracking_type(co.m_bos_ptr->tracking(m_flags)));
ar.vsave(version_type(bos.version()));
}
(const_cast<cobject_type &>(co)).m_initialized = true;
+9 -2
View File
@@ -191,6 +191,15 @@ bool basic_xml_grammar<CharType>::my_parse(
for(;;){
CharType result;
is.get(result);
// Reaching end of input while scanning for the next character is the
// normal way an archive ends (e.g. windup() consuming the trailer);
// it is not a stream error. get() sets both eofbit and failbit at end
// of stream, so test eof() *before* fail() -- otherwise the normal
// termination is misreported as input_stream_error, which is fatal
// when it surfaces in the (noexcept) archive destructor via windup().
// See #99.
if(is.eof())
return false;
if(is.fail()){
boost::serialization::throw_exception(
boost::archive::archive_exception(
@@ -199,8 +208,6 @@ bool basic_xml_grammar<CharType>::my_parse(
)
);
}
if(is.eof())
return false;
arg += result;
if(result == delimiter)
break;
+3
View File
@@ -146,11 +146,14 @@ if ! $(BOOST_ARCHIVE_LIST) {
# we suppress tests of our dlls when using static libraries
[ test-bsl-run test_dll_simple : : dll_a : <link>static:<build>no ]
[ test-bsl-run test_dll_base_pointer : : dll_polymorphic_base dll_polymorphic_derived2 : <link>static:<build>no ]
# [ test-bsl-run test_dll_plugin : : dll_derived2 : <link>static:<build>no <target-os>linux:<linkflags>-ldl ]
[ test-bsl-run test_private_ctor ]
[ test-bsl-run test_reset_object_address : A ]
[ test-bsl-run test_void_cast ]
[ test-bsl-run test_xml_trailing_whitespace ]
[ test-bsl-run test_xml_missing_nvp ]
[ test-bsl-run test_mult_archive_types : : : [ requires std_wstreambuf ] ]
[ test-bsl-run test_iterators : : : [ requires std_wstreambuf ] ]
[ test-bsl-run test_iterators_base64 ]
+75
View File
@@ -0,0 +1,75 @@
/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
// test_dll_base_pointer.cpp
// Copyright 2026 Gennaro Prota
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// https://www.boost.org/LICENSE_1_0.txt)
// Regression test for issue #117. A derived class defined in a shared
// library is serialized and deserialized through a pointer to its abstract
// base, from an executable built with hidden symbol visibility.
// Historically this failed on GCC/ELF: the per type serializer singletons
// were duplicated across the shared library boundary, so the reader could not
// map the class id back to the derived type. The round trip must recover an
// object of the derived type.
#include <fstream>
#include <cstdio>
#include <typeinfo>
#include <boost/config.hpp>
#include "test_tools.hpp"
#ifndef BOOST_NO_CXX11_SMART_PTR
#include <boost/archive/polymorphic_binary_oarchive.hpp>
#include <boost/archive/polymorphic_binary_iarchive.hpp>
#include <boost/archive/tmpdir.hpp>
#include <boost/smart_ptr/make_unique.hpp>
#include <boost/serialization/unique_ptr.hpp>
#include <boost/serialization/nvp.hpp>
// polymorphic_derived2 (and its base, polymorphic_base) live in the shared
// library this test links against
#define POLYMORPHIC_DERIVED2_IMPORT
#include "polymorphic_derived2.hpp"
int test_main(int /* argc */, char * /* argv */ []){
const char * testfile = boost::archive::tmpnam(NULL);
BOOST_REQUIRE(NULL != testfile);
std::unique_ptr<polymorphic_base> saved =
boost::make_unique<polymorphic_derived2>();
{
std::ofstream os(testfile, std::ios::binary);
boost::archive::polymorphic_binary_oarchive oa(os);
boost::archive::polymorphic_oarchive & poa = oa;
poa << boost::serialization::make_nvp("ptr", saved);
}
std::unique_ptr<polymorphic_base> loaded;
{
std::ifstream is(testfile, std::ios::binary);
boost::archive::polymorphic_binary_iarchive ia(is);
boost::archive::polymorphic_iarchive & pia = ia;
pia >> boost::serialization::make_nvp("ptr", loaded);
}
BOOST_CHECK(0 != loaded.get());
// the object recovered through the base pointer must be the derived type
BOOST_CHECK(0 != dynamic_cast<polymorphic_derived2 *>(loaded.get()));
std::remove(testfile);
return EXIT_SUCCESS;
}
#else
int test_main(int /* argc */, char * /* argv */ []){
return EXIT_SUCCESS;
}
#endif // BOOST_NO_CXX11_SMART_PTR
+26 -2
View File
@@ -17,8 +17,11 @@
// `std::filesystem::path` is a C++17 feature; when it isn't available, this
// test has nothing to exercise and simply succeeds.
// Note the guard for GCC < 9, which matches the one in
// boost/serialization/filesystem.hpp: see the comment there for the why.
#if defined(__has_include)
# if __has_include(<filesystem>) && (BOOST_CXX_VERSION >= 201703L)
# if __has_include(<filesystem>) && (BOOST_CXX_VERSION >= 201703L) \
&& ! (defined(__GNUC__) && ! defined(__clang__) && __GNUC__ < 9)
# define BOOST_SERIALIZATION_TEST_STD_FILESYSTEM
# endif
#endif
@@ -29,6 +32,11 @@
#include <boost/serialization/filesystem.hpp>
#include <filesystem>
#include <type_traits>
#ifndef BOOST_NO_STD_WSTREAMBUF
#include <boost/archive/text_woarchive.hpp>
#endif
void check_roundtrip(const std::filesystem::path & original){
const char * testfile = boost::archive::tmpnam(NULL);
@@ -57,6 +65,21 @@ std::filesystem::path from_utf8(const std::string & utf8){
#endif
}
// The wide *text* archive is the one archive that cannot round-trip non-ASCII
// content: it transcodes the narrow UTF-8 std::string through the stream
// locale's codecvt (codecvt_null), which mangles bytes outside the ASCII
// range. Every other archive in the test list preserves them -- narrow text
// and XML store the bytes verbatim, binary stores them raw, and wide XML
// installs a UTF-8 codecvt. So the non-ASCII case is exercised everywhere
// except that single archive.
bool archive_round_trips_non_ascii(){
#ifndef BOOST_NO_STD_WSTREAMBUF
return ! std::is_same<test_oarchive, boost::archive::text_woarchive>::value;
#else
return true;
#endif
}
int test_main(int /* argc */, char * /* argv */ []){
check_roundtrip(std::filesystem::path()); // empty
check_roundtrip("foo/bar/baz.txt"); // relative
@@ -66,7 +89,8 @@ int test_main(int /* argc */, char * /* argv */ []){
// The escapes are the UTF-8 encoding of U+00E9, U+00EF and U+00FC
// (e-acute, i-diaeresis, u-diaeresis); written as `\x` so the source
// file stays pure ASCII and encoding-independent.
check_roundtrip(from_utf8("caf\xC3\xA9/na\xC3\xAF" "ve/\xC3\xBC.txt"));
if(archive_round_trips_non_ascii())
check_roundtrip(from_utf8("caf\xC3\xA9/na\xC3\xAF" "ve/\xC3\xBC.txt"));
return EXIT_SUCCESS;
}
+135
View File
@@ -2,6 +2,7 @@
// test_optional.cpp
// (C) Copyright 2004 Pavel Vozenilek
// 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)
@@ -92,6 +93,130 @@ int test(){
return EXIT_SUCCESS;
}
// A move-only value type: deleted copy, defaulted move. Loading an
// optional<M> must move the deserialized value into place rather than copy
// it.
#if !defined(BOOST_NO_CXX11_RVALUE_REFERENCES) \
&& !defined(BOOST_NO_CXX11_DELETED_FUNCTIONS) \
&& !defined(BOOST_NO_CXX11_DEFAULTED_FUNCTIONS)
#define BOOST_SERIALIZATION_TEST_OPTIONAL_MOVE_ONLY
struct M {
int m_x;
M() : m_x(0) {}
explicit M(int x) : m_x(x) {}
M(const M &) = delete;
M & operator=(const M &) = delete;
M(M &&) = default;
M & operator=(M &&) = default;
template<class Archive>
void serialize(Archive & ar, const unsigned int /* version */){
ar & boost::serialization::make_nvp("x", m_x);
}
};
template<template<class> class Optional>
int test_move_only(){
const char * testfile = boost::archive::tmpnam(NULL);
BOOST_REQUIRE(NULL != testfile);
Optional<M> o_empty;
Optional<M> o_value(M(42));
{
test_ostream os(testfile, TEST_STREAM_FLAGS);
test_oarchive oa(os, TEST_ARCHIVE_FLAGS);
oa << boost::serialization::make_nvp("o_empty", o_empty);
oa << boost::serialization::make_nvp("o_value", o_value);
}
// start each target in the opposite state, so load must both reset and
// assign
Optional<M> o_empty_a(M(7));
Optional<M> o_value_a;
{
test_istream is(testfile, TEST_STREAM_FLAGS);
test_iarchive ia(is, TEST_ARCHIVE_FLAGS);
ia >> boost::serialization::make_nvp("o_empty", o_empty_a);
ia >> boost::serialization::make_nvp("o_value", o_value_a);
}
BOOST_CHECK(! o_empty_a);
BOOST_CHECK(static_cast<bool>(o_value_a) && 42 == o_value_a->m_x);
std::remove(testfile);
return EXIT_SUCCESS;
}
#endif // move-only support available
// A type without a default constructor. It is reconstructed on load through
// save_construct_data / load_construct_data, which is exactly what an
// optional<ND> now relies on (see issue #121). m_i is the constructor
// argument, m_x is ordinary serialized state.
struct ND {
int m_i;
int m_x;
ND(int i, int x) : m_i(i), m_x(x) {}
explicit ND(int i) : m_i(i), m_x(0) {}
template<class Archive>
void serialize(Archive & ar, const unsigned int /* version */){
ar & boost::serialization::make_nvp("x", m_x);
}
bool operator==(const ND & rhs) const {
return m_i == rhs.m_i && m_x == rhs.m_x;
}
};
namespace boost {
namespace serialization {
template<class Archive>
void save_construct_data(
Archive & ar, const ND * p, const unsigned int /* version */
){
ar << boost::serialization::make_nvp("i", p->m_i);
}
template<class Archive>
void load_construct_data(
Archive & ar, ND * p, const unsigned int /* version */
){
int i;
ar >> boost::serialization::make_nvp("i", i);
::new(p) ND(i);
}
} // serialization
} // boost
template<template<class> class Optional>
int test_non_default_ctor(){
const char * testfile = boost::archive::tmpnam(NULL);
BOOST_REQUIRE(NULL != testfile);
const Optional<ND> o_empty;
const Optional<ND> o_value(ND(7, 42));
{
test_ostream os(testfile, TEST_STREAM_FLAGS);
test_oarchive oa(os, TEST_ARCHIVE_FLAGS);
oa << boost::serialization::make_nvp("o_empty", o_empty);
oa << boost::serialization::make_nvp("o_value", o_value);
}
// start each target in the opposite state, so load must both reset and
// reconstruct
Optional<ND> o_empty_a(ND(1, 1));
Optional<ND> o_value_a;
{
test_istream is(testfile, TEST_STREAM_FLAGS);
test_iarchive ia(is, TEST_ARCHIVE_FLAGS);
ia >> boost::serialization::make_nvp("o_empty", o_empty_a);
ia >> boost::serialization::make_nvp("o_value", o_value_a);
}
BOOST_CHECK(! o_empty_a);
BOOST_CHECK(static_cast<bool>(o_value_a));
BOOST_CHECK(o_value_a && *o_value == *o_value_a);
std::remove(testfile);
return EXIT_SUCCESS;
}
#include <boost/serialization/optional.hpp>
#ifndef BOOST_NO_CXX17_HDR_OPTIONAL
#include <optional>
@@ -102,5 +227,15 @@ int test_main( int /* argc */, char* /* argv */[] ){
#ifndef BOOST_NO_CXX17_HDR_OPTIONAL
test<std::optional>();
#endif
#ifdef BOOST_SERIALIZATION_TEST_OPTIONAL_MOVE_ONLY
test_move_only<boost::optional>();
#ifndef BOOST_NO_CXX17_HDR_OPTIONAL
test_move_only<std::optional>();
#endif
#endif
test_non_default_ctor<boost::optional>();
#ifndef BOOST_NO_CXX17_HDR_OPTIONAL
test_non_default_ctor<std::optional>();
#endif
return EXIT_SUCCESS;
}
+58
View File
@@ -0,0 +1,58 @@
/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
// test_xml_missing_nvp.cpp
// Copyright 2026 Gennaro Prota
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// https://www.boost.org/LICENSE_1_0.txt)
// Regression test for issue #109. Attempting to read an nvp that is not
// present throws archive_exception, which the caller may catch. The failed
// read consumes the closing tag, so at destruction windup() reaches end of
// input -- the same Boost 1.66 my_parse regression as #82/#99, reached by a
// different path. The input archive must still destruct without terminating.
#include <sstream>
#include <string>
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/archive_exception.hpp>
#include <boost/serialization/nvp.hpp>
#include "test_tools.hpp"
int test_main(int /* argc */, char * /* argv */ []){
// Build a valid XML archive holding a single value.
std::string content;
{
std::ostringstream os;
{
boost::archive::xml_oarchive oa(os);
const int x = 42;
oa << boost::serialization::make_nvp("x", x);
}
content = os.str();
}
int x = 0;
bool caught = false;
{
std::istringstream is(content);
boost::archive::xml_iarchive ia(is);
ia >> boost::serialization::make_nvp("x", x);
try {
int y = 0;
ia >> boost::serialization::make_nvp("not_there", y);
} catch (const boost::archive::archive_exception &){
caught = true;
}
// `ia` is destroyed here, after the caught exception. Before the fix,
// windup() hit end of input (the failed read consumed the closing
// tag) and threw out of the noexcept destructor, terminating.
}
BOOST_CHECK(42 == x);
BOOST_CHECK(caught);
return EXIT_SUCCESS;
}
+59
View File
@@ -0,0 +1,59 @@
/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8
// test_xml_trailing_whitespace.cpp
// Copyright 2026 Gennaro Prota
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// https://www.boost.org/LICENSE_1_0.txt)
// Regression test for issue #99. When an XML input archive is destroyed and
// its stream has no closing tag left -- only trailing whitespace before end
// of input (a truncated archive; the reported case had the stream "contain
// \r\n") -- windup() reaches end of input while scanning for the trailing
// tag. End of input there is normal termination, not a stream error, so the
// (implicitly noexcept) destructor must complete cleanly rather than throw,
// which used to terminate the process.
#include <sstream>
#include <string>
#include <boost/archive/xml_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/serialization/nvp.hpp>
#include "test_tools.hpp"
int test_main(int /* argc */, char * /* argv */ []){
// Build a valid XML archive holding a single value.
std::string content;
{
std::ostringstream os;
{
boost::archive::xml_oarchive oa(os);
const int x = 42;
oa << boost::serialization::make_nvp("x", x);
}
content = os.str();
}
// Drop the closing document tag and leave only trailing whitespace, so the
// input archive's windup() reaches end of input at destruction instead of
// finding a tag.
const std::string::size_type close = content.rfind("</boost_serialization>");
BOOST_REQUIRE(std::string::npos != close);
content.erase(close);
content += "\r\n";
int y = 0;
{
std::istringstream is(content);
boost::archive::xml_iarchive ia(is);
ia >> boost::serialization::make_nvp("x", y);
// `ia` is destroyed here with only trailing whitespace left. Before
// the fix, windup() threw input_stream_error out of the noexcept
// destructor and terminated the process.
}
BOOST_CHECK(42 == y);
return EXIT_SUCCESS;
}