From fcbfaae35d9cc1d6756f53eb7657b48182950b11 Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Mon, 1 Dec 2025 21:31:07 +0900 Subject: [PATCH] Implement `TimeZoneIf` with Windows Registry This commit introduces a new implementation of the TimeZoneIf interface that is compatible with Windows Time APIs. Background: Here's a quick comparison of how time zone libraries are implemented on modern Windows: * in Microsoft STL -> Uses system "icu.dll" [1] if available [2]. * System.TimeZoneInfo in .NET -> Uses Windows Registry as the source of time zone information. * CCTZ -> Uses zoneinfo if found in TZDIR or ZoneInfoSourceFactory. This commit brings what .NET does to CCTZ. The major advantages of using Windows Registry instead of "icu.dll" are: * It gives the full consistency with the Windows Time APIs. * The data has proven to be updated via Windows Update in a good cadence [3]. For instance, tzdb 2022b is still used in "icu.dll" shipped with Windows 11 24H2. Implementation Note: Windows uses per-year time zone information as follows [4]: struct REG_TZI_FORMAT { // Base offset in minutes, where UTC == local time + Bias. LONG Bias; // Additional offset in minutes applied to standard time. LONG StandardBias; // Additional offset in minutes applied to DST. LONG DaylightBias; // Localtime (in the previous offset) when the standard time begins. SYSTEMTIME StandardDate; // Localtime (in the previous offset) when the DST begins. SYSTEMTIME DaylightDate; }; For each time zone ID, one default REG_TZI_FORMAT entry and optional year-keyed REG_TZI_FORMAT entries are stored under the following registry keys: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\ Years that are not explicitly covered by a year-keyed entry are governed by the default entry or the earliest year-keyed entry, respectively. SYSTEMTIME is defined as follows: struct SYSTEMTIME { WORD wYear; WORD wMonth; WORD wDayOfWeek; WORD wDay; WORD wHour; WORD wMinute; WORD wSecond; WORD wMilliseconds; }; There are special rules of SYSTEMTIME used in REG_TZI_FORMAT [4]: * If wMonth is zero, the transition does not occur. * If wYear is not zero, the transition date is absolute. * If wYear is zero and wMonth is not zero, the transition rule is recurring every year based on wDayOfWeek. wDay indicates the occurrence of the day of the week within the month. wDay == 5 means the final occurrence during the month. * All fields are in the local time in the previous offset rather than the new offset. * (wHour, wMinute, wSecond, wMilliseconds) == (23, 59, 59, 999) is a special case that indicates the transition occurs at the end of the day (i.e., 00:00:00.000 of the next day) [5]. With above, we should be able to implement the TimeZoneIf interface. Note that only the data loading steps from the Windows Registry need to be Windows-specific. The rest of the implementation is designed to be platform-independent in case we want to perform further testing and code analysis as needed. To run the basic tests, run the following command. bazelisk test //:time_zone_win_test How to build: To actually use it as the fallback mechanism, CCTZ_USE_WIN_REGISTRY_FALLBACK macro needs to be defined when building cctz on Windows. bazelisk run :time_tool \ --cxxopt=/DCCTZ_USE_WIN_REGISTRY_FALLBACK bazelisk test //:time_zone_lookup_test \ --cxxopt=/DCCTZ_USE_WIN_REGISTRY_FALLBACK [1]: https://learn.microsoft.com/en-us/windows/win32/intl/international-components-for-unicode--icu- [2]: Put PR link to MS STL 1789 here. [3]: https://techcommunity.microsoft.com/category/windows/blog/dstblog [4]: https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information [5]: https://stackoverflow.com/a/47106207 --- BUILD | 17 + CMakeLists.txt | 16 + README.md | 9 +- src/time_zone_if.cc | 18 +- src/time_zone_lookup_test.cc | 26 ++ src/time_zone_name_win.cc | 115 +++++- src/time_zone_name_win.h | 5 + src/time_zone_win.cc | 722 +++++++++++++++++++++++++++++++++++ src/time_zone_win.h | 121 ++++++ src/time_zone_win_loader.cc | 245 ++++++++++++ src/time_zone_win_loader.h | 30 ++ src/time_zone_win_test.cc | 204 ++++++++++ 12 files changed, 1502 insertions(+), 26 deletions(-) create mode 100644 src/time_zone_win.cc create mode 100644 src/time_zone_win.h create mode 100644 src/time_zone_win_loader.cc create mode 100644 src/time_zone_win_loader.h create mode 100644 src/time_zone_win_test.cc diff --git a/BUILD b/BUILD index 14d6b971..df0ba420 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 f8bfbdda..119caaf9 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 eeaa0160..055cfef5 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 2de47ff5..61c27f73 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 ffcbcaf1..36387d4f 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 3dcdfd71..3c3c15df 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 4a9de03d..a273bff9 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 00000000..d0aa5372 --- /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 00000000..c87e0456 --- /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 00000000..8e88cbbc --- /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 00000000..2f485200 --- /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 00000000..14cb0347 --- /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