Don't terminate when fmt::ostream fails to write in the destructor

The destructor flushed the buffer and let write errors escape, calling
std::terminate on errors like a full disk. Report the error instead, like
file and buffered_file do on close errors. Also clear the buffer before
writing in flush so that data from a failed write, e.g. in close, is not
written again by the destructor.
This commit is contained in:
Victor Zverovich 2026-11-08 09:06:43 -08:00
parent bee9426a64
commit aa2ce7d30d
3 changed files with 23 additions and 3 deletions

View File

@ -376,9 +376,11 @@ class ostream : private detail::buffer<char> {
}
inline void flush() {
if (size() == 0) return;
file_.write(data(), size() * sizeof(data()[0]));
size_t n = size();
if (n == 0) return;
// Clear before writing to not write the data again if the write fails.
clear();
file_.write(data(), n);
}
template <typename... T>

View File

@ -396,7 +396,8 @@ ostream::ostream(ostream&& other) noexcept
}
ostream::~ostream() {
flush();
FMT_TRY { flush(); }
FMT_CATCH(...) { report_system_error(errno, "cannot write to file"); }
delete[] data();
}
#endif // FMT_USE_FCNTL

View File

@ -328,6 +328,23 @@ TEST(ostream_test, flush) {
EXPECT_READ(in, "x");
}
TEST(ostream_test, write_error_in_dtor) {
auto test_file = uniq_file_name(__LINE__);
EXPECT_WRITE(
stderr,
fmt::output_file(test_file, file::RDONLY | file::CREATE).print("x"),
system_error_message(EBADF, "cannot write to file") + "\n");
}
TEST(ostream_test, close_error) {
auto out =
fmt::output_file(uniq_file_name(__LINE__), file::RDONLY | file::CREATE);
out.print("x");
EXPECT_THROW(out.close(), std::system_error);
// Check that the destructor doesn't try to write the data again.
EXPECT_WRITE(stderr, { auto moved = std::move(out); }, "");
}
TEST(file_test, default_ctor) {
file f;
EXPECT_EQ(-1, f.descriptor());