Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
}),
Expand All @@ -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"],
Expand Down Expand Up @@ -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(
Expand Down
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
$<$<PLATFORM_ID:Windows>:src/time_zone_name_win.cc>
$<$<PLATFORM_ID:Windows>:src/time_zone_name_win.h>
$<$<PLATFORM_ID:Windows>:src/time_zone_win_loader.cc>
$<$<PLATFORM_ID:Windows>:src/time_zone_win_loader.h>
${CCTZ_HDRS}
)
cctz_target_set_cxx_standard(cctz)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 16 additions & 2 deletions src/time_zone_if.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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> TimeZoneIf::UTC() {
Expand All @@ -31,8 +36,17 @@ std::unique_ptr<TimeZoneIf> 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<TimeZoneIf> 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.
Expand Down
26 changes: 26 additions & 0 deletions src/time_zone_lookup_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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].
//
Expand Down
115 changes: 95 additions & 20 deletions src/time_zone_name_win.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> g_unavailable;
std::atomic<ucal_getTimeZoneIDForWindowsID_func>
g_ucal_getTimeZoneIDForWindowsID;
std::atomic<ucal_getWindowsTimeZoneID_func> g_ucal_getWindowsTimeZoneID;

template <typename T> static T AsProcAddress(HMODULE module, const char* name) {
static_assert(
Expand All @@ -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<ucal_getTimeZoneIDForWindowsID_func>(
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<ucal_getWindowsTimeZoneID_func>(
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
Expand All @@ -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<ucal_getTimeZoneIDForWindowsID_func>(
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
Expand Down Expand Up @@ -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<std::int32_t>::max()) {
return std::wstring();
}
const std::int32_t iana_name_length =
static_cast<std::int32_t>(iana_name.size());

std::wstring result;
std::size_t len = std::max<std::size_t>(
std::min<size_t>(result.capacity(), std::numeric_limits<int>::max()), 1);
for (;;) {
UErrorCode status = U_ZERO_ERROR;
result.resize(len);
len = static_cast<std::size_t>(getWindowsTimeZoneID(
iana_name.c_str(), iana_name_length, &result[0], static_cast<int>(len),
&status));
if (U_SUCCESS(status)) {
return result;
}
if (status != U_BUFFER_OVERFLOW_ERROR) {
return std::wstring();
}
}
}

} // namespace cctz
5 changes: 5 additions & 0 deletions src/time_zone_name_win.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Loading