Compare commits

..

7 Commits

Author SHA1 Message Date
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
9 changed files with 207 additions and 5 deletions
+1
View File
@@ -7,6 +7,7 @@ on:
- master
- develop
- feature/**
merge_group:
env:
UBSAN_OPTIONS: print_stacktrace=1
@@ -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
}
}
+2 -1
View File
@@ -20,6 +20,7 @@
#include <optional>
#endif
#include <boost/move/utility_core.hpp>
#include <boost/serialization/item_version_type.hpp>
#include <boost/serialization/library_version_type.hpp>
#include <boost/serialization/version.hpp>
@@ -83,7 +84,7 @@ void load_impl(
}
typename OT::value_type t;
ar >> boost::serialization::make_nvp("value",t);
ot = t;
ot = boost::move(t);
}
} // detail
+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;
+2
View File
@@ -151,6 +151,8 @@ if ! $(BOOST_ARCHIVE_LIST) {
[ 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 ]
+60
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,59 @@ 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
#include <boost/serialization/optional.hpp>
#ifndef BOOST_NO_CXX17_HDR_OPTIONAL
#include <optional>
@@ -102,5 +156,11 @@ 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
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;
}