From 55087c6cbdf156a69c57fd9935c567eefc4232a7 Mon Sep 17 00:00:00 2001 From: gm Date: Tue, 14 Jun 2016 05:38:47 +1200 Subject: [PATCH] HAS_REMOTE_API support for Windows. Adds support for MingW too. * HAS_REMOTE_API still defaults to 0 * When HAS_REMOTE_API=0 only libraries advapi32.lib, ole32.lib and shell32.lib are required. * Setting HAS_REMOTE_API=1 requires curl: https://curl.haxx.se/libcurl/ *- Also need to manually install 7-Zip (http://www.7-zip.org). *- Will automatically install Tinyxml2 (http://www.grinninglizard.com/tinyxml2/). *- Will automatically install http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml --- date.h | 11 +- test/tz_test/validate.cpp | 1 - tz.cpp | 1058 ++++++++++++++++++++++++------------- tz.h | 52 +- 4 files changed, 733 insertions(+), 389 deletions(-) diff --git a/date.h b/date.h index 41aba4f..7f157ba 100644 --- a/date.h +++ b/date.h @@ -50,17 +50,20 @@ namespace date // MSVC's constexpr support is still a WIP, even in VS2015. // Fall back to a lesser mode to support it. // TODO: Remove this or retest later once MSVC's constexpr improves. -#if defined(_MSC_VER) +#if defined(_MSC_VER) && ! defined(__clang__) +// MS cl compiler. # define CONSTDATA const # define CONSTCD11 # define CONSTCD14 # define NOEXCEPT _NOEXCEPT #elif __cplusplus >= 201402 +// C++14 # define CONSTDATA constexpr # define CONSTCD11 constexpr # define CONSTCD14 constexpr # define NOEXCEPT noexcept #else +// C++11 # define CONSTDATA constexpr # define CONSTCD11 constexpr # define CONSTCD14 @@ -3826,14 +3829,16 @@ class time_of_day { using base = detail::time_of_day_storage; public: -#ifndef _MSC_VER +#if !(_MSC_VER && !defined(__clang__)) + // C++11 using base::base; #else + // MS cl compiler workaround. template explicit time_of_day(Args&& ...args) : base(std::forward(args)...) {} -#endif // _MSC_VER +#endif }; template ; using zoned_microseconds = zoned_time; - using local_microseconds = local_time; zoned_microseconds local{zone}; if (begin > sys_days{jan/1/1700}) diff --git a/tz.cpp b/tz.cpp index a55ffd2..9cb927f 100644 --- a/tz.cpp +++ b/tz.cpp @@ -25,6 +25,54 @@ // been invented (that woud involve another several millennia of evolution). // We did not mean to shout. +#if _WIN32 +// Windows.h will be included directly and indirectly (e.g. by curl). +// We need to define these macros to prevent Windows.h bringing in +// more than we need and do it eearly so Windows.h doesn't get included +// without these macros having been defined. +// min/max macrosinterfere with the C++ versions. +#ifndef NOMINMAX +#define NOMINMAX +#endif +// We don't need all that Windows has to offer. +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif // _WIN32 + +// None of this happens with the MS SDK (at least VS14 which I tested), but: +// Compiling with mingw, we get "error: 'KF_FLAG_DEFAULT' was not declared in this scope." +// and error: 'SHGetKnownFolderPath' was not declared in this scope.". +// It seems when using mingw NTDDI_VERSION is undefined and that +// causes KNOWN_FOLDER_FLAG and the KF_ flags to not get defined. +// So we must define NTDDI_VERSION to get those flags on mingw. +// The docs say though here: +// https://msdn.microsoft.com/en-nz/library/windows/desktop/aa383745(v=vs.85).aspx +// that "If you define NTDDI_VERSION, you must also define _WIN32_WINNT." +// So we declare we require Vista or greater. +#if __MINGW32__ + +#ifndef NTDDI_VERSION +#define NTDDI_VERSION 0x06000000 +#define _WIN32_WINNT _WIN32_WINNT_VISTA +#elif NTDDI_VERSION < 0x06000000 +#warning "If this fails to compile NTDDI_VERSION may be to low. See comments above." +#endif +// But once we define the values above we then get this linker error: +// "tz.cpp:(.rdata$.refptr.FOLDERID_Downloads[.refptr.FOLDERID_Downloads]+0x0): " +// "undefined reference to `FOLDERID_Downloads'" +// which #include cures see: +// https://support.microsoft.com/en-us/kb/130869 +#include +// But with included, the error moves on to: +// error: 'FOLDERID_Downloads' was not declared in this scope +// Which #include cures. +#include + +#endif // __MINGW32__ + +#include +#endif // _WIN32 + #include "tz_private.h" #include @@ -39,57 +87,92 @@ #include #include #include -#if HAS_REMOTE_API -#include -#endif -#ifdef _WIN32 + +#if _WIN32 #include #include -#endif - -#if TIMEZONE_MAPPING -// Timezone mapping maps native (e.g. Windows) timezone names to the "Standard" names -// used by this library. -// The mapping process parses a CSV file of mapping data where each line is of the format: -// "other","territory","type" -// e.g. "GMT Standard Time", "001", "Europe/London" -// and -// Windows typically uses CRLF, Linux/Unix/Mac OS use LF, and old Mac's use CR. -// On Linux/Mac, no mapping file is required as that is the native format already. -// C++11 should work but C++14 is preferred even there too -// because the date library in general works better with C++14. -#include -#endif +#endif // _WIN32 // unistd.h is used on some platforms as part of the the means to get // the current time zone. On Win32 Windows.h provides a means to do it. // gcc/mingw supports unistd.h on Win32 but MSVC does not. -#ifdef _WIN32 -// Prevent windows defining min/max macros that will interfere with C++ versions. -#ifndef NOMINMAX -#define NOMINMAX -#endif -// We don't need everything Windows.h has to offer. -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include -#include -#else -#include -#include +#if _WIN32 +# include // _unlink etc. +# include // CoTaskFree, ShGetKnownFolderPath etc. +# if HAS_REMOTE_API +# include // _mkdir +# include // ShFileOperation etc. +# endif // HAS_REMOTE_API +#else // !WIN32 +# include +# include +#endif // !WIN32 + +#if HAS_REMOTE_API +// Note curl includes windows.h so we must include curl AFTER definitions of things +// that effect windows.h such as NOMINMAX. +#include #endif -namespace date +#if TIMEZONE_MAPPING +// See comments in tz.h regarding the XML mapping file. +#include "tinyxml2.h" +#endif + +#if _WIN32 + +static CONSTDATA char folder_delimiter = '\\'; + +namespace { -// +---------------------+ -// | Begin Configuration | -// +---------------------+ + struct task_mem_deleter + { + void operator()(wchar_t buf[]) + { + if (buf != nullptr) + CoTaskMemFree(buf); + } + }; + using co_task_mem_ptr = std::unique_ptr; +} -#if _WIN32 // TODO: sensible default for all platforms. -static const std::string install{ "c:\\tzdata" }; -#else // !_WIN32 +// 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 +std::string +get_known_folder(const GUID& folderid) +{ + std::string folder; + PWSTR pfolder = nullptr; + HRESULT hr = SHGetKnownFolderPath(folderid, KF_FLAG_DEFAULT, NULL, &pfolder); + if (SUCCEEDED(hr)) + { + co_task_mem_ptr folder_ptr(pfolder); + folder = std::string(folder_ptr.get(), folder_ptr.get() + wcslen(folder_ptr.get())); + } + return folder; +} + +// Usually something like "c:\Program Files". +static +std::string +get_program_folder() +{ + return get_known_folder(FOLDERID_ProgramFiles); +} + +// Usually something like "c:\Users\username\Downloads". +static +std::string +get_download_folder() +{ + return get_known_folder(FOLDERID_Downloads); +} + +#else // !_WIN32 + +static CONSTDATA char folder_delimiter = '/'; static std::string @@ -103,10 +186,36 @@ expand_path(std::string path) return path; } -static const std::string install = expand_path("~/Downloads/tzdata"); - #endif // !_WIN32 +namespace date +{ +// +---------------------+ +// | Begin Configuration | +// +---------------------+ + +static std::string get_install() +{ +#if _WIN32 + std::string install = get_download_folder(); + install += folder_delimiter; + install += "tzdata"; +#else + std::string install = expand_path("~/Downloads/tzdata"); +#endif + return install; +} + +static const std::string install = get_install(); + +static +std::string +get_download_gz_file(const std::string& version) +{ + auto file = install + version + ".tar.gz"; + return file; +} + static const std::vector files = { "africa", "antarctica", "asia", "australasia", "backward", "etcetera", "europe", @@ -133,27 +242,162 @@ struct undocumented {explicit undocumented() = default;}; static_assert(min_year <= max_year, "Configuration error"); #endif -// Until filesystem arrives. -static CONSTDATA char folder_delimiter = -#ifdef _WIN32 -'\\'; -#else -'/'; -#endif +#if TIMEZONE_MAPPING -static bool file_exists(const std::string& filename) +namespace // Put types in an anonymous name space. { -#ifdef _WIN32 - return ::_access(filename.c_str(), 0) == 0; -#else - return ::access(filename.c_str(), F_OK) == 0; -#endif + +// A simple type to manage RAII for key handles and to +// implement the trivial registry interface we need. +// Not intended to be general-purpose. +class reg_key +{ +private: + // Note there is no value documented to be an invalid handle value. + // Not NULL nor INVALID_HANDLE_VALUE. We must rely on is_open. + HKEY m_key = nullptr; + bool m_is_open = false; +public: + ~reg_key() + { + close(); + } + + reg_key() = default; + reg_key(const reg_key&) = delete; + reg_key& operator=(const reg_key&) = delete; + + HKEY handle() + { + return m_key; + } + + bool is_open() const + { + return m_is_open; + } + + LONG open(const wchar_t* key_name) + { + LONG result; + result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ, &m_key); + if (result == ERROR_SUCCESS) + m_is_open = true; + return result; + } + + LONG close() + { + if (m_is_open) + { + auto result = RegCloseKey(m_key); + assert(result == ERROR_SUCCESS); + if (result == ERROR_SUCCESS) + { + m_is_open = false; + m_key = nullptr; + } + return result; + } + return ERROR_SUCCESS; + } + + // WARNING: this function is not a general-purpose function. + // It has a hard-coded value size limit that should be sufficient for our use cases. + bool get_string(const wchar_t* key_name, std::string& value) + { + value.clear(); + wchar_t value_buffer[256]; + // in/out parameter. Documentation say that size is a count of bytes not chars. + DWORD size = sizeof(value_buffer) - sizeof(value_buffer[0]); + DWORD tzi_type = REG_SZ; + if (RegQueryValueExW(handle(), key_name, nullptr, &tzi_type, + reinterpret_cast(value_buffer), &size) == ERROR_SUCCESS) + { + // Function does not guarantee to null terminate. + value_buffer[size/sizeof(value_buffer[0])] = L'\0'; + std::wstring_convert> converter; + value = converter.to_bytes(value_buffer); + return true; + } + return false; + } + + bool get_binary(const wchar_t* key_name, void* value, int value_size) + { + DWORD size = value_size; + DWORD type = REG_BINARY; + if (RegQueryValueExW(handle(), key_name, nullptr, &type, + reinterpret_cast(value), &size) == ERROR_SUCCESS + && (int) size == value_size) + return true; + return false; + } +}; + +} // anonymous namespace + +static +std::string +get_download_mapping_file(const std::string& version) +{ + auto file = install + version + "windowsZones.xml"; + return file; } -#ifdef _WIN32 -// Win32 support requires calling OS functions. -// This routine maps OS error codes to readable text strngs. -static std::string get_win32_message(DWORD error_code) +static +std::vector +load_zone_mappings_from_xml_file(const std::string& input_path) +{ + using tinyxml2::XMLDocument; + std::vector zone_map_list; + XMLDocument doc; + doc.LoadFile(input_path.c_str()); + auto supplementalData = doc.FirstChildElement("supplementalData"); + auto windowsZones = supplementalData->FirstChildElement("windowsZones"); + auto mapTimeZones = windowsZones->FirstChildElement("mapTimezones"); + + for (auto mapZone = mapTimeZones->FirstChildElement("mapZone"); + mapZone != nullptr; mapZone = mapZone->NextSiblingElement()) + { + auto other = mapZone->Attribute("other"); + auto territory = mapZone->Attribute("territory"); + auto type = mapZone->Attribute("type"); + zone_map_list.emplace_back(other, territory, type); + } + return zone_map_list; +} + +static +void +sort_zone_mappings(std::vector& mappings) +{ + std::sort(mappings.begin(), mappings.end(), + [](const date::detail::timezone_mapping& lhs, + const date::detail::timezone_mapping& rhs)->bool + { + auto other_result = lhs.other.compare(rhs.other); + if (other_result < 0) + return true; + else if (other_result == 0) + { + auto territory_result = lhs.territory.compare(rhs.territory); + if (territory_result < 0) + return true; + else if (territory_result == 9) + { + if (lhs.type < rhs.type) + return true; + } + } + return false; + }); +} + +// This routine maps Win32 OS error codes to readable text strngs. +static +std::string +get_win32_message(DWORD error_code) { struct free_message { void operator()(char buf[]) { @@ -180,104 +424,15 @@ static std::string get_win32_message(DWORD error_code) assert(message_buffer.get() != nullptr); return std::string(message_buffer.get()); } -#endif // _WIN32 - -#if TIMEZONE_MAPPING - -namespace // Put types in an anonymous name space. -{ - // A simple type to manage RAII for key handles and to - // implement the trivial registry interface we need. - // Not intended to be general-purpose. - class reg_key - { - private: - // Note there is no value documented to be an invalid handle value. - // Not NULL nor INVALID_HANDLE_VALUE. We must rely on is_open. - HKEY m_key = nullptr; - bool m_is_open = false; - public: - HKEY handle() - { - return m_key; - } - bool is_open() const - { - return m_is_open; - } - LONG open(const wchar_t* key_name) - { - LONG result; - result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, key_name, 0, KEY_READ, &m_key); - if (result == ERROR_SUCCESS) - m_is_open = true; - return result; - } - LONG close() - { - if (m_is_open) - { - auto result = RegCloseKey(m_key); - assert(result == ERROR_SUCCESS); - if (result == ERROR_SUCCESS) - { - m_is_open = false; - m_key = nullptr; - } - return result; - } - return ERROR_SUCCESS; - } - - // WARNING: this function has a hard-coded value size limit. - // It is not a general-purpose function. - // It should be sufficient for our use cases. - // The function could be made workable for any size string - // but we don't need the complexity of implementing that - // for our meagre purposes right now. - bool get_string(const wchar_t* key_name, std::string& value) - { - value.clear(); - wchar_t value_buffer[256]; - // in/out parameter. Documentation say that size is a count of bytes not chars. - DWORD size = sizeof(value_buffer) - sizeof(value_buffer[0]); - DWORD tzi_type = REG_SZ; - if (RegQueryValueExW(handle(), key_name, nullptr, &tzi_type, - reinterpret_cast(value_buffer), &size) == ERROR_SUCCESS) - { - // Function does not guarantee to null terminate. - value_buffer[size/sizeof(value_buffer[0])] = L'\0'; - std::wstring_convert> converter; - value = converter.to_bytes(value_buffer); - return true; - } - return false; - } - - bool get_binary(const wchar_t* key_name, void* value, int value_size) - { - DWORD size = value_size; - DWORD type = REG_BINARY; - if (RegQueryValueExW(handle(), key_name, nullptr, &type, - reinterpret_cast(value), &size) == ERROR_SUCCESS - && (int) size == value_size) - return true; - return false; - } - - ~reg_key() - { - close(); - } - }; -} // anonymous namespace // This function returns an exhaustive list of time zone information // from the Windows registry. // The routine tries to load as many time zone entries as possible despite errors. // We don't want to fail to load the whole database just because one record can't be read. -static void get_windows_timezone_info(std::vector& tz_list) +static +void +get_windows_timezone_info(std::vector& tz_list) { tz_list.clear(); LONG result; @@ -353,8 +508,9 @@ static void get_windows_timezone_info(std::vector& tz_lis // under the windows registry key Time Zones. // To be clear, standard_name does NOT represent a windows timezone id // or an IANA tzid -static const detail::timezone_info* find_native_timezone_by_standard_name( - const std::string& standard_name) +static +const detail::timezone_info* +find_native_timezone_by_standard_name(const std::string& standard_name) { // TODO! we can improve on linear search. const auto& native_zones = get_tzdb().native_zones; @@ -367,119 +523,8 @@ static const detail::timezone_info* find_native_timezone_by_standard_name( return nullptr; } -// Read CSV file of "other","territory","type". -// See timezone_mapping structure for more info. -// This function should be kept in sync with the code that writes this file. -static std::vector -load_timezone_mappings_from_csv_file(const std::string& input_path) -{ - size_t line = 0; - std::vector mappings; - std::ifstream is(input_path, -#if _WIN32 - std::ios_base::in // open in text mode on Win32, so getline will eat '\n' and '\r\n' -#else - std::ios_base::in | std::ios_base::binary -#endif - ); - 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); - } - - std::istringstream sis; - auto error = [&](const char* info) - { - std::string msg = "Error reading zone mapping file \""; - msg += input_path; - msg += "\" at line "; - msg += std::to_string(line); - msg += ": "; - msg += info; - throw std::runtime_error(msg); - }; - auto read_field_quote = [&]() - { - char field_delim; - sis.read(&field_delim, 1); - auto read_count = sis.gcount(); - if (sis.gcount() != 1 || field_delim != '"') - error("field '\"' expected."); - }; - auto read_field_delim = [&]() - { - char field_delim; - sis.read(&field_delim, 1); - if (sis.gcount() != 1 || field_delim != ',') - error("delimiter ',' expected."); - }; - std::string copyright; - bool blank = false; - for (;;) - { - std::getline(is, copyright); - ++line; // Make sure our line number is in sync with however many copyright lines we have. - if (is.eof()) - break; - if (copyright.empty()) - { - --line; - blank = true; - break; - } - } - const int min_copyright_lines = 3; - if (!blank || line < min_copyright_lines) - { - std::string msg = "Expected at least "; - msg += std::to_string(min_copyright_lines); - msg += " lines of copyright notice followed by a blank line."; - error(msg.c_str()); - } - ++line; - - std::string linebuf; - for (;;) - { - detail::timezone_mapping zm{}; - std::getline(is, linebuf); - // Stop on error or first blank line. - // linebuf.size() is the length read but it excludes the length of any line delimiter actually read (e.g. LF/CRLF). - // If linebuf.size() is 0 it *might* not mean nothing was read (error) or eof, it could just - // indicate only a line delimiter was read. Use is.eof() to distinquish end of file from a blank line. - if (linebuf.empty()) // on eof linebuf will be empty. - { - if (is.eof()) - break; - error("Formatting error. Blank lines not allowed."); - } - sis.clear(); - sis.str(linebuf); - - read_field_quote(); - std::getline(sis, zm.other, '\"'); - read_field_delim(); - - read_field_quote(); - std::getline(sis, zm.territory, '\"'); - read_field_delim(); - - read_field_quote(); - std::getline(sis, zm.type, '\"'); - if ((size_t)sis.tellg() != linebuf.length()) // Excess characters? We should have processed all in the line buffer. - error("Formatting error."); - ++line; - mappings.push_back(std::move(zm)); - } - is.close(); - return mappings; -} - -static bool +static +bool native_to_standard_timezone_name(const std::string& native_tz_name, std::string& standard_tz_name) { @@ -502,6 +547,7 @@ native_to_standard_timezone_name(const std::string& native_tz_name, } return false; } + #endif // TIMEZONE_MAPPING // Parsing helpers @@ -1984,6 +2030,17 @@ operator<<(std::ostream& os, const leap& x) return os << x.date_ << " +"; } +static +bool +file_exists(const std::string& filename) +{ +#if _WIN32 + return ::_access(filename.c_str(), 0) == 0; +#else + return ::access(filename.c_str(), F_OK) == 0; +#endif +} + #if HAS_REMOTE_API // CURL tools @@ -2004,41 +2061,83 @@ std::unique_ptr curl_init() { static const auto curl_is_now_initiailized = curl_global(); + (void)curl_is_now_initiailized; return std::unique_ptr{::curl_easy_init(), curl_delete}; } +static +bool +download_to_string(const std::string& url, std::string& str) +{ + str.clear(); + auto curl = curl_init(); + if (!curl) + return false; + std::string version; + curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); + curl_write_callback write_cb = [](char* contents, std::size_t size, std::size_t nmemb, + void* userp) -> std::size_t + { + auto& str = *static_cast(userp); + auto realsize = size * nmemb; + str.append(contents, realsize); + return realsize; + }; + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, write_cb); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &str); + auto res = curl_easy_perform(curl.get()); + return (res == CURLE_OK); +} + +namespace +{ + enum class download_file_options { binary, text }; +} + +static +bool +download_to_file(const std::string& url, const std::string& local_filename, + download_file_options opts) +{ + auto curl = curl_init(); + if (!curl) + return false; + curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); + curl_write_callback write_cb = [](char* contents, std::size_t size, std::size_t nmemb, + void* userp) -> std::size_t + { + auto& of = *static_cast(userp); + auto realsize = size * nmemb; + of.write(contents, realsize); + return realsize; + }; + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, write_cb); + decltype(curl_easy_perform(curl.get())) res; + { + std::ofstream of(local_filename, + opts == download_file_options::binary ? + std::ofstream::out | std::ofstream::binary : + std::ofstream::out); + of.exceptions(std::ios::badbit); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &of); + res = curl_easy_perform(curl.get()); + } + return res == CURLE_OK; +} + std::string remote_version() { std::string version; - auto curl = curl_init(); - if (curl != nullptr) + std::string str; + if (download_to_string("http://www.iana.org/time-zones", str)) { - curl_easy_setopt(curl.get(), CURLOPT_URL, "http://www.iana.org/time-zones"); - using curl_callback = std::size_t(*)(void* contents, std::size_t size, - std::size_t nmemb, void* userp); - curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, - static_cast( - [](void* contents, std::size_t size, std::size_t nmemb, void* userp) - -> std::size_t - { - auto& str = *static_cast(userp); - auto realsize = size * nmemb; - auto data = static_cast(contents); - str.append(data, realsize); - return realsize; - })); - std::string str; - curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &str); - auto res = curl_easy_perform(curl.get()); - if (res == CURLE_OK) - { - CONSTDATA char db[] = "/time-zones/repository/releases/tzdata"; - CONSTDATA auto db_size = sizeof(db) - 1; - auto p = str.find(db, 0, db_size); - if (p != std::string::npos && p + (db_size + 5) <= str.size()) - version = str.substr(p + db_size, 5); - } + CONSTDATA char db[] = "/time-zones/repository/releases/tzdata"; + CONSTDATA auto db_size = sizeof(db) - 1; + auto p = str.find(db, 0, db_size); + const int ver_str_len = 5; + if (p != std::string::npos && p + (db_size + ver_str_len) <= str.size()) + version = str.substr(p + db_size, ver_str_len); } return version; } @@ -2047,55 +2146,282 @@ bool remote_download(const std::string& version) { assert(!version.empty()); - auto curl = curl_init(); - if (curl != nullptr) + auto url = "http://www.iana.org/time-zones/repository/releases/tzdata" + version + + ".tar.gz"; + bool result = download_to_file(url, get_download_gz_file(version), + download_file_options::binary); +#if TIMEZONE_MAPPING + if (result) { - auto url = "http://www.iana.org/time-zones/repository/releases/tzdata" + - version + ".tar.gz"; - curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); - using curl_callback = std::size_t(*)(void* contents, std::size_t size, - std::size_t nmemb, void* userp); - curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, - static_cast( - [](void* contents, std::size_t size, std::size_t nmemb, void* userp) - -> std::size_t - { - auto& of = *static_cast(userp); - auto realsize = size * nmemb; - auto data = static_cast(contents); - of.write(data, realsize); - return realsize; - })); - auto tarfile = install + version + ".tar.gz"; - decltype(curl_easy_perform(curl.get())) res; - { - std::ofstream of(tarfile); - curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &of); - res = curl_easy_perform(curl.get()); - } - return res == CURLE_OK; + auto mapping_file = get_download_mapping_file(version); + result = download_to_file("http://unicode.org/repos/cldr/trunk/common/" + "supplemental/windowsZones.xml", + mapping_file, download_file_options::text); + } +#endif + return result; +} + +// TODO! Using system() create a process and a console window. +// This is useful to see what errors may occur but is slow and distracting. +// Consider implementing this functionality more directly, such as +// using _mkdir and CreateProcess etc. +// But use the current means now as matches Unix implementations and while +// in proof of concept / testing phase. + +static +bool +remove_folder_and_subfolders(const std::string& folder) +{ +#if _WIN32 +# if USE_SHELL_API + // Delete the folder contents by deleting the folder. + std::string cmd = "rd /s /q \""; + cmd += folder; + cmd += '\"'; + return std::system(cmd.c_str()) == EXIT_SUCCESS; +# else // !USE_SHELL_API + // Create a buffer containing the path to delete. It must be terminated + // by two nulls. Who designs these API's... + std::vector from; + from.assign(folder.begin(), folder.end()); + from.push_back('\0'); + from.push_back('\0'); + SHFILEOPSTRUCT fo{}; // Zero initialize. + fo.wFunc = FO_DELETE; + fo.pFrom = from.data(); + fo.fFlags = FOF_NO_UI; + int ret = SHFileOperation(&fo); + if (ret == 0 && !fo.fAnyOperationsAborted) + return true; + return false; +# endif // !USE_SHELL_API +#else // !WIN32 + return std::system(("rm -R " + folder).c_str()) == EXIT_SUCCESS; +#endif // !WIN32 +} + +static +bool +make_directory(const std::string& folder) +{ +#if _WIN32 +# if USE_SHELL_API + // Re-create the folder. + std::string cmd = "mkdir \""; + cmd += folder; + cmd += '\"'; + return std::system(cmd.c_str()) == EXIT_SUCCESS; +# else // !USE_SHELL_API + return _mkdir(folder.c_str()) == 0; +# endif // !USE_SHELL_API +#else // !WIN32 + return std::system(("mkdir " + folder).c_str()) == EXIT_SUCCESS; +#endif +} + +static +bool +delete_file(const std::string& file) +{ +#if _WIN32 +# if USE_SHELL_API + std::string cmd = "del \""; + cmd += file; + cmd += '\"'; + return std::system(cmd.c_str()) == 0; +# else // !USE_SHELL_API + return _unlink(file.c_str()) == 0; +# endif // !USE_SHELL_API +#else // !WIN32 + return std::system(("rm " + file).c_str()) == EXIT_SUCCESS; +#endif // !WIN32 +} + +#if TIMEZONE_MAPPING + +static +bool +move_file(const std::string& from, const std::string& to) +{ +#if _WIN32 +# if USE_SHELL_API + std::string cmd = "move \""; + cmd += from; + cmd += "\" \""; + cmd += to; + cmd += '\"'; + return std::system(cmd.c_str()) == EXIT_SUCCESS; +# else // !USE_SHELL_API + return !!::MoveFile(from.c_str(), to.c_str()); +# endif // !USE_SHELL_API +#else // !WIN32 + return std::system(("mv " + from + " " + to).c_str()) == EXIT_SUCCESS; +#endif // !WIN32 +} + +#endif // TIMEZONE_MAPPING + +#if _WIN32 + +// Note folder can and usually does contain spaces. +// Note assume's 7 zip is in the default installation location. +// TODO! consider more certain means of finding it such as looking in the registry. +static +std::string +get_unzip_program() +{ + std::string path; + path += get_program_folder(); + path += folder_delimiter; + path += "7-Zip\\7z.exe"; + return path; +} + +static +int +run_program(const std::string& command) +{ + STARTUPINFO si{}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi{}; + + // Allegedly CreateProcess overwrites the command line. Ugh. + std::string mutable_command(command); + if (CreateProcess(nullptr, &mutable_command[0], + nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) + { + WaitForSingleObject(pi.hProcess, INFINITE); + DWORD exit_code; + bool got_exit_code = !!GetExitCodeProcess(pi.hProcess, &exit_code); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + // Not 100% sure about this still active thing is correct, + // but I'm going with it because I *think* WaitForSingleObject might + // return in some cases without INFINITE-ly waiting. + // But why/wouldn't GetExitCodeProcess return false in that case? + if (got_exit_code && exit_code != STILL_ACTIVE) + return static_cast(exit_code); + } + return EXIT_FAILURE; +} + +static +std::string +get_download_tar_file(const std::string& version) +{ + auto file = install; + file += folder_delimiter; + file += "tzdata"; + file += version; + file += ".tar"; + return file; +} + +static +bool +extract_gz_file(const std::string& version, const std::string& gz_file, + const std::string& dest_folder) +{ + auto unzip_prog = get_unzip_program(); + bool unzip_result = false; + // Use the unzip program to extract the tar file from the archive. + + // Aim to create a string like: + // "C:\Program Files\7-Zip\7z.exe" x "C:\Users\SomeUser\Downloads\tzdata2016d.tar.gz" + // -o"C:\Users\SomeUser\Downloads\tzdata" + std::string cmd; + cmd = '\"'; + cmd += unzip_prog; + cmd += "\" x \""; + cmd += gz_file; + cmd += "\" -o\""; + cmd += dest_folder; + cmd += '\"'; + +#if USE_SHELL_API + // When using shelling out with std::system() extra quotes are required around the + // whole command. It's weird but neccessary it seems, see: + // http://stackoverflow.com/q/27975969/576911 + + cmd = "\"" + cmd + "\""; + if (std::system(cmd.c_str()) == EXIT_SUCCESS) + unzip_result = true; +#else // !USE_SHELL_API + if (run_program(cmd) == EXIT_SUCCESS) + unzip_result = true; +#endif // !USE_SHELL_API + if (unzip_result) + delete_file(gz_file); + + // Use the unzip program extract the data from the tar file that was + // just extracted from the archive. + auto tar_file = get_download_tar_file(version); + cmd = '\"'; + cmd += unzip_prog; + cmd += "\" x \""; + cmd += tar_file; + cmd += "\" -o\""; + cmd += install; + cmd += '\"'; +#if USE_SHELL_API + cmd = "\"" + cmd + "\""; + if (std::system(cmd.c_str()) == EXIT_SUCCESS) + unzip_result = true; +#else // !USE_SHELL_API + if (run_program(cmd) == EXIT_SUCCESS) + unzip_result = true; +#endif // !USE_SHELL_API + + if (unzip_result) + delete_file(tar_file); + + return unzip_result; +} + +#else // !_WIN32 + +static +bool +extract_gz_file(const std::string&, const std::string& gz_file, const std::string&) +{ + if (std::system(("tar -xzf " + gz_file + " -C " + install).c_str()) == EXIT_SUCCESS) + { + delete_file(gz_file); + return true; } return false; } +#endif // !_WIN32 + bool remote_install(const std::string& version) { auto success = false; assert(!version.empty()); - auto tarfile = install + version + ".tar.gz"; - if (file_exists(tarfile)) + + auto gz_file = get_download_gz_file(version); + if (file_exists(gz_file)) { if (file_exists(install)) - std::system(("rm -R " + install).c_str()); - if (std::system(("mkdir " + install + " && " - "tar -xzf " + tarfile + " -C " + install).c_str()) == 0) - success = true; - std::system(("rm " + tarfile).c_str()); + remove_folder_and_subfolders(install); + if (make_directory(install)) + { + if (extract_gz_file(version, gz_file, install)) + success = true; +#if TIMEZONE_MAPPING + auto mapping_file_source = get_download_mapping_file(version); + auto mapping_file_dest = install; + mapping_file_dest += folder_delimiter; + mapping_file_dest += "windowsZones.xml"; + if (!move_file(mapping_file_source, mapping_file_dest)) + success = false; +#endif + } } return success; } - #endif // HAS_REMOTE_API static @@ -2131,7 +2457,17 @@ init_tzdb() { auto rv = remote_version(); if (!rv.empty() && remote_download(rv)) - remote_install(rv); + { + if (!remote_install(rv)) + { + std::string msg = "Timezone database version \""; + msg += rv; + msg += "\" did not install correctly to \""; + msg += install; + msg += "\""; + throw std::runtime_error(msg); + } + } if (!file_exists(install)) { std::string msg = "Timezone database not found at \""; @@ -2221,10 +2557,11 @@ init_tzdb() db.leaps.shrink_to_fit(); #if TIMEZONE_MAPPING - std::string mapping_file = path + "TimeZoneMappings.csv"; - db.mappings = load_timezone_mappings_from_csv_file(mapping_file); + std::string mapping_file = path + "windowsZones.xml"; + db.mappings = load_zone_mappings_from_xml_file(mapping_file); + sort_zone_mappings(db.mappings); get_windows_timezone_info(db.native_zones); -#endif +#endif // TIMEZONE_MAPPING return db; } @@ -2286,25 +2623,6 @@ locate_zone(const std::string& tz_name) return &*zi; } -#ifdef TZ_TEST -#ifdef _WIN32 -const time_zone* -locate_native_zone(const std::string& native_tz_name) -{ - std::string standard_tz_name; - if (!native_to_standard_timezone_name(native_tz_name, standard_tz_name)) - { - std::string msg; - msg = "locate_native_zone() failed: A mapping from the Windows Time Zone id \""; - msg += native_tz_name; - msg += "\" was not found in the time zone mapping database."; - throw std::runtime_error(msg); - } - return locate_zone(standard_tz_name); -} -#endif -#endif - std::ostream& operator<<(std::ostream& os, const TZ_DB& db) { @@ -2391,7 +2709,7 @@ operator<<(std::ostream& os, const local_info& r) return os; } -#ifdef _WIN32 +#if _WIN32 const time_zone* current_zone() @@ -2426,13 +2744,20 @@ current_zone() throw std::runtime_error(msg); } return date::locate_zone(standard_tzid); -#else - // Currently Win32 requires mapping for this function to work. +#else // !TIMEZONE_MAPPING + // Currently Win32 requires iana <--> windows tz name mappings + // for this function to work. + // TODO! we should really support TIMEZONE_MAPPINGS=0 on Windows, + // And in this mode we should read the current iana timezone from a file. + // This would allow the TZ library do be used by apps that don't care + // about Windows standard names just iana names. + // This would allow the xml dependency to be dropped and none of + // the name mapping functions would be needed. throw std::runtime_error("current_zone not implemented."); -#endif +#endif // !TIMEZONE_MAPPING } -#else // ! WIN32 +#else // !WIN32 const time_zone* current_zone() @@ -2458,7 +2783,6 @@ current_zone() auto sz = readlink(timezone, &result.front(), result.size()); if (sz == -1) throw std::runtime_error("readlink failure"); - auto tmp = result.size(); result.resize(sz); const char zonepath[] = "/usr/share/zoneinfo/"; const size_t zonepath_len = sizeof(zonepath)/sizeof(zonepath[0])-1; @@ -2503,6 +2827,26 @@ current_zone() } throw std::runtime_error("Could not get current timezone"); } -#endif + +#endif // !WIN32 + +#if TZ_TEST && TIMEZONE_MAPPING + +const time_zone* +locate_native_zone(const std::string& native_tz_name) +{ + std::string standard_tz_name; + if (!native_to_standard_timezone_name(native_tz_name, standard_tz_name)) + { + std::string msg; + msg = "locate_native_zone() failed: A mapping from the native/Windows Time Zone id \""; + msg += native_tz_name; + msg += "\" was not found in the time zone mapping database."; + throw std::runtime_error(msg); + } + return locate_zone(standard_tz_name); +} + +#endif // TZ_TEST && TIMEZONE_MAPPING } // namespace date diff --git a/tz.h b/tz.h index e3b7c44..c6cc742 100644 --- a/tz.h +++ b/tz.h @@ -29,29 +29,25 @@ // Get more recent database at http://www.iana.org/time-zones -// Questions: -// 1. Reload database. -// 4. Is the utc to sys renaming complete? Was it done correctly? +// The notion of "current timezone" is something the operating system is expected to "just +// know". How it knows this is system specific. It's often a value set by the user at OS +// intallation time and recorded by the OS somewhere. On Linux and Mac systems the current +// timezone name is obtained by looking at the name or contents of a particular file on +// disk. On Windows the current timzeone name comes from the registry. In either method, +// there is no guarantee that the "native" current timezone name obtained will match any +// of the "Standard" names in this library's "database". On Linux, the names usually do +// seem to match so mapping functions to map from native to "Standard" are typically not +// required. On Windows, the names are never "Standard" so mapping is always required. +// Technically any OS may use the mapping process but currently only Windows does use it. -/* -The notion of "current timezone" is something the operating system is expected -to "just know". How it knows this is system specific. It's often a value -set by the user at OS intallation time and recorded by the OS somewhere. -On Linux and Mac systems the current timezone name is obtained by looking at -the name or contents of a particular file on disk. -On Windows the current timzeone name comes from the registry. -In either method, there is no guarantee that the "native" current timezone name obtained -will match any of the "Standard" names in this library's "database". -On Linux, the names usually do seem to match so mapping functions to map from -native to "Standard" are typically not required. -On Windows, the names are never "Standard" so mapping is always required. -Technically any OS may use the mapping process but currently only Windows does use it. -*/ - -#ifdef _WIN32 -#ifndef TIMEZONE_MAPPING -#define TIMEZONE_MAPPING 1 -#endif +#if _WIN32 +# ifndef TIMEZONE_MAPPING +# define TIMEZONE_MAPPING 1 +# endif +#else +# if TIMEZONE_MAPPING +# error "Timezone mapping is not required or not implemented for this platform." +# endif #endif #ifndef LAZY_INIT @@ -59,10 +55,10 @@ Technically any OS may use the mapping process but currently only Windows does u #endif #ifndef HAS_REMOTE_API -# ifndef _MSC_VER -# define HAS_REMOTE_API 1 -# else +# if _WIN32 # define HAS_REMOTE_API 0 +# else +# define HAS_REMOTE_API 1 # endif #endif @@ -735,10 +731,10 @@ bool remote_install(const std::string& version); const time_zone* locate_zone(const std::string& tz_name); #ifdef TZ_TEST -#ifdef _WIN32 +# if _WIN32 const time_zone* locate_native_zone(const std::string& native_tz_name); -#endif -#endif +# endif // _WIN32 +#endif // TZ_TEST const time_zone* current_zone(); // zoned_time