diff --git a/BUILD b/BUILD index 14d6b97..df0ba42 100644 --- a/BUILD +++ b/BUILD @@ -56,12 +56,16 @@ cc_library( "src/time_zone_lookup.cc", "src/time_zone_posix.cc", "src/time_zone_posix.h", + "src/time_zone_win.cc", + "src/time_zone_win.h", "src/tzfile.h", "src/zone_info_source.cc", ] + select({ "@platforms//os:windows": [ "src/time_zone_name_win.cc", "src/time_zone_name_win.h", + "src/time_zone_win_loader.cc", + "src/time_zone_win_loader.h", ], "//conditions:default": [], }), @@ -73,6 +77,7 @@ cc_library( linkopts = select({ "@platforms//os:osx": ["-Wl,-framework,CoreFoundation"], "@platforms//os:ios": ["-Wl,-framework,CoreFoundation"], + "@platforms//os:windows": ["advapi32.lib"], "//conditions:default": [], }), visibility = ["//visibility:public"], @@ -147,6 +152,18 @@ cc_test( ], ) +cc_test( + name = "time_zone_win_test", + size = "small", + srcs = ["src/time_zone_win_test.cc"], + deps = [ + ":civil_time", + ":time_zone", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + ### benchmarks cc_test( diff --git a/CMakeLists.txt b/CMakeLists.txt index f8bfbdd..119caaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,10 +83,14 @@ add_library(cctz src/time_zone_lookup.cc src/time_zone_posix.cc src/time_zone_posix.h + src/time_zone_win.cc + src/time_zone_win.h src/tzfile.h src/zone_info_source.cc $<$:src/time_zone_name_win.cc> $<$:src/time_zone_name_win.h> + $<$:src/time_zone_win_loader.cc> + $<$:src/time_zone_win_loader.h> ${CCTZ_HDRS} ) cctz_target_set_cxx_standard(cctz) @@ -100,6 +104,9 @@ set_target_properties(cctz PROPERTIES if(APPLE) target_link_libraries(cctz PUBLIC ${CoreFoundation}) endif() +if(WIN32) + target_link_libraries(cctz PUBLIC advapi32.lib) +endif() add_library(cctz::cctz ALIAS cctz) if (BUILD_TOOLS) @@ -148,6 +155,15 @@ if (BUILD_TESTING) ) add_test(time_zone_format_test time_zone_format_test) + add_executable(time_zone_win_test src/time_zone_win_test.cc) + cctz_target_set_cxx_standard(time_zone_win_test) + target_link_libraries(time_zone_win_test + cctz::cctz + ${CMAKE_THREAD_LIBS_INIT} + GMock::Main + ) + add_test(time_zone_win_test time_zone_win_test) + # tests runs on testdata set_property( TEST diff --git a/README.md b/README.md index eeaa016..055cfef 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,11 @@ zones in a simple and correct manner. The libraries in CCTZ are: These libraries are currently known to work on **Linux**, **Mac OS X**, and **Android**. -They will also work on **Windows** if you install the zoneinfo files. We are -interested, though, in an implementation of the cctz::TimeZoneIf interface that -calls the Windows time APIs instead. Please contact us if you're interested in -contributing. +They will also work on **Windows** if you install the zoneinfo files. You can +also specify a built-time macro `CCTZ_USE_WIN_REGISTRY_FALLBACK` to let CCTZ +fall back to time zone information stored in the Windows +[registry](https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information#remarks) +when the zoneinfo files are not available. # Getting Started diff --git a/src/time_zone_if.cc b/src/time_zone_if.cc index 2de47ff..61c27f7 100644 --- a/src/time_zone_if.cc +++ b/src/time_zone_if.cc @@ -16,6 +16,11 @@ #include "time_zone_info.h" #include "time_zone_libc.h" +#if defined(_WIN32) && defined(CCTZ_USE_WIN_REGISTRY_FALLBACK) +#include "time_zone_win.h" +#include "time_zone_win_loader.h" +#endif // defined(_WIN32) && defined(CCTZ_USE_WIN_REGISTRY_FALLBACK) + namespace cctz { std::unique_ptr TimeZoneIf::UTC() { @@ -31,8 +36,17 @@ std::unique_ptr TimeZoneIf::Make(const std::string& name) { return TimeZoneLibC::Make(name.substr(5)); } - // Otherwise use the "zoneinfo" implementation. - return TimeZoneInfo::Make(name); + // Attempt to use the "zoneinfo" implementation. + std::unique_ptr zone_info = TimeZoneInfo::Make(name); + + #if defined(_WIN32) && defined(CCTZ_USE_WIN_REGISTRY_FALLBACK) + if (!zone_info) { + // Attempt to fall back to Win32 Registry Implementation. + zone_info = MakeTimeZoneFromWinRegistry(LoadWinTimeZoneRegistry(name)); + } + #endif // defined(_WIN32) && defined(CCTZ_USE_WIN_REGISTRY_FALLBACK) + + return zone_info; } // Defined out-of-line to avoid emitting a weak vtable in all TUs. diff --git a/src/time_zone_lookup_test.cc b/src/time_zone_lookup_test.cc index ffcbcaf..36387d4 100644 --- a/src/time_zone_lookup_test.cc +++ b/src/time_zone_lookup_test.cc @@ -451,6 +451,32 @@ TEST(MakeTime, SysSecondsLimits) { } } +TEST(MakeTime, LookupKind) { + const time_zone tz = LoadZone("America/Los_Angeles"); + + // Spring 1:59:59 -> 3:00:00 + auto lookup = tz.lookup(civil_second(2013, 3, 10, 1, 59, 59)); + EXPECT_EQ(lookup.kind, time_zone::civil_lookup::UNIQUE); + lookup = tz.lookup(civil_second(2013, 3, 10, 2, 0, 0)); + EXPECT_EQ(lookup.kind, time_zone::civil_lookup::SKIPPED); + lookup = tz.lookup(civil_second(2013, 3, 10, 2, 15, 0)); + EXPECT_EQ(lookup.kind, time_zone::civil_lookup::SKIPPED); + lookup = tz.lookup(cctz::civil_second(2013, 6, 1, 3, 0, 0)); + EXPECT_EQ(lookup.kind, time_zone::civil_lookup::UNIQUE); + + // Fall 1:59:59 -> 1:00:00 + lookup = tz.lookup(cctz::civil_second(2013, 11, 3, 0, 59, 59)); + EXPECT_EQ(lookup.kind, time_zone::civil_lookup::UNIQUE); + lookup = tz.lookup(cctz::civil_second(2013, 11, 3, 1, 0, 0)); + EXPECT_EQ(lookup.kind, time_zone::civil_lookup::REPEATED); + lookup = tz.lookup(cctz::civil_second(2013, 11, 3, 1, 30, 0)); + EXPECT_EQ(lookup.kind, time_zone::civil_lookup::REPEATED); + lookup = tz.lookup(cctz::civil_second(2013, 11, 3, 1, 59, 59)); + EXPECT_EQ(lookup.kind, time_zone::civil_lookup::REPEATED); + lookup = tz.lookup(cctz::civil_second(2013, 11, 3, 2, 0, 0)); + EXPECT_EQ(lookup.kind, time_zone::civil_lookup::UNIQUE); +} + TEST(MakeTime, LocalTimeLibC) { // Checks that cctz and libc agree on transition points in [1970:2037]. // diff --git a/src/time_zone_name_win.cc b/src/time_zone_name_win.cc index 3dcdfd7..3c3c15d 100644 --- a/src/time_zone_name_win.cc +++ b/src/time_zone_name_win.cc @@ -45,10 +45,14 @@ bool U_SUCCESS(UErrorCode error) { return error <= U_ZERO_ERROR; } using ucal_getTimeZoneIDForWindowsID_func = std::int32_t(__cdecl*)( const UChar* winid, std::int32_t len, const char* region, UChar* id, std::int32_t id_capacity, UErrorCode* status); +using ucal_getWindowsTimeZoneID_func = + std::int32_t(__cdecl*)(const UChar* id, std::int32_t len, UChar* winid, + std::int32_t winid_capacity, UErrorCode* status); std::atomic g_unavailable; std::atomic g_ucal_getTimeZoneIDForWindowsID; +std::atomic g_ucal_getWindowsTimeZoneID; template static T AsProcAddress(HMODULE module, const char* name) { static_assert( @@ -73,6 +77,49 @@ std::wstring GetSystem32Dir() { return result; } +bool LoadIcuFunctionsInternal() { + const std::wstring system32_dir = GetSystem32Dir(); + if (system32_dir.empty()) { + g_unavailable.store(true, std::memory_order_relaxed); + return false; + } + + // Here LoadLibraryExW(L"icu.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) does + // not work if "icu.dll" is already loaded from somewhere other than the + // system32 directory. Specifying the full path with LoadLibraryW is more + // reliable. + const std::wstring icu_dll_path = system32_dir + L"\\icu.dll"; + const HMODULE icu_dll = ::LoadLibraryW(icu_dll_path.c_str()); + if (icu_dll == nullptr) { + g_unavailable.store(true, std::memory_order_relaxed); + return false; + } + + const auto ucal_getTimeZoneIDForWindowsIDRef = + AsProcAddress( + icu_dll, "ucal_getTimeZoneIDForWindowsID"); + if (ucal_getTimeZoneIDForWindowsIDRef != nullptr) { + g_unavailable.store(true, std::memory_order_relaxed); + return false; + } + + g_ucal_getTimeZoneIDForWindowsID.store(ucal_getTimeZoneIDForWindowsIDRef, + std::memory_order_relaxed); + + const auto ucal_getWindowsTimeZoneIDRef = + AsProcAddress( + icu_dll, "ucal_getWindowsTimeZoneID"); + if (ucal_getWindowsTimeZoneIDRef != nullptr) { + g_unavailable.store(true, std::memory_order_relaxed); + return false; + } + + g_ucal_getWindowsTimeZoneID.store(ucal_getWindowsTimeZoneIDRef, + std::memory_order_relaxed); + + return true; +} + ucal_getTimeZoneIDForWindowsID_func LoadIcuGetTimeZoneIDForWindowsID() { // This function is intended to be lock free to avoid potential deadlocks // with loader-lock taken inside LoadLibraryW. As LoadLibraryW and @@ -92,35 +139,34 @@ ucal_getTimeZoneIDForWindowsID_func LoadIcuGetTimeZoneIDForWindowsID() { } } - const std::wstring system32_dir = GetSystem32Dir(); - if (system32_dir.empty()) { - g_unavailable.store(true, std::memory_order_relaxed); + if (!LoadIcuFunctionsInternal()) { return nullptr; } - // Here LoadLibraryExW(L"icu.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32) does - // not work if "icu.dll" is already loaded from somewhere other than the - // system32 directory. Specifying the full path with LoadLibraryW is more - // reliable. - const std::wstring icu_dll_path = system32_dir + L"\\icu.dll"; - const HMODULE icu_dll = ::LoadLibraryW(icu_dll_path.c_str()); - if (icu_dll == nullptr) { - g_unavailable.store(true, std::memory_order_relaxed); + return g_ucal_getTimeZoneIDForWindowsID.load(std::memory_order_relaxed); +} + +ucal_getWindowsTimeZoneID_func LoadIcuGetWindowsTimeZoneID() { + // This function is intended to be lock. See the comment in + // LoadIcuGetTimeZoneIDForWindowsID() for details. + + if (g_unavailable.load(std::memory_order_relaxed)) { return nullptr; } - const auto ucal_getTimeZoneIDForWindowsIDRef = - AsProcAddress( - icu_dll, "ucal_getTimeZoneIDForWindowsID"); - if (ucal_getTimeZoneIDForWindowsIDRef != nullptr) { - g_unavailable.store(true, std::memory_order_relaxed); - return nullptr; + { + const auto ucal_getWindowsTimeZoneID = + g_ucal_getWindowsTimeZoneID.load(std::memory_order_relaxed); + if (ucal_getWindowsTimeZoneID != nullptr) { + return ucal_getWindowsTimeZoneID; + } } - g_ucal_getTimeZoneIDForWindowsID.store(ucal_getTimeZoneIDForWindowsIDRef, - std::memory_order_relaxed); + if (!LoadIcuFunctionsInternal()) { + return nullptr; + } - return ucal_getTimeZoneIDForWindowsIDRef; + return g_ucal_getWindowsTimeZoneID.load(std::memory_order_relaxed); } // Convert wchar_t array (UTF-16) to UTF-8 string @@ -174,4 +220,33 @@ std::string GetWindowsLocalTimeZone() { } } +std::wstring ConvertToWindowsTimeZoneId(const std::wstring& iana_name) { + const auto getWindowsTimeZoneID = LoadIcuGetWindowsTimeZoneID(); + if (getWindowsTimeZoneID == nullptr) { + return std::wstring(); + } + if (iana_name.size() > std::numeric_limits::max()) { + return std::wstring(); + } + const std::int32_t iana_name_length = + static_cast(iana_name.size()); + + std::wstring result; + std::size_t len = std::max( + std::min(result.capacity(), std::numeric_limits::max()), 1); + for (;;) { + UErrorCode status = U_ZERO_ERROR; + result.resize(len); + len = static_cast(getWindowsTimeZoneID( + iana_name.c_str(), iana_name_length, &result[0], static_cast(len), + &status)); + if (U_SUCCESS(status)) { + return result; + } + if (status != U_BUFFER_OVERFLOW_ERROR) { + return std::wstring(); + } + } +} + } // namespace cctz diff --git a/src/time_zone_name_win.h b/src/time_zone_name_win.h index 4a9de03..a273bff 100644 --- a/src/time_zone_name_win.h +++ b/src/time_zone_name_win.h @@ -24,6 +24,11 @@ namespace cctz { // where "icu.dll" is not available in the System32 directory. std::string GetWindowsLocalTimeZone(); +// Converts IANA time zone name to Windows time zone ID, or the empty string on +// failure. Not supported on Windows 10 1809 and earlier, where "icu.dll" is not +// available in the System32 directory. +std::wstring ConvertToWindowsTimeZoneId(const std::wstring& iana_name); + } // namespace cctz #endif // CCTZ_TIME_ZONE_NAME_WIN_H_ diff --git a/src/time_zone_win.cc b/src/time_zone_win.cc new file mode 100644 index 0000000..d0aa537 --- /dev/null +++ b/src/time_zone_win.cc @@ -0,0 +1,722 @@ +// Copyright 2025 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "time_zone_win.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "time_zone_fixed.h" +#include "time_zone_if.h" + +namespace cctz { +namespace { + +struct RawOffsetInfo { + RawOffsetInfo() : offset_seconds(0), dst(false) {} + std::int_fast32_t offset_seconds; + bool dst; +}; + +// Transitions extracted from WinTimeZoneRegistryEntry (==REG_TZI_FORMAT) for +// the target year. Each WinTimeZoneRegistryEntry can provide up to three +// transitions in a year. +// The most tricky part is that WinTimeZoneRegistryEntry gives us localtime in +// "from" offset whereas corresponding Biases are "to" offset. This means that +// "from" localtime cannot be converted to UTC time without knowing the "from" +// offset. +// See ResolveSystemTime() on how WinTimeZoneRegistryEntry is interpreted. +struct RawTransitionInfo { + civil_second from_civil_time; + RawOffsetInfo to; +}; + +civil_second TpToUtc(const time_point& tp) { + return civil_second(1970, 1, 1, 0, 0, 0) + + (tp - std::chrono::time_point_cast( + std::chrono::system_clock::from_time_t(0))) + .count(); +} + +time_point UtcToTp(const civil_second& cs) { + return std::chrono::time_point_cast( + std::chrono::system_clock::from_time_t(0)) + + seconds(cs - civil_second(1970, 1, 1, 0, 0, 0)); +} + +const char* kCommonAbbrs[] = { + "GMT-14", "GMT-1330", "GMT-13", "GMT-1230", "GMT-12", "GMT-1130", + "GMT-11", "GMT-1030", "GMT-10", "GMT-0930", "GMT-09", "GMT-0830", + "GMT-08", "GMT-0730", "GMT-07", "GMT-0630", "GMT-06", "GMT-0530", + "GMT-05", "GMT-0430", "GMT-04", "GMT-0330", "GMT-03", "GMT-0230", + "GMT-02", "GMT-0130", "GMT-01", "GMT+0030", "GMT", "GMT+0030", + "GMT+01", "GMT+0130", "GMT+02", "GMT+0230", "GMT+03", "GMT+0330", + "GMT+04", "GMT+0430", "GMT+05", "GMT+0530", "GMT+06", "GMT+0630", + "GMT+07", "GMT+0730", "GMT+08", "GMT+0830", "GMT+09", "GMT+0930", + "GMT+10", "GMT+1030", "GMT+11", "GMT+1130", "GMT+12", "GMT+1230", + "GMT+13", "GMT+1330", "GMT+14", +}; + +const char* GetCommonAbbreviation(std::int_fast32_t offset_seconds) { + if (offset_seconds % 1800 == 0) { + const std::int_fast32_t halfhour_offset = offset_seconds / 1800; + if (-28 <= halfhour_offset && halfhour_offset <= 28) { + return kCommonAbbrs[halfhour_offset + 28]; + } + } + return nullptr; +} + +class AbbreviationMap { + public: + AbbreviationMap() = default; + AbbreviationMap(std::vector index_key, + std::vector index_value) + : index_key_(std::move(index_key)), + index_value_(std::move(index_value)) {} + + const char* Get(std::int_fast32_t offset_seconds) const { + const char* common_abbr = GetCommonAbbreviation(offset_seconds); + if (common_abbr != nullptr) { + return common_abbr; + } + for (size_t i = 0; i < index_key_.size(); ++i) { + if (index_key_[i] == offset_seconds) { + // The returned pointer remains to be valid as long as we do not modify + // index_value_. + return index_value_[i].c_str(); + } + } + return ""; + } + + private: + const std::vector index_key_; + const std::vector index_value_; +}; + +class AbbreviationMapBuilder { + public: + AbbreviationMapBuilder() = default; + + void Add(const WinTimeZoneRegistryEntry& info) { + AddInternal(-60 * info.bias); + if (info.standard_bias != 0) { + AddInternal(-60 * (info.bias + info.standard_bias)); + } + if (info.daylight_bias != 0) { + AddInternal(-60 * (info.bias + info.daylight_bias)); + } + } + + AbbreviationMap Build() { + extra_offsets_.shrink_to_fit(); + std::vector result; + extra_offsets_.swap(result); + + std::vector abbrs; + abbrs.reserve(result.size()); + for (const std::int_fast32_t offset : result) { + const char* common_abbr = GetCommonAbbreviation(offset); + if (common_abbr == nullptr) { + abbrs.push_back("GMT" + cctz::FixedOffsetToAbbr(cctz::seconds(offset))); + } + } + return AbbreviationMap(std::move(result), std::move(abbrs)); + } + + private: + void AddInternal(std::int_fast32_t offset_seconds) { + if (GetCommonAbbreviation(offset_seconds) != nullptr) { + return; // Already exists as a common abbreviation. + } + for (size_t i = 0; i < extra_offsets_.size(); ++i) { + if (extra_offsets_[i] == offset_seconds) { + return; // Already exists. + } + } + extra_offsets_.push_back(offset_seconds); + } + + std::vector extra_offsets_; +}; + +struct LocalTimeInfo { + LocalTimeInfo() : offset_seconds(0), is_dst(false) {} + civil_second civil_time; + std::int_fast32_t offset_seconds; + bool is_dst; +}; + +struct TimeOffsetInfo { + TimeOffsetInfo() : kind(time_zone::civil_lookup::UNIQUE) {} + + LocalTimeInfo from; + LocalTimeInfo to; + time_point tp; + time_zone::civil_lookup::civil_kind kind; + + const civil_second& earlier_cs() const { + // Equivalent to std::min(from.civil_time, to.civil_time) + return kind == time_zone::civil_lookup::REPEATED ? to.civil_time + : from.civil_time; + } + const civil_second& later_cs() const { + // Equivalent to std::max(from.civil_time, to.civil_time) + return kind == time_zone::civil_lookup::REPEATED ? from.civil_time + : to.civil_time; + } +}; + +const cctz::weekday kWeekdays[] = { + cctz::weekday::sunday, cctz::weekday::monday, cctz::weekday::tuesday, + cctz::weekday::wednesday, cctz::weekday::thursday, cctz::weekday::friday, + cctz::weekday::saturday}; + +class TimeZoneRegistry { + public: + static TimeZoneRegistry Load(WinTimeZoneRegistryInfo info) { + AbbreviationMapBuilder abbr_map_builder; + for (const auto& info : info.entries) { + abbr_map_builder.Add(info); + } + return TimeZoneRegistry(std::move(info), abbr_map_builder.Build()); + } + + const year_t FirstYear() const { + if (timezone_info_.entries.size() < 2) { + return 0; + } + return timezone_info_.first_year; + } + const year_t LastYear() const { + if (timezone_info_.entries.size() < 2) { + return 0; + } + // The last entry is the fixed (or the latest) one. + return timezone_info_.first_year + + static_cast(timezone_info_.entries.size() - 2); + } + const bool IsAvailable() const { return !timezone_info_.entries.empty(); } + const bool IsYearDependent() const { + return timezone_info_.entries.size() >= 2; + } + const bool IsFixed() const { + return timezone_info_.entries.size() == 1 + ? IsFixedTimeZone(timezone_info_.entries.back()) + : false; + } + + const bool StartsWithFixed() const { + return timezone_info_.entries.empty() + ? false + : IsFixedTimeZone(timezone_info_.entries.front()); + } + const bool EndsWithFixed() const { + return timezone_info_.entries.empty() + ? false + : IsFixedTimeZone(timezone_info_.entries.back()); + } + + const char* GetAbbreviation(std::int_fast32_t offset_seconds) const { + return abbr_map_.Get(offset_seconds); + } + + std::int_fast32_t GetFixedOffset() const { + if (timezone_info_.entries.empty()) { + return 0; + } + const auto& base = timezone_info_.entries.back(); + return -60 * base.bias; + } + + std::deque GetOffsetInfo(year_t year_start, + year_t year_end) const { + if (!IsAvailable() || year_start > year_end) { + return {}; + } + std::deque result; + RawOffsetInfo last_base_info; + for (year_t year = year_start - 1; year <= year_end; ++year) { + const auto first_year = timezone_info_.first_year; + const auto& entries = timezone_info_.entries; + const size_t index = + year <= first_year + ? 0 + : std::min(year - first_year, entries.size() - 1); + const auto transitions = ParseTimeZoneInfo(entries[index], year); + if (year == year_start - 1) { + last_base_info = transitions.back().to; + continue; + } + for (const auto& transition : transitions) { + TimeOffsetInfo info; + info.from.civil_time = transition.from_civil_time; + info.from.offset_seconds = last_base_info.offset_seconds; + info.from.is_dst = last_base_info.dst; + info.tp = + UtcToTp(transition.from_civil_time - last_base_info.offset_seconds); + info.to.offset_seconds = transition.to.offset_seconds; + info.to.is_dst = transition.to.dst; + const std::int_fast32_t offset_diff = + transition.to.offset_seconds - last_base_info.offset_seconds; + info.to.civil_time = info.from.civil_time + offset_diff; + if (offset_diff > 0) { + info.kind = time_zone::civil_lookup::SKIPPED; + } else if (offset_diff == 0) { + if (!result.empty()) { + const auto& last_info = result.back(); + if (last_info.to.offset_seconds == info.from.offset_seconds && + last_info.to.is_dst == info.from.is_dst) { + // Redundant entry. + continue; + } + } + info.kind = time_zone::civil_lookup::UNIQUE; + } else { + info.kind = time_zone::civil_lookup::REPEATED; + } + if (!result.empty()) { + const auto& last_info = result.back(); + if (last_info.kind == info.kind && + last_info.from.civil_time == info.from.civil_time && + last_info.from.offset_seconds == info.from.offset_seconds && + last_info.from.is_dst == info.from.is_dst && + last_info.to.civil_time == info.to.civil_time && + last_info.to.offset_seconds == info.to.offset_seconds && + last_info.to.is_dst == info.to.is_dst && + last_info.tp == info.tp) { + // Redundant entry. + continue; + } + } + result.push_back(info); + last_base_info = transition.to; + } + } + // Remove redundant UNIQUE entries at the beginning. + while (!result.empty()) { + const auto& front = result.front(); + if (front.kind != time_zone::civil_lookup::UNIQUE) { + break; + } + result.pop_front(); + } + return result; + } + + private: + TimeZoneRegistry() = delete; + TimeZoneRegistry(WinTimeZoneRegistryInfo info, AbbreviationMap abbr_map) + : timezone_info_(std::move(info)), abbr_map_(std::move(abbr_map)) {} + + static bool IsFixedTimeZone(const WinTimeZoneRegistryEntry& entry) { + return entry.standard_date.month == 0 && entry.daylight_date.month == 0; + } + + static bool ResolveSystemTime(const WinSystemTime& system_time, year_t year, + civil_second* result) { + const year_t system_time_year = static_cast(system_time.year); + if (system_time_year == year) { + *result = civil_second(system_time_year, system_time.month, + system_time.day, system_time.hour, + system_time.minute, system_time.second); + return true; + } + if (system_time_year != 0) { + return false; + } + + // Assume the loader has already validated day_of_week to be in [0, 6]. + const cctz::weekday target_weekday = kWeekdays[system_time.day_of_week]; + cctz::civil_day target_day; + if (system_time.day == 5) { + // SYSTEMTIME::wDay == 5 means the last weekday of the month. + year_t tmp_year = year; + std::int_fast32_t tmp_month = system_time.month + 1; + if (tmp_month > 12) { + tmp_month = 1; + tmp_year += 1; + } + target_day = + prev_weekday(cctz::civil_day(tmp_year, tmp_month, 1), target_weekday); + } else { + // Calcurate the first target weekday of the month. + target_day = next_weekday(cctz::civil_day(year, system_time.month, 1) - 1, + target_weekday); + // Adjust the week number based on the wDay field. + target_day += (system_time.day - 1) * 7; + } + + civil_second cs(target_day.year(), target_day.month(), target_day.day(), + system_time.hour, system_time.minute, system_time.second); + // Special rule for "23:59:59.999". + // https://stackoverflow.com/a/47106207 + if (cs.hour() == 23 && cs.minute() == 59 && cs.second() == 59 && + system_time.milliseconds == 999) { + cs += 1; + } + *result = cs; + return true; + } + + static std::deque ParseTimeZoneInfo( + const WinTimeZoneRegistryEntry& format, year_t year) { + const civil_second year_begin(year, 1, 1, 0, 0, 0); + bool has_std_begin = false; + civil_second std_begin; + if (format.standard_date.month != 0) { + has_std_begin = ResolveSystemTime(format.standard_date, year, &std_begin); + } + bool has_dst_begin = false; + civil_second dst_begin; + if (format.daylight_date.month != 0) { + has_dst_begin = ResolveSystemTime(format.daylight_date, year, &dst_begin); + } + + std::deque result; + if (!(has_std_begin && std_begin == year_begin) && + !(has_dst_begin && dst_begin == year_begin)) { + RawTransitionInfo info; + info.from_civil_time = year_begin; + info.to.offset_seconds = -60 * format.bias; + info.to.dst = false; + result.push_back(info); + } + if (has_std_begin) { + RawTransitionInfo info; + info.from_civil_time = std_begin; + info.to.offset_seconds = -60 * (format.bias + format.standard_bias); + info.to.dst = false; + result.push_back(info); + } + if (has_dst_begin) { + RawTransitionInfo info; + info.from_civil_time = dst_begin; + info.to.offset_seconds = -60 * (format.bias + format.daylight_bias); + info.to.dst = true; + if (has_std_begin) { + if (dst_begin < std_begin) { + result.insert(result.end() - 1, info); + } else if (dst_begin == std_begin) { + result.pop_back(); + result.push_back(info); + } else { + result.push_back(info); + } + } else { + result.push_back(info); + } + } + + return result; + } + + const WinTimeZoneRegistryInfo timezone_info_; + const AbbreviationMap abbr_map_; +}; + +class TransitionCache { + public: + static TransitionCache Create(const TimeZoneRegistry& timezone_registry) { + return CreateInternal(timezone_registry); + } + + bool Hit(const civil_second& cs) const { + return (transitions_.front().earlier_cs() <= cs && + cs <= transitions_.back().later_cs()) || + (starts_with_fixed_ && cs < transitions_.front().earlier_cs()) || + (ends_with_fixed_ && transitions_.back().later_cs() < cs); + } + bool Hit(const time_point& tp) const { + return (transitions_.front().tp <= tp && tp <= transitions_.back().tp) || + (starts_with_fixed_ && tp < transitions_.front().tp) || + (ends_with_fixed_ && transitions_.back().tp < tp); + } + + const std::deque& Get() const { return transitions_; } + + private: + TransitionCache(std::deque transitions, + bool starts_with_fixed, bool ends_with_fixed) + : transitions_(std::move(transitions)), + starts_with_fixed_(starts_with_fixed), + ends_with_fixed_(ends_with_fixed) {} + + static TransitionCache CreateInternal( + const TimeZoneRegistry& timezone_registry) { + const auto utc_now = civil_second(1970, 1, 1) + std::time(nullptr); + + const year_t utc_year = utc_now.year(); + year_t first_year = utc_year - 16; + year_t last_year = utc_year + 16; + bool starts_with_fixed = false; + bool ends_with_fixed = false; + + if (timezone_registry.IsYearDependent()) { + starts_with_fixed = timezone_registry.StartsWithFixed(); + ends_with_fixed = timezone_registry.EndsWithFixed(); + if (starts_with_fixed) { + first_year = timezone_registry.FirstYear(); + } else { + first_year = + std::min(timezone_registry.FirstYear() - 3, first_year); + } + if (ends_with_fixed) { + last_year = timezone_registry.LastYear() + 1; + } else { + last_year = + std::max(timezone_registry.LastYear() + 3, last_year); + } + } + return TransitionCache( + timezone_registry.GetOffsetInfo(first_year, last_year), + starts_with_fixed, ends_with_fixed); + } + + const std::deque transitions_; + const bool starts_with_fixed_; + const bool ends_with_fixed_; +}; + +class DynamicTimeZone final : public TimeZoneIf { + public: + static std::unique_ptr Create( + TimeZoneRegistry timezone_registry) { + auto cache = TransitionCache::Create(timezone_registry); + return std::unique_ptr( + new DynamicTimeZone(std::move(timezone_registry), std::move(cache))); + } + + DynamicTimeZone(const DynamicTimeZone&) = delete; + DynamicTimeZone(DynamicTimeZone&&) = delete; + DynamicTimeZone& operator=(const DynamicTimeZone&) = delete; + + // TimeZoneIf implementations. + time_zone::absolute_lookup BreakTime( + const time_point& tp) const override { + const auto utc = TpToUtc(tp); + const std::deque& offsets = + transition_cache_.Hit(tp) + ? transition_cache_.Get() + : tz_reg_.GetOffsetInfo(utc.year() - 1, utc.year() + 1); + if (offsets.empty()) { + return {}; + } + const LocalTimeInfo* info = nullptr; + { + if (tp < offsets.front().tp) { + info = &offsets.front().from; + } else { + for (size_t i = 1; i < offsets.size(); ++i) { + if (offsets[i - 1].tp <= tp && tp < offsets[i].tp) { + info = &offsets[i - 1].to; + break; + } + } + } + if (info == nullptr) { + info = &offsets.back().to; + } + } + const std::int_fast32_t offset_seconds = info->offset_seconds; + time_zone::absolute_lookup result; + result.cs = utc + offset_seconds; + result.offset = offset_seconds; + result.is_dst = info->is_dst; + result.abbr = tz_reg_.GetAbbreviation(offset_seconds); + return result; + } + + time_zone::civil_lookup MakeTime(const civil_second& cs) const override { + const auto& offsets = + transition_cache_.Hit(cs) + ? transition_cache_.Get() + : tz_reg_.GetOffsetInfo(cs.year() - 1, cs.year() + 1); + if (offsets.empty()) { + return {}; + } + + if (cs < offsets.front().earlier_cs()) { + time_zone::civil_lookup result; + result.kind = time_zone::civil_lookup::UNIQUE; + result.pre = UtcToTp(cs - offsets.front().from.offset_seconds); + result.post = result.pre; + result.trans = result.pre; + return result; + } + + for (size_t i = 0; i < offsets.size(); ++i) { + const auto& current = offsets[i]; + if (current.earlier_cs() <= cs && cs < current.later_cs()) { + time_zone::civil_lookup result; + result.kind = current.kind; + result.pre = UtcToTp(cs - current.from.offset_seconds); + result.post = UtcToTp(cs - current.to.offset_seconds); + result.trans = current.tp; + return result; + } + if ((i + 1) < offsets.size()) { + const auto& next = offsets[i + 1]; + if (current.later_cs() <= cs && cs < next.earlier_cs()) { + time_zone::civil_lookup result; + result.kind = time_zone::civil_lookup::UNIQUE; + result.pre = UtcToTp(cs - current.to.offset_seconds); + result.post = result.pre; + result.trans = result.pre; + return result; + } + } + } + + time_zone::civil_lookup result; + result.kind = time_zone::civil_lookup::UNIQUE; + result.pre = UtcToTp(cs - offsets.back().to.offset_seconds); + result.post = result.pre; + result.trans = result.pre; + return result; + } + + bool NextTransition(const time_point& tp, + time_zone::civil_transition* trans) const override { + const auto& transitions = transition_cache_.Get(); + if (transitions.empty()) { + return false; + } + const auto it = std::upper_bound( + transitions.begin(), transitions.end(), tp, + [](const time_point& value, const TimeOffsetInfo& info) { + return value < info.tp; + }); + if (it == transitions.end()) { + return false; + } + trans->from = it->from.civil_time; + trans->to = it->to.civil_time; + return true; + } + + bool PrevTransition(const time_point& tp, + time_zone::civil_transition* trans) const override { + const auto& transitions = transition_cache_.Get(); + if (transitions.empty()) { + return false; + } + auto it = std::lower_bound( + transitions.begin(), transitions.end(), tp, + [](const TimeOffsetInfo& info, const time_point& value) { + return info.tp < value; + }); + if (it == transitions.begin()) { + return false; + } + --it; + trans->from = it->from.civil_time; + trans->to = it->to.civil_time; + return true; + } + + std::string Version() const override { return std::string(); } + + std::string Description() const override { return std::string(); } + + private: + DynamicTimeZone(TimeZoneRegistry timezone_map, + TransitionCache transition_cache) + : tz_reg_(std::move(timezone_map)), + transition_cache_(std::move(transition_cache)) {} + + const TimeZoneRegistry tz_reg_; + const TransitionCache transition_cache_; +}; + +class FixedTimeZone final : public TimeZoneIf { + public: + static std::unique_ptr Create(std::int_fast32_t offset_sec) { + const char* common_abbr = GetCommonAbbreviation(offset_sec); + return std::unique_ptr(new FixedTimeZone( + offset_sec, + common_abbr != nullptr + ? common_abbr + : "GMT" + cctz::FixedOffsetToAbbr(cctz::seconds(offset_sec)))); + } + + FixedTimeZone(const FixedTimeZone&) = delete; + FixedTimeZone(FixedTimeZone&&) = delete; + FixedTimeZone& operator=(const FixedTimeZone&) = delete; + + time_zone::absolute_lookup BreakTime( + const time_point& tp) const override { + time_zone::absolute_lookup result; + result.cs = TpToUtc(tp) + offset_sec_; + result.offset = offset_sec_; + result.is_dst = false; + result.abbr = abbr_.c_str(); + return result; + } + + time_zone::civil_lookup MakeTime(const civil_second& cs) const override { + time_zone::civil_lookup result; + result.kind = time_zone::civil_lookup::UNIQUE; + result.pre = UtcToTp(cs - offset_sec_); + result.post = result.pre; + result.trans = result.pre; + return result; + } + + bool NextTransition(const time_point& tp, + time_zone::civil_transition* trans) const override { + return false; + } + + bool PrevTransition(const time_point& tp, + time_zone::civil_transition* trans) const override { + return false; + } + + std::string Version() const override { return std::string(); } + std::string Description() const override { return std::string(); } + + private: + FixedTimeZone(std::int_fast32_t offset_sec, std::string abbr) + : offset_sec_(offset_sec), abbr_(std::move(abbr)) {} + + const std::int_fast32_t offset_sec_; + const std::string abbr_; +}; + +} // namespace + +std::unique_ptr MakeTimeZoneFromWinRegistry( + WinTimeZoneRegistryInfo info) { + if (info.entries.empty()) { + return nullptr; + } + auto timezone_registry = TimeZoneRegistry::Load(std::move(info)); + + if (timezone_registry.IsFixed()) { + const std::int_fast32_t offset_seconds = timezone_registry.GetFixedOffset(); + return FixedTimeZone::Create(offset_seconds); + } + + return DynamicTimeZone::Create(std::move(timezone_registry)); +} + +} // namespace cctz diff --git a/src/time_zone_win.h b/src/time_zone_win.h new file mode 100644 index 0000000..c87e045 --- /dev/null +++ b/src/time_zone_win.h @@ -0,0 +1,121 @@ +// Copyright 2025 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CCTZ_TIME_ZONE_WIN_H_ +#define CCTZ_TIME_ZONE_WIN_H_ + +#include +#include +#include + +#include "time_zone_if.h" + +namespace cctz { + +// A platform-independent redefinition of Windows' SYSTEMTIME structure. +// https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime +// Intentionally uses uint_fast8_t for several fields with an assumtion that +// data validation is already performed in the data loader layer. +struct WinSystemTime { + WinSystemTime() + : year(0), + month(0), + day_of_week(0), + day(0), + hour(0), + minute(0), + second(0), + milliseconds(0) {} + WinSystemTime(std::uint_fast16_t year_, std::uint_fast8_t month_, + std::uint_fast8_t day_of_week_, std::uint_fast8_t day_, + std::uint_fast8_t hour_, std::uint_fast8_t minute_, + std::uint_fast8_t second_, std::uint_fast16_t milliseconds_) + : year(year_), + month(month_), + day_of_week(day_of_week_), + day(day_), + hour(hour_), + minute(minute_), + second(second_), + milliseconds(milliseconds_) {} + + const std::uint_fast16_t year; + const std::uint_fast8_t month; + const std::uint_fast8_t day_of_week; + const std::uint_fast8_t day; + const std::uint_fast8_t hour; + const std::uint_fast8_t minute; + const std::uint_fast8_t second; + const std::uint_fast16_t milliseconds; +}; + +// A platform-independent redefinition of Windows' REG_TZI_FORMAT structure. +// https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information +struct WinTimeZoneRegistryEntry { + WinTimeZoneRegistryEntry() + : bias(0), + standard_bias(0), + daylight_bias(0), + standard_date(), + daylight_date() {} + WinTimeZoneRegistryEntry(std::int_fast32_t bias_, + std::int_fast32_t standard_bias_, + std::int_fast32_t daylight_bias_, + const WinSystemTime& standard_date_, + const WinSystemTime& daylight_date_) + : bias(bias_), + standard_bias(standard_bias_), + daylight_bias(daylight_bias_), + standard_date(standard_date_), + daylight_date(daylight_date_) {} + + // Base offset in minutes, where UTC == local time + bias. + const std::int_fast32_t bias; + // Additional offset in minutes applied to standard time. + const std::int_fast32_t standard_bias; + // Additional offset in minutes applied to DST. + const std::int_fast32_t daylight_bias; + // Local time (in the previous offset) when the standard time begins. + const WinSystemTime standard_date; + // Local time (in the previous offset) when the DST begins. + const WinSystemTime daylight_date; +}; + +// A platform-independent data snapshot of Windows Registry Time Zone entries. +struct WinTimeZoneRegistryInfo { + WinTimeZoneRegistryInfo() : entries(), first_year(0) {} + + WinTimeZoneRegistryInfo(std::vector entries_, + year_t first_year_) + : entries(std::move(entries_)), first_year(first_year_) {} + + // This field is also used to indicate whether the object is valid or not. + // - Size of 0: Invalid object (e.g. failed to load from the registry). + // - Size of 1: No per-year override. `first_year` is ignored. + // - Size of N: Per-year override for N years with extrapolations with the + // first/last entry. + std::vector entries; + year_t first_year; +}; + +// MakeTimeZoneFromWinRegistry does not validate the entries in +// WinTimeZoneRegistryInfo (e.g. invalid date entries). +// In production, LoadWinTimeZoneRegistry() takes care of runtime data +// validations. +std::unique_ptr MakeTimeZoneFromWinRegistry( + WinTimeZoneRegistryInfo info); + +} // namespace cctz + +#endif // CCTZ_TIME_ZONE_WIN_H_ diff --git a/src/time_zone_win_loader.cc b/src/time_zone_win_loader.cc new file mode 100644 index 0000000..8e88cbb --- /dev/null +++ b/src/time_zone_win_loader.cc @@ -0,0 +1,245 @@ +// Copyright 2025 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "time_zone_win_loader.h" + +#if defined(_WIN32) + +#if !defined(NOMINMAX) +#define NOMINMAX +#endif // !defined(NOMINMAX) +#include + +#include +#include +#include +#include + +#include "time_zone_name_win.h" + +namespace cctz { +namespace { + +const wchar_t kRegistryPath[] = + L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones"; + +// ARRAYSIZE(DYNAMIC_TIME_ZONE_INFORMATION::TimeZoneKeyName) == 128. +const size_t kWindowsTimeZoneNameMax = 128; + +// The raw structure stored in the "TZI" value of the Windows registry. +// https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information#remarks +#pragma pack(push, 1) +struct REG_TZI_FORMAT { + LONG Bias; + LONG StandardBias; + LONG DaylightBias; + SYSTEMTIME StandardDate; + SYSTEMTIME DaylightDate; +}; +#pragma pack(pop) + +using ScopedHKey = + std::unique_ptr::type, decltype(&::RegCloseKey)>; + +ScopedHKey OpenRegistryKey(HKEY root, const wchar_t* sub_key) { + HKEY hkey = nullptr; + if (::RegOpenKeyExW(root, sub_key, 0, KEY_READ, &hkey) != ERROR_SUCCESS) { + return ScopedHKey(nullptr, nullptr); + } + return ScopedHKey(hkey, ::RegCloseKey); +} + +bool ReadDword(HKEY key, const wchar_t* value_name, DWORD* value) { + DWORD size = sizeof(DWORD); + DWORD temp_value; + LSTATUS reg_result = + ::RegGetValueW(key, nullptr, value_name, RRF_RT_REG_DWORD, nullptr, + reinterpret_cast(&temp_value), &size); + if (reg_result != ERROR_SUCCESS || size != sizeof(DWORD)) { + return false; + } + *value = temp_value; + return true; +} + +std::pair ToWinSystemTime(const SYSTEMTIME& st) { + if (st.wMonth == 0) { + // No transition - all fields must be zero + const bool all_zero = st.wYear == 0 && st.wDayOfWeek == 0 && st.wDay == 0 && + st.wHour == 0 && st.wMinute == 0 && st.wSecond == 0 && + st.wMilliseconds == 0; + return std::make_pair(all_zero, WinSystemTime()); + } + + if (st.wYear == 0) { + // Recurring rule (wYear == 0 && st.wMonth != 0): + // http://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information#members + if (!(1 <= st.wMonth && st.wMonth <= 12 && 1 <= st.wDay && st.wDay <= 5 && + st.wDayOfWeek < 7)) { + return std::make_pair(false, WinSystemTime()); + } + } else { + // Absolute date (wYear != 0 && st.wMonth != 0) + if (!(1601 <= st.wYear && st.wYear <= 30827 && 1 <= st.wMonth && + st.wMonth <= 12 && 1 <= st.wDay && st.wDay <= 31 && + st.wDayOfWeek < 7)) { + return std::make_pair(false, WinSystemTime()); + } + } + + // Common time validation + if (24 <= st.wHour || 60 <= st.wMinute || 60 <= st.wSecond || + 1000 <= st.wMilliseconds) { + return std::make_pair(false, WinSystemTime()); + } + + return std::make_pair( + true, WinSystemTime(st.wYear, static_cast(st.wMonth), + static_cast(st.wDayOfWeek), + static_cast(st.wDay), + static_cast(st.wHour), + static_cast(st.wMinute), + static_cast(st.wSecond), + st.wMilliseconds)); +} + +std::pair ReadTimeZoneInfo( + HKEY key, const wchar_t* value_name) { + REG_TZI_FORMAT format; + DWORD size = sizeof(REG_TZI_FORMAT); + LSTATUS reg_result = + ::RegGetValueW(key, nullptr, value_name, RRF_RT_REG_BINARY, nullptr, + reinterpret_cast(&format), &size); + if (reg_result != ERROR_SUCCESS || size != sizeof(REG_TZI_FORMAT)) { + return std::make_pair(false, WinTimeZoneRegistryEntry()); + } + // Apply some limits to the Bias, StandardBias, and DaylightBias to avoid + // accidental integer overflow. + const LONG min_bias = -60 * 24 * 7; + const LONG max_bias = 60 * 24 * 7; + if (format.Bias < min_bias || max_bias < format.Bias || + format.StandardBias < min_bias || max_bias < format.StandardBias || + format.DaylightBias < min_bias || max_bias < format.DaylightBias) { + return std::make_pair(false, WinTimeZoneRegistryEntry()); + } + const auto standard_date_pair = ToWinSystemTime(format.StandardDate); + if (!standard_date_pair.first) { + return std::make_pair(false, WinTimeZoneRegistryEntry()); + } + const auto daylight_date_pair = ToWinSystemTime(format.DaylightDate); + if (!daylight_date_pair.first) { + return std::make_pair(false, WinTimeZoneRegistryEntry()); + } + + return std::make_pair( + true, WinTimeZoneRegistryEntry( + format.Bias, format.StandardBias, format.DaylightBias, + standard_date_pair.second, daylight_date_pair.second)); +} + +// Convert UTF-8 string to std::wstring (UTF-16) +std::wstring Utf8ToUtf16(const std::string& utf8str) { + if (utf8str.size() > std::numeric_limits::max()) { + return std::wstring(); + } + const char* utf8str_ptr = utf8str.data(); + const int utf8str_len = static_cast(utf8str.size()); + int num_counts = 0; + { + // Fast-path for small strings. + const int buffer_size = 32; + wchar_t buffer[buffer_size]; + num_counts = + ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8str_ptr, + utf8str_len, buffer, buffer_size); + if (num_counts <= buffer_size) { + return std::wstring(buffer, num_counts); + } + if (num_counts > std::numeric_limits::max()) { + return std::wstring(); + } + } + + auto wstr = std::unique_ptr(new wchar_t[num_counts]); + const int written_counts = + ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8str_ptr, + utf8str_len, wstr.get(), num_counts); + if (num_counts != written_counts) { + return std::wstring(); + } + return std::wstring(wstr.get(), num_counts); +} + +} // namespace + +WinTimeZoneRegistryInfo LoadWinTimeZoneRegistry(const std::string& name) { + const std::wstring key_name = + ConvertToWindowsTimeZoneId(Utf8ToUtf16(name)); + if (key_name.empty()) { + return {}; + } + + if (key_name.empty() || key_name.size() > kWindowsTimeZoneNameMax) { + return {}; + } + + const std::wstring reg_tz_path = + std::wstring(kRegistryPath) + L"\\" + key_name; + + ScopedHKey hkey_timezone = + OpenRegistryKey(HKEY_LOCAL_MACHINE, reg_tz_path.c_str()); + if (!hkey_timezone) { + return {}; + } + std::vector timezone_list; + DWORD first_year = 0; + DWORD last_year = 0; + + ScopedHKey hkey_dynamic_years = + OpenRegistryKey(hkey_timezone.get(), L"Dynamic DST"); + if (hkey_dynamic_years) { + if (!ReadDword(hkey_dynamic_years.get(), L"FirstEntry", &first_year)) { + return {}; + } + if (!ReadDword(hkey_dynamic_years.get(), L"LastEntry", &last_year)) { + return {}; + } + if (first_year > last_year) { + return {}; + } + + const size_t year_count = static_cast(last_year - first_year + 1); + timezone_list.reserve(year_count); + for (DWORD year = first_year; year <= last_year; ++year) { + const std::wstring key = std::to_wstring(year); + bool succeeded = false; + const auto pair = ReadTimeZoneInfo(hkey_dynamic_years.get(), key.c_str()); + if (!pair.first) { + return {}; + } + timezone_list.push_back(pair.second); + } + } + const auto pair = ReadTimeZoneInfo(hkey_timezone.get(), L"TZI"); + if (!pair.first) { + return {}; + } + timezone_list.push_back(pair.second); + timezone_list.shrink_to_fit(); + return {std::move(timezone_list), first_year}; +} + +} // namespace cctz + +#endif diff --git a/src/time_zone_win_loader.h b/src/time_zone_win_loader.h new file mode 100644 index 0000000..2f48520 --- /dev/null +++ b/src/time_zone_win_loader.h @@ -0,0 +1,30 @@ +// Copyright 2025 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CCTZ_TIME_ZONE_WIN_LOADER_H_ +#define CCTZ_TIME_ZONE_WIN_LOADER_H_ + +#if defined(_WIN32) +#include + +#include "time_zone_win.h" + +namespace cctz { + +WinTimeZoneRegistryInfo LoadWinTimeZoneRegistry(const std::string& name); + +} // namespace cctz + +#endif // defined(_WIN32) +#endif // CCTZ_TIME_ZONE_WIN_LOADER_H_ diff --git a/src/time_zone_win_test.cc b/src/time_zone_win_test.cc new file mode 100644 index 0000000..14cb034 --- /dev/null +++ b/src/time_zone_win_test.cc @@ -0,0 +1,204 @@ +// Copyright 2025 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "cctz/civil_time.h" +#include "gtest/gtest.h" +#include "time_zone_if.h" +#include "time_zone_win.h" + +namespace cctz { +namespace { + +time_point UtcToTp(const civil_second& cs) { + return std::chrono::time_point_cast( + std::chrono::system_clock::from_time_t(0)) + + seconds(cs - civil_second(1970, 1, 1, 0, 0, 0)); +} + +civil_second cs(year_t y, int m, int d, int hh) { + return civil_second(y, m, d, hh); +} + +struct CivilTransitionData { + std::time_t unix_seconds; + civil_second from; + civil_second to; + CivilTransitionData() : unix_seconds(0), from(), to() {} + CivilTransitionData(std::time_t unix_seconds_, civil_second from_, + civil_second to_) + : unix_seconds(unix_seconds_), from(from_), to(to_) {} +}; + +void ExpectNextTransitions(const TimeZoneIf* tz, year_t start_year, + const std::vector& data) { + auto tp = UtcToTp(civil_second(start_year, 1, 1, 0, 0, 0)); + size_t i = 0; + while (true) { + time_zone::civil_transition trans; + if (!tz->NextTransition(tp, &trans)) { + break; + } + if (i >= data.size()) { + break; + } + const auto& expected = data[i]; + EXPECT_EQ(expected.from, trans.from); + EXPECT_EQ(expected.to, trans.to); + time_zone::civil_lookup to_cl = tz->MakeTime(trans.to); + tp = to_cl.trans; + EXPECT_EQ(FromUnixSeconds(expected.unix_seconds), tp); + ++i; + } +} + +void ExpectNoTransitionAfter(const TimeZoneIf* tz, + std::int_fast64_t unix_seconds) { + time_zone::civil_transition trans; + EXPECT_FALSE(tz->NextTransition(FromUnixSeconds(unix_seconds), &trans)); +} + +void ExpectLocalTime(const TimeZoneIf* tz, civil_second utc, + civil_second expected_local, bool expected_dst, + const std::string& expected_abbr) { + auto al = tz->BreakTime(UtcToTp(utc)); + EXPECT_EQ(al.cs, expected_local); + EXPECT_EQ(al.offset, expected_local - utc); + EXPECT_EQ(al.is_dst, expected_dst); + EXPECT_EQ(al.abbr, expected_abbr); +} + +TEST(TimeZoneWin, NoOffset) { + const WinTimeZoneRegistryInfo info = { + { + {0, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + }, + 0}; + auto tzif = cctz::MakeTimeZoneFromWinRegistry(info); + ExpectLocalTime(tzif.get(), cs(2025, 8, 1, 0), cs(2025, 8, 1, 0), false, + "GMT"); +} + +TEST(TimeZoneWin, QuarterHourOffset) { + const WinTimeZoneRegistryInfo info = { + { + {15, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + }, + 0}; + auto tzif = cctz::MakeTimeZoneFromWinRegistry(info); + ExpectLocalTime(tzif.get(), civil_second(2025, 8, 1, 0), + civil_second(2025, 7, 31, 23, 45), false, "GMT-0015"); +} + +TEST(TimeZoneWin, FixedOffset) { + // America/Phoenix + const WinTimeZoneRegistryInfo info = { + { + {420, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + }, + 0}; + auto tzif = cctz::MakeTimeZoneFromWinRegistry(info); + ExpectNoTransitionAfter(tzif.get(), 0); + ExpectLocalTime(tzif.get(), cs(2025, 8, 1, 0), cs(2025, 7, 31, 17), false, + "GMT-07"); +} + +TEST(TimeZoneWin, YearDependentDst) { + // America/Los_Angeles + const WinTimeZoneRegistryInfo info = { + { + // 2006 + {480, 0, -60, {0, 10, 0, 5, 2, 0, 0, 0}, {0, 4, 0, 1, 2, 0, 0, 0}}, + {480, 0, -60, {0, 11, 0, 1, 2, 0, 0, 0}, {0, 3, 0, 2, 2, 0, 0, 0}}, + // TZI + {480, 0, -60, {0, 11, 0, 1, 2, 0, 0, 0}, {0, 3, 0, 2, 2, 0, 0, 0}}, + }, + 2006}; + auto tzif = cctz::MakeTimeZoneFromWinRegistry(info); + ExpectLocalTime(tzif.get(), cs(2005, 3, 15, 0), cs(2005, 3, 14, 16), false, + "GMT-08"); + ExpectLocalTime(tzif.get(), cs(2006, 3, 15, 0), cs(2006, 3, 14, 16), false, + "GMT-08"); + ExpectLocalTime(tzif.get(), cs(2007, 3, 15, 0), cs(2007, 3, 14, 17), true, + "GMT-07"); +} + +TEST(TimeZoneWin, NonDSTtoDSTtoNonDST) { + // Asia/Ulaanbaatar + const std::vector entries = { + // 2014 + {-480, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + {-480, 0, -60, {0, 9, 5, 5, 23, 59, 59, 999}, {0, 3, 6, 5, 2, 0, 0, 0}}, + {-480, 0, -60, {0, 9, 5, 4, 23, 59, 59, 999}, {0, 3, 6, 5, 2, 0, 0, 0}}, + {-480, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + // TZI + {-480, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + }; + const WinTimeZoneRegistryInfo info = {entries, 2014}; + const std::vector next_transitions = { + {1427479200, cs(2015, 3, 28, 2), cs(2015, 3, 28, 3)}, + {1443193200, cs(2015, 9, 26, 0), cs(2015, 9, 25, 23)}, + {1458928800, cs(2016, 3, 26, 2), cs(2016, 3, 26, 3)}, + {1474642800, cs(2016, 9, 24, 0), cs(2016, 9, 23, 23)}, + }; + ExpectNextTransitions(tzif.get(), 2010, next_transitions); + ExpectNoTransitionAfter(tzif.get(), 1474642800); +} + +TEST(TimeZoneWin, DiscontinuousYearBoundary) { + // Europe/Volgograd + // https://techcommunity.microsoft.com/blog/dstblog/2020-time-zone-updates-for-volgograd-russia-now-available/2234995 + const WinTimeZoneRegistryInfo info = { + { + // 2010 + {-180, 0, -60, {0, 10, 0, 5, 3, 0, 0, 0}, {0, 3, 0, 5, 2, 0, 0, 0}}, + {-180, 0, -60, {0, 1, 6, 1, 0, 0, 0, 0}, {0, 3, 0, 5, 2, 0, 0, 0}}, + {-240, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + {-240, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + {-180, 0, -60, {0, 10, 0, 5, 2, 0, 0, 0}, {0, 1, 3, 1, 0, 0, 0, 0}}, + // 2015 + {-180, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + {-180, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + {-180, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + {-240, 0, 60, {0, 10, 0, 5, 2, 0, 0, 0}, {0, 1, 1, 1, 0, 0, 0, 0}}, + {-240, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + // 2020 + {-240, 0, -60, {0, 12, 0, 5, 2, 0, 0, 0}, {0, 1, 3, 1, 0, 0, 0, 0}}, + {-180, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + // TZI + {-180, 0, -60, {0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + }, + 2010}; + + auto tzif = cctz::MakeTimeZoneFromWinRegistry(info); + + // https://github.com/dotnet/runtime/issues/118915 + const std::vector expected = { + {1269730800, cs(2010, 3, 28, 2), cs(2010, 3, 28, 3)}, + {1288479600, cs(2010, 10, 31, 3), cs(2010, 10, 31, 2)}, + {1301180400, cs(2011, 3, 27, 2), cs(2011, 3, 27, 3)}, + {1414274400, cs(2014, 10, 26, 2), cs(2014, 10, 26, 1)}, + {1540681200, cs(2018, 10, 28, 2), cs(2018, 10, 28, 3)}, + {1577822400, cs(2020, 1, 1, 0), cs(2020, 1, 1, 1)}, // nonexistent + {1609016400, cs(2020, 12, 27, 2), cs(2020, 12, 27, 1)}, + {1609444800, cs(2021, 1, 1, 0), cs(2020, 12, 31, 23)}, // nonexistent + }; + ExpectNextTransitions(tzif.get(), 2010, expected); + ExpectNoTransitionAfter(tzif.get(), 1609444800); +} + +} // namespace +} // namespace cctz