diff --git a/include/boost/archive/basic_binary_iprimitive.hpp b/include/boost/archive/basic_binary_iprimitive.hpp index 3ba8b959c..57c9f7a3d 100644 --- a/include/boost/archive/basic_binary_iprimitive.hpp +++ b/include/boost/archive/basic_binary_iprimitive.hpp @@ -55,6 +55,7 @@ namespace std{ #include #include #include +#include #include // must be the last header namespace boost { @@ -167,10 +168,15 @@ basic_binary_iprimitive::load_binary( static_cast(address), s ); - if(scount != s) + if(scount != s){ + const std::string message = detail::stream_position_message(& m_sb); boost::serialization::throw_exception( - archive_exception(archive_exception::input_stream_error) + archive_exception( + archive_exception::input_stream_error, + message.c_str() + ) ); + } // note: an optimizer should eliminate the following for char files BOOST_ASSERT(count % sizeof(Elem) <= boost::integer_traits::const_max); s = static_cast(count % sizeof(Elem)); @@ -181,10 +187,16 @@ basic_binary_iprimitive::load_binary( // ); Elem t; scount = m_sb.sgetn(& t, 1); - if(scount != 1) + if(scount != 1){ + const std::string message = + detail::stream_position_message(& m_sb); boost::serialization::throw_exception( - archive_exception(archive_exception::input_stream_error) + archive_exception( + archive_exception::input_stream_error, + message.c_str() + ) ); + } std::memcpy(static_cast(address) + (count - s), &t, static_cast(s)); } } diff --git a/include/boost/archive/basic_text_iprimitive.hpp b/include/boost/archive/basic_text_iprimitive.hpp index 4b9f1a8c3..4ab2734cd 100644 --- a/include/boost/archive/basic_text_iprimitive.hpp +++ b/include/boost/archive/basic_text_iprimitive.hpp @@ -25,6 +25,7 @@ // use two template parameters #include +#include #include // size_t #include @@ -48,6 +49,7 @@ namespace std{ #include #include #include +#include #include // must be the last header namespace boost { @@ -88,8 +90,13 @@ class BOOST_SYMBOL_VISIBLE basic_text_iprimitive { { if(is >> t) return; + const std::string message = + detail::stream_position_message(is.rdbuf()); boost::serialization::throw_exception( - archive_exception(archive_exception::input_stream_error) + archive_exception( + archive_exception::input_stream_error, + message.c_str() + ) ); } diff --git a/include/boost/archive/detail/stream_position_message.hpp b/include/boost/archive/detail/stream_position_message.hpp new file mode 100644 index 000000000..8fec5489b --- /dev/null +++ b/include/boost/archive/detail/stream_position_message.hpp @@ -0,0 +1,50 @@ +#ifndef BOOST_ARCHIVE_DETAIL_STREAM_POSITION_MESSAGE_HPP +#define BOOST_ARCHIVE_DETAIL_STREAM_POSITION_MESSAGE_HPP + +// MS compatible compilers support #pragma once +#if defined(_MSC_VER) +# pragma once +#endif + +/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8 +// stream_position_message.hpp: a string describing where an input stream +// error was met + +// 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. + +#include +#include +#include +#include + +namespace boost { +namespace archive { +namespace detail { + +// The buffer is asked, rather than the stream it belongs to, because a +// stream in a fail state answers -1 whatever its position really is. A +// buffer which is absent, or which cannot seek, answers -1 as well, and is +// reported as an unknown position. +template +std::string stream_position_message( + std::basic_streambuf * const sb +){ + const std::streampos pos = (NULL == sb) + ? std::streampos(-1) + : sb->pubseekoff(0, std::ios_base::cur, std::ios_base::in); + return "at offset " + + (std::streampos(-1) == pos + ? std::string("") + : std::to_string(static_cast(pos))); +} + +} +} +} + +#endif // BOOST_ARCHIVE_DETAIL_STREAM_POSITION_MESSAGE_HPP diff --git a/include/boost/archive/impl/basic_xml_iarchive.ipp b/include/boost/archive/impl/basic_xml_iarchive.ipp index bce94813b..e39783cc2 100644 --- a/include/boost/archive/impl/basic_xml_iarchive.ipp +++ b/include/boost/archive/impl/basic_xml_iarchive.ipp @@ -16,6 +16,7 @@ #include #include #include +#include #include namespace boost { @@ -32,8 +33,14 @@ basic_xml_iarchive::load_start(const char *name){ return; bool result = this->This()->gimpl->parse_start_tag(this->This()->get_is()); if(true != result){ + const std::string message = detail::stream_position_message( + this->This()->get_is().rdbuf() + ); boost::serialization::throw_exception( - archive_exception(archive_exception::input_stream_error) + archive_exception( + archive_exception::input_stream_error, + message.c_str() + ) ); } // don't check start tag at highest level @@ -48,8 +55,14 @@ basic_xml_iarchive::load_end(const char *name){ return; bool result = this->This()->gimpl->parse_end_tag(this->This()->get_is()); if(true != result){ + const std::string message = detail::stream_position_message( + this->This()->get_is().rdbuf() + ); boost::serialization::throw_exception( - archive_exception(archive_exception::input_stream_error) + archive_exception( + archive_exception::input_stream_error, + message.c_str() + ) ); } diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index 965d8dabb..ad2174269 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -159,6 +159,7 @@ if ! $(BOOST_ARCHIVE_LIST) { # [ test-bsl-run test_dll_plugin : : dll_derived2 : static:no linux:-ldl ] [ test-bsl-run test_duplicate_type_registration ] + [ test-bsl-run test_stream_error_position ] [ test-bsl-run test_private_ctor ] [ test-bsl-run test_reset_object_address : A ] [ test-bsl-run test_void_cast ] diff --git a/test/test_stream_error_position.cpp b/test/test_stream_error_position.cpp new file mode 100644 index 000000000..b11c25061 --- /dev/null +++ b/test/test_stream_error_position.cpp @@ -0,0 +1,143 @@ +/////////1/////////2/////////3/////////4/////////5/////////6/////////7/////////8 +// test_stream_error_position.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 input_stream_error used to say only that the input had failed. The +// exception now names the offset the input went wrong at. + +// Suggested by swebb2066 in +// https://github.com/boostorg/serialization/pull/318. Thanks! + +// The offset itself is not checked against a fixed number: it moves with the +// archive version, and with the width of whatever the header happens to hold. +// What has to hold is that an offset is reported at all, that it is a number, +// and that it falls inside the archive. + +#include +#include // atol +#include +#include + +// The lightweight test of Boost.Core is used here in place of +// test_tools.hpp, whose test_main runs one archive type per build. Every +// archive type has to be exercised in the same run, because each reaches the +// offset by a different route: the text primitive through a failed +// extraction, the binary one through a short read, and the XML one through a +// parse which ran out of input. +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +struct pair_of_ints { + int x; + int y; + pair_of_ints() : x(0), y(0) {} + template + void serialize(Archive & ar, const unsigned int /* version */){ + ar & BOOST_SERIALIZATION_NVP(x); + ar & BOOST_SERIALIZATION_NVP(y); + } +}; + +// Returns the offset named by the message, or -1 if it names none. A +// message reads "input stream error-at offset 36". +static long reported_offset(const std::string & message){ + const std::string tag("at offset "); + const std::size_t at = message.find(tag); + if(std::string::npos == at){ + return -1; + } + const std::string digits = message.substr(at + tag.size()); + if(digits.empty() || '0' > digits[0] || '9' < digits[0]){ + return -1; + } + return std::atol(digits.c_str()); +} + +template +static std::string archive_of_a_pair(){ + std::ostringstream os; + { + OArchive oa(os); + pair_of_ints p; + p.x = 11; + p.y = 22; + oa << BOOST_SERIALIZATION_NVP(p); + } + return os.str(); +} + +// Loads the given text as an archive and returns the offset the resulting +// input_stream_error names, or -1 if the load did not fail that way. +template +static long offset_of_failure(const std::string & data){ + BOOST_TRY { + std::istringstream is(data); + IArchive ia(is); + pair_of_ints p; + ia >> BOOST_SERIALIZATION_NVP(p); + } + BOOST_CATCH(const boost::archive::archive_exception & e){ + if(boost::archive::archive_exception::input_stream_error != e.code){ + return -1; + } + return reported_offset(e.what()); + } + BOOST_CATCH_END + return -1; +} + +// A value the archive cannot read back, put where the second int was +// written. The text and binary archives keep the last "22" for that int; +// the XML one writes it before the closing tags, so the first occurrence is +// the one to spoil. +template +static void test_corrupted_value(bool from_front){ + std::string data = archive_of_a_pair(); + const std::size_t at = from_front ? data.find("22") : data.rfind("22"); + BOOST_TEST(std::string::npos != at); + data.replace(at, 2, "zz"); + + const long offset = offset_of_failure(data); + BOOST_TEST_GT(offset, 0); + BOOST_TEST_LE(offset, static_cast(data.size())); +} + +// An archive cut short, so that the failure comes from running out of input +// rather than from a value which will not parse. +template +static void test_truncated(std::size_t chop){ + std::string data = archive_of_a_pair(); + BOOST_TEST_GT(data.size(), chop); + data.resize(data.size() - chop); + + const long offset = offset_of_failure(data); + BOOST_TEST_GT(offset, 0); + BOOST_TEST_LE(offset, static_cast(data.size())); +} + +int +main(){ + using namespace boost::archive; + + test_corrupted_value(false); + test_corrupted_value(true); + + test_truncated(6); + test_truncated(30); + + return boost::report_errors(); +}