diff --git a/include/boost/archive/impl/xml_iarchive_impl.ipp b/include/boost/archive/impl/xml_iarchive_impl.ipp index b0847d4a1..2c7b04633 100644 --- a/include/boost/archive/impl/xml_iarchive_impl.ipp +++ b/include/boost/archive/impl/xml_iarchive_impl.ipp @@ -189,7 +189,14 @@ xml_iarchive_impl::~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 diff --git a/include/boost/archive/impl/xml_wiarchive_impl.ipp b/include/boost/archive/impl/xml_wiarchive_impl.ipp index 198dfafb9..546ab0d4c 100644 --- a/include/boost/archive/impl/xml_wiarchive_impl.ipp +++ b/include/boost/archive/impl/xml_wiarchive_impl.ipp @@ -177,7 +177,14 @@ xml_wiarchive_impl::~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 } } diff --git a/src/basic_xml_grammar.ipp b/src/basic_xml_grammar.ipp index c0d5320c7..ee9eced64 100644 --- a/src/basic_xml_grammar.ipp +++ b/src/basic_xml_grammar.ipp @@ -191,6 +191,15 @@ bool basic_xml_grammar::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::my_parse( ) ); } - if(is.eof()) - return false; arg += result; if(result == delimiter) break; diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index a7b521577..6800c81d4 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -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 ] diff --git a/test/test_xml_missing_nvp.cpp b/test/test_xml_missing_nvp.cpp new file mode 100644 index 000000000..054e61fec --- /dev/null +++ b/test/test_xml_missing_nvp.cpp @@ -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 +#include + +#include +#include +#include +#include + +#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; +} diff --git a/test/test_xml_trailing_whitespace.cpp b/test/test_xml_trailing_whitespace.cpp new file mode 100644 index 000000000..b912a6c3b --- /dev/null +++ b/test/test_xml_trailing_whitespace.cpp @@ -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 +#include + +#include +#include +#include + +#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_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; +}