diff --git a/README.md b/README.md index 92bba08..9c159c2 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,6 @@ This is actually three separate C++11/C++14 libraries: Here are the Cppcon 2015 slides on date.h: http://schd.ws/hosted_files/cppcon2015/43/hinnant_dates.pdf -2. `"tz.h"` / `"tz.cpp"` are a timezone library built on top of the `"date.h"` library. This timezone library is a complete parser of the IANA timezone database. It provides for an easy way to access all of the data in this database, using the types from `"date.h"` and ``. The IANA database also includes data on leap seconds, and this library provides utilities to compute with that information as well. See http://howardhinnant.github.io/tz.html for more details. See the `auto_download` branch for an experimental version of this library which auto-installs the latest version of the IANA timezone database (if you are connected to the internet). +2. `"tz.h"` / `"tz.cpp"` are a timezone library built on top of the `"date.h"` library. This timezone library is a complete parser of the IANA timezone database. It provides for an easy way to access all of the data in this database, using the types from `"date.h"` and ``. The IANA database also includes data on leap seconds, and this library provides utilities to compute with that information as well. See http://howardhinnant.github.io/tz.html for more details. 3. `"iso_week.h"` is a header-only library built on top of the `"date.h"` library which implements the ISO week date calendar. See http://howardhinnant.github.io/iso_week.html for more details. diff --git a/tz.cpp b/tz.cpp index 39bae30..ab4870f 100644 --- a/tz.cpp +++ b/tz.cpp @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2015 Howard Hinnant +// Copyright (c) 2015, 2016 Howard Hinnant // Copyright (c) 2015 Ville Voutilainen // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -35,6 +35,9 @@ #include #include #include +#if HAS_REMOTE_API +#include +#endif #ifdef _WIN32 #include #include @@ -73,6 +76,7 @@ #include #else #include +#include #endif namespace date @@ -82,10 +86,24 @@ namespace date // +---------------------+ #if _WIN32 // TODO: sensible default for all platforms. -static std::string install{ "c:\\tzdata" }; -#else -static std::string install{ "/Users/howardhinnant/Downloads/tzdata" }; -#endif +static const std::string install{ "c:\\tzdata" }; +#else // !_WIN32 + +static +std::string +expand_path(std::string path) +{ + ::wordexp_t w{}; + ::wordexp(path.c_str(), &w, 0); + assert(w.we_wordc == 1); + path = w.we_wordv[0]; + ::wordfree(&w); + return path; +} + +static const std::string install = expand_path("~/Downloads/tzdata"); + +#endif // !_WIN32 static const std::vector files = { @@ -117,7 +135,7 @@ static_assert(boring_day.ok(), "Configuration error"); #endif // Until filesystem arrives. -static const char folder_delimiter = +static CONSTDATA char folder_delimiter = #ifdef _WIN32 '\\'; #else @@ -163,7 +181,7 @@ static std::string get_win32_message(DWORD error_code) assert(message_buffer.get() != nullptr); return std::string(message_buffer.get()); } -#endif +#endif // _WIN32 #if TIMEZONE_MAPPING @@ -447,7 +465,7 @@ native_to_standard_timezone_name(const std::string& native_tz_name, } return false; } -#endif +#endif // TIMEZONE_MAPPING // Parsing helpers @@ -1893,6 +1911,138 @@ operator<<(std::ostream& os, const Leap& x) return os << x.date_ << " +"; } +#if HAS_REMOTE_API + +// CURL tools + +static +int +curl_global() +{ + if (::curl_global_init(CURL_GLOBAL_DEFAULT) != 0) + throw std::runtime_error("CURL global initialization failed"); + return 0; +} + +static const auto curl_delete = [](CURL* p) {::curl_easy_cleanup(p);}; + +static +std::unique_ptr +curl_init() +{ + static const auto curl_is_now_initiailized = curl_global(); + return std::unique_ptr{::curl_easy_init(), curl_delete}; +} + +std::string +remote_version() +{ + std::string version; + auto curl = curl_init(); + if (curl != nullptr) + { + 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); + } + } + return version; +} + +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"; + 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; + } + return false; +} + +bool +remote_install(const std::string& version) +{ + auto success = false; + assert(!version.empty()); + auto tarfile = install + version + ".tar.gz"; + if (file_exists(tarfile)) + { + 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()); + } + return success; +} + +#endif // HAS_REMOTE_API + +static +std::string +get_version(const std::string& path) +{ + std::ifstream infile(path + "Makefile"); + std::string version; + while (infile) + { + infile >> version; + if (version == "VERSION=") + { + infile >> version; + return version; + } + } + throw std::runtime_error("Unable to get Timezone database version from " + path); +} + static TZ_DB init_tzdb() @@ -1903,6 +2053,35 @@ init_tzdb() bool continue_zone = false; TZ_DB db; +#if AUTO_DOWNLOAD + if (!file_exists(install)) + { + auto rv = remote_version(); + if (!rv.empty() && remote_download(rv)) + remote_install(rv); + if (!file_exists(install)) + { + std::string msg = "Timezone database not found at \""; + msg += install; + msg += "\""; + throw std::runtime_error(msg); + } + db.version = get_version(path); + } + else + { + db.version = get_version(path); + auto rv = remote_version(); + if (!rv.empty() && db.version != rv) + { + if (remote_download(rv)) + { + remote_install(rv); + db.version = get_version(path); + } + } + } +#else // !AUTO_DOWNLOAD if (!file_exists(install)) { std::string msg = "Timezone database not found at \""; @@ -1910,21 +2089,8 @@ init_tzdb() msg += "\""; throw std::runtime_error(msg); } - - { - std::ifstream infile(path + "Makefile"); - while (infile) - { - infile >> line; - if (line == "VERSION=") - { - infile >> db.version; - break; - } - } - if (db.version.empty()) - throw std::runtime_error("Unable to get Timezone database version"); - } + db.version = get_version(path); +#endif // !AUTO_DOWNLOAD for (const auto& filename : files) { @@ -2001,13 +2167,11 @@ access_tzdb() const TZ_DB& reload_tzdb() { - return access_tzdb() = init_tzdb(); -} - -const TZ_DB& -reload_tzdb(const std::string& new_install) -{ - install = new_install; +#if AUTO_DOWNLOAD + auto const& v = access_tzdb().version; + if (!v.empty() && v == remote_version()) + return access_tzdb(); +#endif return access_tzdb() = init_tzdb(); } diff --git a/tz.h b/tz.h index 0533179..5d75ec1 100644 --- a/tz.h +++ b/tz.h @@ -3,7 +3,7 @@ // The MIT License (MIT) // -// Copyright (c) 2015 Howard Hinnant +// Copyright (c) 2015, 2016 Howard Hinnant // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -54,6 +54,21 @@ Technically any OS may use the mapping process but currently only Windows does u # define LAZY_INIT 1 #endif +#ifndef HAS_REMOTE_API +# ifndef _MSC_VER +# define HAS_REMOTE_API 1 +# else +# define HAS_REMOTE_API 0 +# endif +#endif + +#ifndef AUTO_DOWNLOAD +# define AUTO_DOWNLOAD HAS_REMOTE_API +#endif + +static_assert(HAS_REMOTE_API == 0 ? AUTO_DOWNLOAD == 0 : true, + "AUTO_DOWNLOAD can not be turned on without HAS_REMOTE_API"); + #include "date.h" #include @@ -627,7 +642,12 @@ std::ostream& operator<<(std::ostream& os, const TZ_DB& db); const TZ_DB& get_tzdb(); const TZ_DB& reload_tzdb(); -const TZ_DB& reload_tzdb(const std::string& new_install); + +#if HAS_REMOTE_API +std::string remote_version(); +bool remote_download(const std::string& version); +bool remote_install(const std::string& version); +#endif const Zone* locate_zone(const std::string& tz_name); #ifdef TZ_TEST