From 529a09267f6f4b1f3ac0ab11864ef452be92dd10 Mon Sep 17 00:00:00 2001 From: DavisVaughan Date: Tue, 19 Oct 2021 09:45:53 -0400 Subject: [PATCH] Allow `set_install()` on Windows to use a file path containing Unicode Anywhere we utilize that install file path, we carefully convert it to UTF-16 and utilize wide character helpers for file manipulation --- src/tz.cpp | 155 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 139 insertions(+), 16 deletions(-) diff --git a/src/tz.cpp b/src/tz.cpp index fc8dcce..0006f42 100644 --- a/src/tz.cpp +++ b/src/tz.cpp @@ -198,6 +198,35 @@ namespace using co_task_mem_ptr = std::unique_ptr; } +static +std::wstring +convert_utf8_to_utf16(const std::string& s) +{ + std::wstring out; + const int size = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, NULL, 0); + + if (size == 0) + { + std::string msg = "Failed to determine required size when converting \""; + msg += s; + msg += "\" to UTF-16."; + throw std::runtime_error(msg); + } + + out.resize(size); + const int check = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, &out[0], size); + + if (size != check) + { + std::string msg = "Failed to convert \""; + msg += s; + msg += "\" to UTF-16."; + throw std::runtime_error(msg); + } + + return out; +} + // We might need to know certain locations even if not using the remote API, // so keep these routines out of that block for now. static @@ -267,6 +296,89 @@ get_download_folder() # endif // !_WIN32 +/* + * This class is provided to mimic the following usage of `ifstream`: + * + * std::ifstream is(filename); + * + * file_streambuf ibuf(filename); + * std::istream is(&ibuf); + * + * This is required because `ifstream` does not support opening files + * containing wide characters on Windows. On Windows, `file_streambuf` uses + * `file_open()` to convert the file name to UTF-16 before opening it with + * `_wfopen()`. + * + * Note that this is not an exact re-implementation of `ifstream`, + * but is enough for usage here. + * + * It is partially based on these two implementations: + * - fdinbuf from http://www.josuttis.com/cppcode/fdstream.html + * - stdiobuf https://stackoverflow.com/questions/12342542/convert-file-to-ifstream-c-android-ndk + * + * Apparently MSVC provides non-standard overloads of `ifstream` that support + * a `const wchar_t*` file name, but MinGW does not https://stackoverflow.com/a/822032 + */ +class file_streambuf + : public std::streambuf +{ +private: + FILE* file_; + static const int buffer_size_ = 1024; + char buffer_[buffer_size_]; + +public: + ~file_streambuf() + { + if (file_) + { + ::fclose(file_); + } + } + file_streambuf(const file_streambuf&) = delete; + file_streambuf& operator=(const file_streambuf&) = delete; + + file_streambuf(const std::string& filename) + : file_(file_open(filename)) + { + } + +protected: + virtual + int_type + underflow() + { + if (gptr() == egptr() && file_) + { + const size_t size = ::fread(buffer_, 1, buffer_size_, file_); + setg(buffer_, buffer_, buffer_ + size); + } + return (gptr() == egptr()) + ? traits_type::eof() + : traits_type::to_int_type(*gptr()); + } + +private: + FILE* + file_open(const std::string& filename) + { +# ifdef _WIN32 + std::wstring wfilename = convert_utf8_to_utf16(filename); + FILE* file = ::_wfopen(wfilename.c_str(), L"rb"); +# else // !_WIN32 + FILE* file = ::fopen(filename.c_str(), "rb"); +# endif // _WIN32 + if (file == NULL) + { + std::string msg = "Error opening file \""; + msg += filename; + msg += "\"."; + throw std::runtime_error(msg); + } + return file; + } +}; + #endif // !USE_OS_TZDB namespace date @@ -559,15 +671,8 @@ load_timezone_mappings_from_xml_file(const std::string& input_path) std::vector mappings; std::string line; - std::ifstream is(input_path); - if (!is.is_open()) - { - // We don't emit file exceptions because that's an implementation detail. - std::string msg = "Error opening time zone mapping file \""; - msg += input_path; - msg += "\"."; - throw std::runtime_error(msg); - } + file_streambuf ibuf(input_path); + std::istream is(&ibuf); auto error = [&input_path, &line_num](const char* info) { @@ -697,7 +802,6 @@ load_timezone_mappings_from_xml_file(const std::string& input_path) } } - is.close(); return mappings; } @@ -2836,7 +2940,8 @@ bool file_exists(const std::string& filename) { #ifdef _WIN32 - return ::_access(filename.c_str(), 0) == 0; + std::wstring wfilename = convert_utf8_to_utf16(filename); + return ::_waccess(wfilename.c_str(), 0) == 0; #else return ::access(filename.c_str(), F_OK) == 0; #endif @@ -3413,16 +3518,27 @@ std::string get_version(const std::string& path) { std::string version; - std::ifstream infile(path + "version"); - if (infile.is_open()) + + std::string path_version = path + "version"; + + if (file_exists(path_version)) { + file_streambuf inbuf(path_version); + std::istream infile(&inbuf); + infile >> version; + if (!infile.fail()) return version; } - else + + std::string path_news = path + "NEWS"; + + if (file_exists(path_news)) { - infile.open(path + "NEWS"); + file_streambuf inbuf(path_news); + std::istream infile(&inbuf); + while (infile) { infile >> version; @@ -3433,6 +3549,7 @@ get_version(const std::string& path) } } } + throw std::runtime_error("Unable to get Timezone database version from " + path); } @@ -3504,7 +3621,13 @@ init_tzdb() for (const auto& filename : files) { - std::ifstream infile(path + filename); + std::string file_path = path + filename; + if (!file_exists(file_path)) + { + continue; + } + file_streambuf inbuf(file_path); + std::istream infile(&inbuf); while (infile) { std::getline(infile, line);