Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make (Read/Write)BinaryFile work with char vector, use AutoFile #29229

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ BITCOIN_TESTS =\
test/raii_event_tests.cpp \
test/random_tests.cpp \
test/rbf_tests.cpp \
test/readwritefile_tests.cpp \
Sjors marked this conversation as resolved.
Show resolved Hide resolved
test/rest_tests.cpp \
test/result_tests.cpp \
test/reverselock_tests.cpp \
Expand Down
6 changes: 3 additions & 3 deletions src/i2p.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -434,9 +434,9 @@ void Session::CreateIfNotCreatedAlready()
} else {
// Read our persistent destination (private key) from disk or generate
// one and save it to disk. Then use it when creating the session.
const auto& [read_ok, data] = ReadBinaryFile(m_private_key_file);
if (read_ok) {
m_private_key.assign(data.begin(), data.end());
std::optional<std::string> data{ReadBinaryFile<std::string>(m_private_key_file)};
if (data) {
m_private_key.assign(data->begin(), data->end());
} else {
GenerateAndSavePrivateKey(*sock);
}
Expand Down
86 changes: 86 additions & 0 deletions src/test/readwritefile_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) 2024 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <common/args.h>
#include <test/util/setup_common.h>
#include <util/readwritefile.h>

#include <fstream>
#include <optional>
#include <stdint.h>
#include <string.h>
#include <vector>

#include <boost/test/unit_test.hpp>

BOOST_FIXTURE_TEST_SUITE(readwritefile_tests, BasicTestingSetup)

BOOST_AUTO_TEST_CASE(util_ReadBinaryFile)
{
fs::path tmpfolder = m_args.GetDataDirBase();
fs::path tmpfile = tmpfolder / "read_binary.dat";
std::string expected_text;
for (int i = 0; i < 30; i++) {
expected_text += "0123456789";
}
{
std::ofstream file{tmpfile};
file << expected_text;
}
{
// read all contents in file
auto text{ReadBinaryFile<std::string>(tmpfile)};
BOOST_REQUIRE(text);
BOOST_CHECK_EQUAL(text.value(), expected_text);
}
{
// read half contents in file
auto text{ReadBinaryFile<std::string>(tmpfile, expected_text.size() / 2)};
BOOST_REQUIRE(text);
BOOST_CHECK_EQUAL(text.value(), expected_text.substr(0, expected_text.size() / 2));
}
{
// read from non-existent file
fs::path invalid_file = tmpfolder / "invalid_binary.dat";
auto text{ReadBinaryFile<std::string>(invalid_file)};
BOOST_REQUIRE(!text);
}
{
std::vector<unsigned char> expected_data;
for (int i = 0; i < 30; i++) {
// store result as bytes (ASCII "0" is character 48)
expected_data.insert(expected_data.end(), {48,49,50,51,52,53,54,55,56,57});
}
auto data{ReadBinaryFile<std::vector<unsigned char>>(tmpfile)};
BOOST_REQUIRE(data);
BOOST_REQUIRE(data.value() == expected_data);
}
}

BOOST_AUTO_TEST_CASE(util_WriteBinaryFile)
{
fs::path tmpfolder = m_args.GetDataDirBase();
fs::path tmpfile = tmpfolder / "write_binary.dat";
std::string expected_text = "bitcoin";
{
auto valid = WriteBinaryFile(tmpfile, expected_text);
BOOST_CHECK(valid);
std::string actual_text;
std::ifstream file{tmpfile};
file >> actual_text;
BOOST_CHECK_EQUAL(actual_text, expected_text);
}
{
std::vector<unsigned char> bytes{98, 105, 116, 99, 111, 105, 110}; // "bitcoin"
auto valid = WriteBinaryFile(tmpfile, bytes);
BOOST_CHECK(valid);
std::string actual_text;
std::ifstream file{tmpfile};
file >> actual_text;
BOOST_CHECK_EQUAL(actual_text, expected_text);
}

}

BOOST_AUTO_TEST_SUITE_END()
47 changes: 0 additions & 47 deletions src/test/util_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
#include <util/message.h> // For MessageSign(), MessageVerify(), MESSAGE_MAGIC
#include <util/moneystr.h>
#include <util/overflow.h>
#include <util/readwritefile.h>
#include <util/spanparsing.h>
#include <util/strencodings.h>
#include <util/string.h>
Expand Down Expand Up @@ -1745,52 +1744,6 @@ BOOST_AUTO_TEST_CASE(util_ParseByteUnits)
BOOST_CHECK(!ParseByteUnits("1x", noop));
}

BOOST_AUTO_TEST_CASE(util_ReadBinaryFile)
{
fs::path tmpfolder = m_args.GetDataDirBase();
fs::path tmpfile = tmpfolder / "read_binary.dat";
std::string expected_text;
for (int i = 0; i < 30; i++) {
expected_text += "0123456789";
}
{
std::ofstream file{tmpfile};
file << expected_text;
}
{
// read all contents in file
auto [valid, text] = ReadBinaryFile(tmpfile);
BOOST_CHECK(valid);
BOOST_CHECK_EQUAL(text, expected_text);
}
{
// read half contents in file
auto [valid, text] = ReadBinaryFile(tmpfile, expected_text.size() / 2);
BOOST_CHECK(valid);
BOOST_CHECK_EQUAL(text, expected_text.substr(0, expected_text.size() / 2));
}
{
// read from non-existent file
fs::path invalid_file = tmpfolder / "invalid_binary.dat";
auto [valid, text] = ReadBinaryFile(invalid_file);
BOOST_CHECK(!valid);
BOOST_CHECK(text.empty());
}
}

BOOST_AUTO_TEST_CASE(util_WriteBinaryFile)
{
fs::path tmpfolder = m_args.GetDataDirBase();
fs::path tmpfile = tmpfolder / "write_binary.dat";
std::string expected_text = "bitcoin";
auto valid = WriteBinaryFile(tmpfile, expected_text);
std::string actual_text;
std::ifstream file{tmpfile};
file >> actual_text;
BOOST_CHECK(valid);
BOOST_CHECK_EQUAL(actual_text, expected_text);
}

BOOST_AUTO_TEST_CASE(clearshrink_test)
{
{
Expand Down
16 changes: 8 additions & 8 deletions src/torcontrol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,10 @@ TorController::TorController(struct event_base* _base, const std::string& tor_co
LogPrintf("tor: Initiating connection to Tor control port %s failed\n", m_tor_control_center);
}
// Read service private key if cached
std::pair<bool,std::string> pkf = ReadBinaryFile(GetPrivateKeyFile());
if (pkf.first) {
auto data{ReadBinaryFile<std::string>(GetPrivateKeyFile())};
if (data) {
LogPrint(BCLog::TOR, "Reading cached private key from %s\n", fs::PathToString(GetPrivateKeyFile()));
private_key = pkf.second;
private_key = data.value();
}
}

Expand Down Expand Up @@ -586,15 +586,15 @@ void TorController::protocolinfo_cb(TorControlConnection& _conn, const TorContro
} else if (methods.count("SAFECOOKIE")) {
// Cookie: hexdump -e '32/1 "%02x""\n"' ~/.tor/control_auth_cookie
LogPrint(BCLog::TOR, "Using SAFECOOKIE authentication, reading cookie authentication from %s\n", cookiefile);
std::pair<bool,std::string> status_cookie = ReadBinaryFile(fs::PathFromString(cookiefile), TOR_COOKIE_SIZE);
if (status_cookie.first && status_cookie.second.size() == TOR_COOKIE_SIZE) {
// _conn.Command("AUTHENTICATE " + HexStr(status_cookie.second), std::bind(&TorController::auth_cb, this, std::placeholders::_1, std::placeholders::_2));
cookie = std::vector<uint8_t>(status_cookie.second.begin(), status_cookie.second.end());
auto status_cookie = ReadBinaryFile<std::string>(fs::PathFromString(cookiefile), TOR_COOKIE_SIZE);
if (status_cookie && status_cookie->size() == TOR_COOKIE_SIZE) {
// _conn.Command("AUTHENTICATE " + HexStr(status_cookie), std::bind(&TorController::auth_cb, this, std::placeholders::_1, std::placeholders::_2));
cookie = std::vector<uint8_t>(status_cookie->begin(), status_cookie->end());
clientNonce = std::vector<uint8_t>(TOR_NONCE_SIZE, 0);
GetRandBytes(clientNonce);
_conn.Command("AUTHCHALLENGE SAFECOOKIE " + HexStr(clientNonce), std::bind(&TorController::authchallenge_cb, this, std::placeholders::_1, std::placeholders::_2));
} else {
if (status_cookie.first) {
if (status_cookie) {
LogPrintf("tor: Authentication cookie %s is not exactly %i bytes, as is required by the spec\n", cookiefile, TOR_COOKIE_SIZE);
} else {
LogPrintf("tor: Authentication cookie %s could not be opened (check permissions)\n", cookiefile);
Expand Down
53 changes: 26 additions & 27 deletions src/util/readwritefile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,45 @@

#include <util/readwritefile.h>

#include <streams.h>
#include <util/fs.h>

#include <algorithm>
#include <cstdio>
#include <limits>
#include <string>
#include <utility>
#include <vector>

std::pair<bool,std::string> ReadBinaryFile(const fs::path &filename, size_t maxsize)
template <typename T>
std::optional<T> ReadBinaryFile(const fs::path& filename, size_t maxsize)
{
FILE *f = fsbridge::fopen(filename, "rb");
if (f == nullptr)
return std::make_pair(false,"");
std::string retval;
char buffer[128];
do {
const size_t n = fread(buffer, 1, std::min(sizeof(buffer), maxsize - retval.size()), f);
// Check for reading errors so we don't return any data if we couldn't
// read the entire file (or up to maxsize)
if (ferror(f)) {
fclose(f);
return std::make_pair(false,"");
}
retval.append(buffer, buffer+n);
} while (!feof(f) && retval.size() < maxsize);
fclose(f);
return std::make_pair(true,retval);
std::FILE *f = fsbridge::fopen(filename, "rb");
if (f == nullptr) return {};
T output{};
size_t file_size = fs::file_size(filename);
output.resize(std::min(file_size, maxsize));
try {
AutoFile{f} >> Span{output};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked more the previous version which called fread(3) here. It was simple stupid. This >> is now hard to follow, especially given that it depends on T. For vector it ends up calling AutoFile::detail_fread(). It does not check whether ferror(3) occurred.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For vector it ends up calling AutoFile::detail_fread(). It does not check whether ferror(3) occurred.

It does. If the return value of detail_fread is not output.size(), operator>> will fail.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not check whether ferror(3) occurred.

It does.

Where? There is no ferror(3) call. From the man page: "The function fread() does not distinguish between end-of-file and error, and callers must use feof(3) and ferror(3) to determine which occurred."

If the return value of detail_fread is not output.size(), operator>> will fail.

Yes, but it can be equal to the desired size under two conditions: eof, or error. The previous code distinguished between both.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the return value of detail_fread is not output.size(), operator>> will fail.

Yes, but it can be equal to the desired size under two conditions: eof, or error.

No, the eof-error would only be raised if read past the desired size, not to it. Unless I am missing something?

I am asking, because if there was a bug, it should be fixed, or at least an issue should be filed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That "must" in the man page is pretty clear: "callers must use feof(3) and ferror(3) to determine which occurred". A ferror(3) check can't hurt and it is better to have an extra check that always returns "no error" than a missing check, failing to detect an IO error. The previous code was doing that - a dumb fread(3) followed by an unconditional ferror(3).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is called to determine which error occurred, see

throw std::ios_base::failure(feof() ? "AutoFile::read: end of file" : "AutoFile::read: fread failed");

Again, if there is a bug in the current code in master, it should be fixed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would extend the check to:

 void AutoFile::read(Span<std::byte> dst)
 {
-    if (detail_fread(dst) != dst.size()) {
+    if (detail_fread(dst) != dst.size() || std::ferror(m_file)) {
         throw std::ios_base::failure(feof() ? "AutoFile::read: end of file" : "AutoFile::read: fread failed");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there is a bug in the current code in master, it should be fixed.

Done in #29307

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad something useful came out of this PR :-)

} catch (const std::ios_base::failure&) {
return {};
}
return output;
}

bool WriteBinaryFile(const fs::path &filename, const std::string &data)
template std::optional<std::string> ReadBinaryFile(const fs::path &filename, size_t maxsize);
template std::optional<std::vector<unsigned char>> ReadBinaryFile(const fs::path &filename, size_t maxsize);

template <typename T>
bool WriteBinaryFile(const fs::path& filename, const T& data)
{
FILE *f = fsbridge::fopen(filename, "wb");
if (f == nullptr)
return false;
if (fwrite(data.data(), 1, data.size(), f) != data.size()) {
fclose(f);
return false;
}
if (fclose(f) != 0) {
try {
AutoFile{fsbridge::fopen(filename, "wb")} << Span{data};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, the dumb version was easier to follow. The new one does not check whether fclose(3) failed, but it should. I think that is a serious deficiency in AutoFile itself.

fwrite(3) may succeed, but if a subsequent fclose(3) fails we should consider the data did not make it safely to disk and that the file is corrupted (fclose(3) writes any buffered data to disk using fflush(3), so a failure at fclose(3) is as bad as failure at fwrite(3)).

Copy link
Contributor

@vasild vasild Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is a serious deficiency in AutoFile itself

Logged as #29307

Copy link
Member Author

@Sjors Sjors Jan 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vasild I would also prefer to fix things in AutoFile, but perhaps add a few comments to explain what happens under the hood?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could happen:

  1. fwrite() succeeds, returns ok the caller, some of the data is buffered in the OS and not yet on disk
  2. fclose() is called, it tries to fflush() the buffered data to disk but fails due to IO error. The caller ignores the error returned by fclose()
  3. the program continues with the wrong assumption that the data is safely on disk

Copy link
Member Author

@Sjors Sjors Jan 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not good. I guess we also don't want to sync to disk, and block for that to complete, for every field that's >>'d to a file though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. That's why I don't see a good solution. It is a design issue with AutoFile to flush/close from the destructor which can't signal the failure to the caller.

} catch (const std::ios_base::failure&) {
return false;
}
return true;
}

template bool WriteBinaryFile(const fs::path& filename, const std::string& data);
template bool WriteBinaryFile(const fs::path& filename, const std::vector<unsigned char>& data);
22 changes: 14 additions & 8 deletions src/util/readwritefile.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,27 @@
#include <util/fs.h>

#include <limits>
#include <optional>
#include <string>
#include <utility>

/** Read full contents of a file and return them in a std::string.
* Returns a pair <status, string>.
* If an error occurred, status will be false, otherwise status will be true and the data will be returned in string.
/**
* Read full contents of a file and return one of the following formats:
* 1. std::vector<unsigned char>
* 2. std::string
*
* @param maxsize Puts a maximum size limit on the file that is read. If the file is larger than this, truncated data
* (with len > maxsize) will be returned.
* @param[in] filename Filename. Returns false it doesn't exist.
* @param[in] maxsize Puts a maximum size limit on the file that is read. If the file
* is larger than this, truncated data (with len > maxsize) will be returned.
* @return result if successful, std::nullopt otherwise
*/
std::pair<bool,std::string> ReadBinaryFile(const fs::path &filename, size_t maxsize=std::numeric_limits<size_t>::max());
template<class T>
std::optional<T> ReadBinaryFile(const fs::path& filename, size_t maxsize=std::numeric_limits<size_t>::max());

/** Write contents of std::string to a file.
/** Write contents of std::string or std::vector<unsigned char> to a file.
* @return true on success.
*/
bool WriteBinaryFile(const fs::path &filename, const std::string &data);
template <class T>
bool WriteBinaryFile(const fs::path& filename, const T& data);

#endif // BITCOIN_UTIL_READWRITEFILE_H