diff --git a/src/Makefile.test.include b/src/Makefile.test.include --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -121,6 +121,7 @@ test/feerate_tests.cpp \ test/finalization_tests.cpp \ test/flatfile_tests.cpp \ + test/fs_tests.cpp \ test/getarg_tests.cpp \ test/hash_tests.cpp \ test/inv_tests.cpp \ diff --git a/src/fs.h b/src/fs.h --- a/src/fs.h +++ b/src/fs.h @@ -7,6 +7,9 @@ #include #include +#if defined WIN32 && defined __GLIBCXX__ +#include +#endif #include #include @@ -40,6 +43,61 @@ }; std::string get_filesystem_error_message(const fs::filesystem_error &e); -}; // namespace fsbridge + +// GNU libstdc++ specific workaround for opening UTF-8 paths on Windows. +// +// On Windows, it is only possible to reliably access multibyte file paths +// through `wchar_t` APIs, not `char` APIs. But because the C++ standard doesn't +// require ifstream/ofstream `wchar_t` constructors, and the GNU library doesn't +// provide them (in contrast to the Microsoft C++ library, see +// https://stackoverflow.com/questions/821873/how-to-open-an-stdfstream-ofstream-or-ifstream-with-a-unicode-filename/822032#822032), +// Boost is forced to fall back to `char` constructors which may not work +// properly. +// +// Work around this issue by creating stream objects with `_wfopen` in +// combination with `__gnu_cxx::stdio_filebuf`. This workaround can be removed +// with an upgrade to C++17, where streams can be constructed directly from +// `std::filesystem::path` objects. + +#if defined WIN32 && defined __GLIBCXX__ +class ifstream : public std::istream { +public: + ifstream() = default; + explicit ifstream(const fs::path &p, + std::ios_base::openmode mode = std::ios_base::in) { + open(p, mode); + } + ~ifstream() { close(); } + void open(const fs::path &p, + std::ios_base::openmode mode = std::ios_base::in); + bool is_open() { return m_filebuf.is_open(); } + void close(); + +private: + __gnu_cxx::stdio_filebuf m_filebuf; + FILE *m_file = nullptr; +}; +class ofstream : public std::ostream { +public: + ofstream() = default; + explicit ofstream(const fs::path &p, + std::ios_base::openmode mode = std::ios_base::out) { + open(p, mode); + } + ~ofstream() { close(); } + void open(const fs::path &p, + std::ios_base::openmode mode = std::ios_base::out); + bool is_open() { return m_filebuf.is_open(); } + void close(); + +private: + __gnu_cxx::stdio_filebuf m_filebuf; + FILE *m_file = nullptr; +}; +#else // !(WIN32 && __GLIBCXX__) +typedef fs::ifstream ifstream; +typedef fs::ofstream ofstream; +#endif // WIN32 && __GLIBCXX__ +}; // namespace fsbridge #endif // BITCOIN_FS_H diff --git a/src/fs.cpp b/src/fs.cpp --- a/src/fs.cpp +++ b/src/fs.cpp @@ -114,4 +114,107 @@ #endif } +#ifdef WIN32 +#ifdef __GLIBCXX__ + +// reference: +// https://github.com/gcc-mirror/gcc/blob/gcc-7_3_0-release/libstdc%2B%2B-v3/include/std/fstream#L270 + +static std::string openmodeToStr(std::ios_base::openmode mode) { + switch (mode & ~std::ios_base::ate) { + case std::ios_base::out: + case std::ios_base::out | std::ios_base::trunc: + return "w"; + case std::ios_base::out | std::ios_base::app: + case std::ios_base::app: + return "a"; + case std::ios_base::in: + return "r"; + case std::ios_base::in | std::ios_base::out: + return "r+"; + case std::ios_base::in | std::ios_base::out | std::ios_base::trunc: + return "w+"; + case std::ios_base::in | std::ios_base::out | std::ios_base::app: + case std::ios_base::in | std::ios_base::app: + return "a+"; + case std::ios_base::out | std::ios_base::binary: + case std::ios_base::out | std::ios_base::trunc | std::ios_base::binary: + return "wb"; + case std::ios_base::out | std::ios_base::app | std::ios_base::binary: + case std::ios_base::app | std::ios_base::binary: + return "ab"; + case std::ios_base::in | std::ios_base::binary: + return "rb"; + case std::ios_base::in | std::ios_base::out | std::ios_base::binary: + return "r+b"; + case std::ios_base::in | std::ios_base::out | std::ios_base::trunc | + std::ios_base::binary: + return "w+b"; + case std::ios_base::in | std::ios_base::out | std::ios_base::app | + std::ios_base::binary: + case std::ios_base::in | std::ios_base::app | std::ios_base::binary: + return "a+b"; + default: + return std::string(); + } +} + +void ifstream::open(const fs::path &p, std::ios_base::openmode mode) { + close(); + mode |= std::ios_base::in; + m_file = fsbridge::fopen(p, openmodeToStr(mode).c_str()); + if (m_file == nullptr) { + return; + } + m_filebuf = __gnu_cxx::stdio_filebuf(m_file, mode); + rdbuf(&m_filebuf); + if (mode & std::ios_base::ate) { + seekg(0, std::ios_base::end); + } +} + +void ifstream::close() { + if (m_file != nullptr) { + m_filebuf.close(); + fclose(m_file); + } + m_file = nullptr; +} + +void ofstream::open(const fs::path &p, std::ios_base::openmode mode) { + close(); + mode |= std::ios_base::out; + m_file = fsbridge::fopen(p, openmodeToStr(mode).c_str()); + if (m_file == nullptr) { + return; + } + m_filebuf = __gnu_cxx::stdio_filebuf(m_file, mode); + rdbuf(&m_filebuf); + if (mode & std::ios_base::ate) { + seekp(0, std::ios_base::end); + } +} + +void ofstream::close() { + if (m_file != nullptr) { + m_filebuf.close(); + fclose(m_file); + } + m_file = nullptr; +} +#else // __GLIBCXX__ + +static_assert( + sizeof(*fs::path().BOOST_FILESYSTEM_C_STR) == sizeof(wchar_t), + "Warning: This build is using boost::filesystem ofstream and ifstream " + "implementations which will fail to open paths containing multibyte " + "characters. You should delete this static_assert to ignore this warning, " + "or switch to a different C++ standard library like the Microsoft C++ " + "Standard Library (where boost uses non-standard extensions to construct " + "stream objects with wide filenames), or the GNU libstdc++ library (where " + "a more complicated workaround has been implemented above)."); + +#endif // __GLIBCXX__ +#endif // WIN32 + } // namespace fsbridge diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -410,7 +410,7 @@ GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME)); /* Create the file */ - fs::ofstream configFile(pathConfig, std::ios_base::app); + fsbridge::ofstream configFile(pathConfig, std::ios_base::app); if (!configFile.good()) { return false; @@ -674,7 +674,7 @@ } bool GetStartOnSystemStartup() { - fs::ifstream optionFile(GetAutostartFilePath()); + fsbridge::ifstream optionFile(GetAutostartFilePath()); if (!optionFile.good()) { return false; } @@ -706,8 +706,8 @@ fs::create_directories(GetAutostartDir()); - fs::ofstream optionFile(GetAutostartFilePath(), - std::ios_base::out | std::ios_base::trunc); + fsbridge::ofstream optionFile( + GetAutostartFilePath(), std::ios_base::out | std::ios_base::trunc); if (!optionFile.good()) { return false; } diff --git a/src/rpc/protocol.cpp b/src/rpc/protocol.cpp --- a/src/rpc/protocol.cpp +++ b/src/rpc/protocol.cpp @@ -13,7 +13,6 @@ #include #include -#include /** * JSON-RPC protocol. Bitcoin speaks version 1.0 for maximum compatibility, but @@ -85,9 +84,9 @@ /** the umask determines what permissions are used to create this file - * these are set to 077 in init.cpp unless overridden with -sysperms. */ - std::ofstream file; + fsbridge::ofstream file; fs::path filepath_tmp = GetAuthCookieFile(true); - file.open(filepath_tmp.string().c_str()); + file.open(filepath_tmp); if (!file.is_open()) { LogPrintf("Unable to open cookie authentication file %s for writing\n", filepath_tmp.string()); @@ -111,10 +110,10 @@ } bool GetAuthCookie(std::string *cookie_out) { - std::ifstream file; + fsbridge::ifstream file; std::string cookie; fs::path filepath = GetAuthCookieFile(); - file.open(filepath.string().c_str()); + file.open(filepath); if (!file.is_open()) { return false; } diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -127,6 +127,7 @@ feerate_tests.cpp finalization_tests.cpp flatfile_tests.cpp + fs_tests.cpp getarg_tests.cpp hash_tests.cpp inv_tests.cpp diff --git a/src/test/fs_tests.cpp b/src/test/fs_tests.cpp new file mode 100644 --- /dev/null +++ b/src/test/fs_tests.cpp @@ -0,0 +1,59 @@ +// Copyright (c) 2011-2019 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 + +#include + +#include + +BOOST_FIXTURE_TEST_SUITE(fs_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(fsbridge_fstream) { + fs::path tmpfolder = SetDataDir("fsbridge_fstream"); + // tmpfile1 should be the same as tmpfile2 + fs::path tmpfile1 = tmpfolder / "fs_tests_₿_🏃"; + fs::path tmpfile2 = tmpfolder / L"fs_tests_₿_🏃"; + { + fsbridge::ofstream file(tmpfile1); + file << "bitcoin"; + } + { + fsbridge::ifstream file(tmpfile2); + std::string input_buffer; + file >> input_buffer; + BOOST_CHECK_EQUAL(input_buffer, "bitcoin"); + } + { + fsbridge::ifstream file(tmpfile1, + std::ios_base::in | std::ios_base::ate); + std::string input_buffer; + file >> input_buffer; + BOOST_CHECK_EQUAL(input_buffer, ""); + } + { + fsbridge::ofstream file(tmpfile2, + std::ios_base::out | std::ios_base::app); + file << "tests"; + } + { + fsbridge::ifstream file(tmpfile1); + std::string input_buffer; + file >> input_buffer; + BOOST_CHECK_EQUAL(input_buffer, "bitcointests"); + } + { + fsbridge::ofstream file(tmpfile2, + std::ios_base::out | std::ios_base::trunc); + file << "bitcoin"; + } + { + fsbridge::ifstream file(tmpfile1); + std::string input_buffer; + file >> input_buffer; + BOOST_CHECK_EQUAL(input_buffer, "bitcoin"); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/util/system.cpp b/src/util/system.cpp --- a/src/util/system.cpp +++ b/src/util/system.cpp @@ -963,7 +963,7 @@ } const std::string confPath = GetArg("-conf", BITCOIN_CONF_FILENAME); - fs::ifstream stream(GetConfigFile(confPath)); + fsbridge::ifstream stream(GetConfigFile(confPath)); // ok to not have a config file if (stream.good()) { @@ -1001,7 +1001,7 @@ } for (const std::string &to_include : includeconf) { - fs::ifstream include_config(GetConfigFile(to_include)); + fsbridge::ifstream include_config(GetConfigFile(to_include)); if (include_config.good()) { if (!ReadConfigStream(include_config, error, ignore_invalid_keys)) { diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -24,7 +24,6 @@ #include #include -#include static std::string EncodeDumpString(const std::string &str) { std::stringstream ret; @@ -640,9 +639,8 @@ EnsureWalletIsUnlocked(pwallet); - std::ifstream file; - file.open(request.params[0].get_str().c_str(), - std::ios::in | std::ios::ate); + fsbridge::ifstream file; + file.open(request.params[0].get_str(), std::ios::in | std::ios::ate); if (!file.is_open()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file"); @@ -897,8 +895,8 @@ "move it out of the way first"); } - std::ofstream file; - file.open(filepath.string().c_str()); + fsbridge::ofstream file; + file.open(filepath); if (!file.is_open()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file"); diff --git a/src/wallet/walletutil.cpp b/src/wallet/walletutil.cpp --- a/src/wallet/walletutil.cpp +++ b/src/wallet/walletutil.cpp @@ -36,7 +36,7 @@ return false; } - fs::ifstream file(path.string(), std::ios::binary); + fsbridge::ifstream file(path, std::ios::binary); if (!file.is_open()) { return false; }