util: check that a file has been closed before ~AutoFile() is called

If an `AutoFile` has been written to, then expect callers to have closed
it explicitly via the `AutoFile::fclose()` method. This is because if
the destructor calls `std::fclose()` and encounters an error, then it
is too late to indicate this to the caller in a meaningful way.
This commit is contained in:
Vasil Dimov
2025-05-02 16:22:41 +02:00
parent 8bb34f07df
commit 4bb5dd78ea
2 changed files with 32 additions and 3 deletions

View File

@@ -85,6 +85,7 @@ void AutoFile::write(std::span<const std::byte> src)
if (std::fwrite(src.data(), 1, src.size(), m_file) != src.size()) {
throw std::ios_base::failure("AutoFile::write: write failed");
}
m_was_written = true;
if (m_position.has_value()) *m_position += src.size();
} else {
std::array<std::byte, 4096> buf;
@@ -107,6 +108,7 @@ void AutoFile::write_buffer(std::span<std::byte> src)
if (std::fwrite(src.data(), 1, src.size(), m_file) != src.size()) {
throw std::ios_base::failure("AutoFile::write_buffer: write failed");
}
m_was_written = true;
if (m_position) *m_position += src.size();
}
@@ -117,6 +119,7 @@ bool AutoFile::Commit()
bool AutoFile::Truncate(unsigned size)
{
m_was_written = true;
return ::TruncateFile(m_file, size);
}

View File

@@ -6,10 +6,13 @@
#ifndef BITCOIN_STREAMS_H
#define BITCOIN_STREAMS_H
#include <logging.h>
#include <serialize.h>
#include <span.h>
#include <support/allocators/zeroafterfree.h>
#include <util/check.h>
#include <util/overflow.h>
#include <util/syserror.h>
#include <algorithm>
#include <cassert>
@@ -386,7 +389,13 @@ public:
*
* Will automatically close the file when it goes out of scope if not null.
* If you're returning the file pointer, return file.release().
* If you need to close the file early, use file.fclose() instead of fclose(file).
* If you need to close the file early, use autofile.fclose() instead of fclose(underlying_FILE).
*
* @note If the file has been written to, then the caller must close it
* explicitly with the `fclose()` method, check if it returns an error and treat
* such an error as if the `write()` method failed. The OS's `fclose(3)` may
* fail to flush to disk data that has been previously written, rendering the
* file corrupt.
*/
class AutoFile
{
@@ -394,11 +403,28 @@ protected:
std::FILE* m_file;
std::vector<std::byte> m_xor;
std::optional<int64_t> m_position;
bool m_was_written{false};
public:
explicit AutoFile(std::FILE* file, std::vector<std::byte> data_xor={});
~AutoFile() { fclose(); }
~AutoFile()
{
if (m_was_written) {
// Callers that wrote to the file must have closed it explicitly
// with the fclose() method and checked that the close succeeded.
// This is because here in the destructor we have no way to signal
// errors from fclose() which, after write, could mean the file is
// corrupted and must be handled properly at the call site.
// Destructors in C++ cannot signal an error to the callers because
// they do not return a value and are not allowed to throw exceptions.
Assume(IsNull());
}
if (fclose() != 0) {
LogError("Failed to close file: %s", SysErrorString(errno));
}
}
// Disallow copies
AutoFile(const AutoFile&) = delete;
@@ -406,7 +432,7 @@ public:
bool feof() const { return std::feof(m_file); }
int fclose()
[[nodiscard]] int fclose()
{
if (auto rel{release()}) return std::fclose(rel);
return 0;