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
This commit is contained in:
DavisVaughan
2021-10-19 09:45:53 -04:00
committed by Howard Hinnant
parent d9049ee697
commit 529a09267f

View File

@@ -198,6 +198,35 @@ namespace
using co_task_mem_ptr = std::unique_ptr<wchar_t[], task_mem_deleter>;
}
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<detail::timezone_mapping> 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);