Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions include/boost/archive/basic_binary_iprimitive.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ namespace std{
#include <boost/archive/codecvt_null.hpp>
#include <boost/archive/archive_exception.hpp>
#include <boost/archive/detail/auto_link_archive.hpp>
#include <boost/archive/detail/stream_position_message.hpp>
#include <boost/archive/detail/abi_prefix.hpp> // must be the last header

namespace boost {
Expand Down Expand Up @@ -167,10 +168,15 @@ basic_binary_iprimitive<Archive, Elem, Tr>::load_binary(
static_cast<Elem *>(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<std::streamsize>::const_max);
s = static_cast<std::streamsize>(count % sizeof(Elem));
Expand All @@ -181,10 +187,16 @@ basic_binary_iprimitive<Archive, Elem, Tr>::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<char*>(address) + (count - s), &t, static_cast<std::size_t>(s));
}
}
Expand Down
9 changes: 8 additions & 1 deletion include/boost/archive/basic_text_iprimitive.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
// use two template parameters

#include <locale>
#include <string>
#include <cstddef> // size_t

#include <boost/config.hpp>
Expand All @@ -48,6 +49,7 @@ namespace std{
#include <boost/archive/codecvt_null.hpp>
#include <boost/archive/archive_exception.hpp>
#include <boost/archive/basic_streambuf_locale_saver.hpp>
#include <boost/archive/detail/stream_position_message.hpp>
#include <boost/archive/detail/abi_prefix.hpp> // must be the last header

namespace boost {
Expand Down Expand Up @@ -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()
)
);
}

Expand Down
50 changes: 50 additions & 0 deletions include/boost/archive/detail/stream_position_message.hpp
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <ios>
#include <streambuf>
#include <string>

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<class Elem, class Tr>
std::string stream_position_message(
std::basic_streambuf<Elem, Tr> * 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("<unknown>")
: std::to_string(static_cast<std::streamoff>(pos)));
}

}
}
}

#endif // BOOST_ARCHIVE_DETAIL_STREAM_POSITION_MESSAGE_HPP
17 changes: 15 additions & 2 deletions include/boost/archive/impl/basic_xml_iarchive.ipp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <boost/serialization/throw_exception.hpp>
#include <boost/archive/xml_archive_exception.hpp>
#include <boost/archive/basic_xml_iarchive.hpp>
#include <boost/archive/detail/stream_position_message.hpp>
#include <boost/serialization/tracking.hpp>

namespace boost {
Expand All @@ -32,8 +33,14 @@ basic_xml_iarchive<Archive>::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
Expand All @@ -48,8 +55,14 @@ basic_xml_iarchive<Archive>::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()
)
);
}

Expand Down
1 change: 1 addition & 0 deletions test/Jamfile.v2
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ if ! $(BOOST_ARCHIVE_LIST) {
# [ test-bsl-run test_dll_plugin : : dll_derived2 : <link>static:<build>no <target-os>linux:<linkflags>-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 ]
Expand Down
143 changes: 143 additions & 0 deletions test/test_stream_error_position.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <cstdlib> // atol
#include <sstream>
#include <string>

// 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 <boost/core/lightweight_test.hpp>

#include <boost/archive/archive_exception.hpp>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>
#include <boost/serialization/nvp.hpp>

struct pair_of_ints {
int x;
int y;
pair_of_ints() : x(0), y(0) {}
template<class Archive>
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<class OArchive>
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<class IArchive>
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<class OArchive, class IArchive>
static void test_corrupted_value(bool from_front){
std::string data = archive_of_a_pair<OArchive>();
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<IArchive>(data);
BOOST_TEST_GT(offset, 0);
BOOST_TEST_LE(offset, static_cast<long>(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<class OArchive, class IArchive>
static void test_truncated(std::size_t chop){
std::string data = archive_of_a_pair<OArchive>();
BOOST_TEST_GT(data.size(), chop);
data.resize(data.size() - chop);

const long offset = offset_of_failure<IArchive>(data);
BOOST_TEST_GT(offset, 0);
BOOST_TEST_LE(offset, static_cast<long>(data.size()));
}

int
main(){
using namespace boost::archive;

test_corrupted_value<text_oarchive, text_iarchive>(false);
test_corrupted_value<xml_oarchive, xml_iarchive>(true);

test_truncated<binary_oarchive, binary_iarchive>(6);
test_truncated<xml_oarchive, xml_iarchive>(30);

return boost::report_errors();
}