sal/CppunitTest_sal_osl.mk | 1 sal/CppunitTest_sal_osl_file_details.mk | 3 sal/Library_sal.mk | 4 sal/osl/unx/file_stat.cxx | 19 -- sal/osl/unx/time.cxx | 198 +++++------------------ sal/osl/unx/tz.cxx | 224 ++++++++++++++++++++++++++ sal/osl/unx/tz.hxx | 37 ++++ sal/qa/osl/time/osl_Time.cxx | 273 ++++++++++++++++++++++++++++++++ 8 files changed, 587 insertions(+), 172 deletions(-)
New commits: commit 15d9d8380e0d9b212e949336e66529767e47adc5 Author: Michael Meeks <[email protected]> AuthorDate: Fri Feb 20 18:41:40 2026 +0200 Commit: Noel Grandin <[email protected]> CommitDate: Sat Feb 21 11:38:04 2026 +0100 sal: switch to internal ICU ucal_* API for Unix timezone pieces. COOL improvement - glibc drags in 3Mb or so + stacked file-system waste that has to be copied on jail setulp, and (it turns out) we are already shipping our own timezone database, so lets not do that twice. Create internal only tz:: wrappers of that, and simplify the impl. clean out unnecessary localtime from internal unx file debug logging. Add sal/qa/osl/time/osl_Time.cxx covering a number of corner cases. Change-Id: Icdd0f84eb0ce7c3b2268c555b8545d715e90655a Reviewed-on: https://gerrit.libreoffice.org/c/core/+/199826 Reviewed-by: Noel Grandin <[email protected]> Tested-by: Jenkins CollaboraOffice <[email protected]> diff --git a/sal/CppunitTest_sal_osl.mk b/sal/CppunitTest_sal_osl.mk index aede7abdd6d4..980a1f5d4081 100644 --- a/sal/CppunitTest_sal_osl.mk +++ b/sal/CppunitTest_sal_osl.mk @@ -24,6 +24,7 @@ $(eval $(call gb_CppunitTest_add_exception_objects,sal_osl,\ sal/qa/osl/profile/osl_old_testprofile \ sal/qa/osl/setthreadname/test-setthreadname \ sal/qa/osl/socket \ + sal/qa/osl/time/osl_Time \ )) $(eval $(call gb_CppunitTest_use_libraries,sal_osl,\ diff --git a/sal/CppunitTest_sal_osl_file_details.mk b/sal/CppunitTest_sal_osl_file_details.mk index dbd493109529..33de3cbd30d9 100644 --- a/sal/CppunitTest_sal_osl_file_details.mk +++ b/sal/CppunitTest_sal_osl_file_details.mk @@ -34,6 +34,9 @@ $(eval $(call gb_CppunitTest_set_include,sal_osl_file_details, \ )) $(eval $(call gb_CppunitTest_use_externals,sal_osl_file_details, \ + icu_headers \ + icui18n \ + icuuc \ zlib \ )) diff --git a/sal/Library_sal.mk b/sal/Library_sal.mk index dae08b235a41..c85c58ecb232 100644 --- a/sal/Library_sal.mk +++ b/sal/Library_sal.mk @@ -45,6 +45,9 @@ $(eval $(call gb_Library_use_libraries,sal,\ $(eval $(call gb_Library_use_externals,sal,\ dragonbox \ fast_float \ + icu_headers \ + icui18n \ + icuuc \ valgrind \ zlib \ )) @@ -182,6 +185,7 @@ $(eval $(call gb_Library_add_exception_objects,sal,\ sal/osl/unx/tempfile \ sal/osl/unx/thread \ sal/osl/unx/time \ + sal/osl/unx/tz \ )) # Note that the uunxapi.mm file just includes the uunxapi.cxx one. Ditto for system.mm diff --git a/sal/osl/unx/file_stat.cxx b/sal/osl/unx/file_stat.cxx index 3d0754883a11..a48a9096fa7e 100644 --- a/sal/osl/unx/file_stat.cxx +++ b/sal/osl/unx/file_stat.cxx @@ -338,9 +338,6 @@ static oslFileError osl_psz_setFileTime ( int nRet=0; struct utimbuf aTimeBuffer; struct stat aFileStat; -#ifdef DEBUG_OSL_FILE - struct tm* pTM=0; -#endif if (isForbidden(pszFilePath, osl_File_OpenFlag_Write)) return osl_File_E_ACCES; @@ -354,14 +351,6 @@ static oslFileError osl_psz_setFileTime ( } #ifdef DEBUG_OSL_FILE - fprintf(stderr,"File Times are (in localtime): "); - pTM=localtime(&aFileStat.st_ctime); - fprintf(stderr,"CreationTime is '%s' ",asctime(pTM)); - pTM=localtime(&aFileStat.st_atime); - fprintf(stderr,"AccessTime is '%s' ",asctime(pTM)); - pTM=localtime(&aFileStat.st_mtime); - fprintf(stderr,"Modification is '%s' ",asctime(pTM)); - fprintf(stderr,"File Times are (in UTC): "); fprintf(stderr,"CreationTime is '%s' ",ctime(&aFileStat.st_ctime)); fprintf(stderr,"AccessTime is '%s' ",ctime(&aTimeBuffer.actime)); @@ -389,14 +378,6 @@ static oslFileError osl_psz_setFileTime ( /* mfe: Creation time not used here! */ #ifdef DEBUG_OSL_FILE - fprintf(stderr,"File Times are (in localtime): "); - pTM=localtime(&aFileStat.st_ctime); - fprintf(stderr,"CreationTime now '%s' ",asctime(pTM)); - pTM=localtime(&aTimeBuffer.actime); - fprintf(stderr,"AccessTime now '%s' ",asctime(pTM)); - pTM=localtime(&aTimeBuffer.modtime); - fprintf(stderr,"Modification now '%s' ",asctime(pTM)); - fprintf(stderr,"File Times are (in UTC): "); fprintf(stderr,"CreationTime now '%s' ",ctime(&aFileStat.st_ctime)); fprintf(stderr,"AccessTime now '%s' ",ctime(&aTimeBuffer.actime)); diff --git a/sal/osl/unx/time.cxx b/sal/osl/unx/time.cxx index cf5473ff24bf..e4bac084da25 100644 --- a/sal/osl/unx/time.cxx +++ b/sal/osl/unx/time.cxx @@ -20,7 +20,9 @@ #include <sal/config.h> #include "saltime.hxx" +#include "tz.hxx" +#include <cstdint> #include <osl/time.h> #include <time.h> #include <unistd.h> @@ -30,15 +32,6 @@ #include <mach/mach.h> #endif -/* FIXME: detection should be done in configure script */ -#if defined(MACOSX) || defined(IOS) || defined(FREEBSD) || defined(NETBSD) || \ - defined(LINUX) || defined(OPENBSD) || defined(DRAGONFLY) -#define STRUCT_TM_HAS_GMTOFF 1 - -#elif defined(__sun) -#define HAS_ALTZONE 1 -#endif - #ifdef __MACH__ typedef mach_timespec_t osl_time_t; #else @@ -89,173 +82,72 @@ sal_Bool SAL_CALL osl_getSystemTime(TimeValue* tv) sal_Bool SAL_CALL osl_getDateTimeFromTimeValue( const TimeValue* pTimeVal, oslDateTime* pDateTime ) { - struct tm *pSystemTime; - struct tm tmBuf; - time_t atime; - - atime = static_cast<time_t>(pTimeVal->Seconds); + osl::tz::BrokenDown bd; + if (!osl::tz::epochToUtc(static_cast<std::int64_t>(pTimeVal->Seconds), bd)) + return false; - /* Convert time from type time_t to struct tm */ - pSystemTime = gmtime_r( &atime, &tmBuf ); + pDateTime->NanoSeconds = pTimeVal->Nanosec; + pDateTime->Seconds = bd.second; + pDateTime->Minutes = bd.minute; + pDateTime->Hours = bd.hour; + pDateTime->Day = bd.day; + pDateTime->DayOfWeek = bd.dayOfWeek; + pDateTime->Month = bd.month; + pDateTime->Year = bd.year; - /* Convert struct tm to struct oslDateTime */ - if ( pSystemTime != nullptr ) - { - pDateTime->NanoSeconds = pTimeVal->Nanosec; - pDateTime->Seconds = pSystemTime->tm_sec; - pDateTime->Minutes = pSystemTime->tm_min; - pDateTime->Hours = pSystemTime->tm_hour; - pDateTime->Day = pSystemTime->tm_mday; - pDateTime->DayOfWeek = pSystemTime->tm_wday; - pDateTime->Month = pSystemTime->tm_mon + 1; - pDateTime->Year = pSystemTime->tm_year + 1900; - - return true; - } - - return false; + return true; } sal_Bool SAL_CALL osl_getTimeValueFromDateTime( const oslDateTime* pDateTime, TimeValue* pTimeVal ) { - struct tm aTime; - time_t nSeconds; - - /* Convert struct oslDateTime to struct tm */ - aTime.tm_sec = pDateTime->Seconds; - aTime.tm_min = pDateTime->Minutes; - aTime.tm_hour = pDateTime->Hours; - aTime.tm_mday = pDateTime->Day; - - if ( pDateTime->Month > 0 ) - aTime.tm_mon = pDateTime->Month - 1; - else + /* The API says pDateTime is in GMT, so this is a pure UTC-to-epoch conversion. */ + osl::tz::BrokenDown bd; + bd.year = pDateTime->Year; + bd.month = pDateTime->Month; + bd.day = pDateTime->Day; + bd.hour = pDateTime->Hours; + bd.minute = pDateTime->Minutes; + bd.second = pDateTime->Seconds; + bd.dayOfWeek = 0; // unused for this conversion + + std::int64_t epoch; + if (!osl::tz::utcToEpoch(bd, epoch)) return false; - aTime.tm_year = pDateTime->Year - 1900; - - aTime.tm_isdst = -1; - aTime.tm_wday = 0; - aTime.tm_yday = 0; - -#if defined(STRUCT_TM_HAS_GMTOFF) - aTime.tm_gmtoff = 0; -#endif - - /* Convert time to calendar value */ - nSeconds = mktime( &aTime ); - - /* - * mktime expects the struct tm to be in local timezone, so we have to adjust - * the returned value to be timezone neutral. - */ - - if ( nSeconds != time_t(-1) ) - { - time_t bias; - - /* timezone corrections */ - tzset(); - -#if defined(STRUCT_TM_HAS_GMTOFF) - /* members of struct tm are corrected by mktime */ - bias = 0 - aTime.tm_gmtoff; - -#elif defined(HAS_ALTZONE) - /* check if daylight saving time is in effect */ - bias = aTime.tm_isdst > 0 ? altzone : timezone; -#else - /* expect daylight saving time to be one hour */ - bias = aTime.tm_isdst > 0 ? timezone - 3600 : timezone; -#endif + pTimeVal->Seconds = static_cast<sal_uInt32>(epoch); + pTimeVal->Nanosec = pDateTime->NanoSeconds; - // coverity[store_truncates_time_t] - TODO: sal_uInt32 TimeValue::Seconds is only large - // enough for integer time_t values < 2^32 representing dates until year 2106: - pTimeVal->Seconds = nSeconds; - pTimeVal->Nanosec = pDateTime->NanoSeconds; - - if ( nSeconds > bias ) - pTimeVal->Seconds -= bias; - - return true; - } - - return false; + return true; } sal_Bool SAL_CALL osl_getLocalTimeFromSystemTime( const TimeValue* pSystemTimeVal, TimeValue* pLocalTimeVal ) { - struct tm *pLocalTime; - struct tm tmBuf; - time_t bias; - time_t atime; - - atime = static_cast<time_t>(pSystemTimeVal->Seconds); - pLocalTime = localtime_r( &atime, &tmBuf ); - -#if defined(STRUCT_TM_HAS_GMTOFF) - /* members of struct tm are corrected by mktime */ - bias = -pLocalTime->tm_gmtoff; - -#elif defined(HAS_ALTZONE) - /* check if daylight saving time is in effect */ - bias = pLocalTime->tm_isdst > 0 ? altzone : timezone; -#else - /* expect daylight saving time to be one hour */ - bias = pLocalTime->tm_isdst > 0 ? timezone - 3600 : timezone; -#endif + std::int64_t utcEpoch = static_cast<std::int64_t>(pSystemTimeVal->Seconds); + std::int32_t offset = osl::tz::getUtcOffsetForUtcTime(utcEpoch); + std::int64_t localEpoch = utcEpoch + offset; - if ( static_cast<sal_Int64>(pSystemTimeVal->Seconds) > bias ) - { - pLocalTimeVal->Seconds = pSystemTimeVal->Seconds - bias; - pLocalTimeVal->Nanosec = pSystemTimeVal->Nanosec; + if (localEpoch < 0) + return false; - return true; - } + pLocalTimeVal->Seconds = static_cast<sal_uInt32>(localEpoch); + pLocalTimeVal->Nanosec = pSystemTimeVal->Nanosec; - return false; + return true; } sal_Bool SAL_CALL osl_getSystemTimeFromLocalTime( const TimeValue* pLocalTimeVal, TimeValue* pSystemTimeVal ) { - struct tm *pLocalTime; - struct tm tmBuf; - time_t bias; - time_t atime; - - atime = static_cast<time_t>(pLocalTimeVal->Seconds); - - /* Convert atime, which is a local time, to its GMT equivalent. Then, get - * the timezone offset for the local time for the GMT equivalent time. Note - * that we cannot directly use local time to determine the timezone offset - * because GMT is the only reliable time that we can determine timezone - * offset from. - */ - - atime = mktime( gmtime_r( &atime, &tmBuf ) ); - pLocalTime = localtime_r( &atime, &tmBuf ); - -#if defined(STRUCT_TM_HAS_GMTOFF) - /* members of struct tm are corrected by mktime */ - bias = 0 - pLocalTime->tm_gmtoff; - -#elif defined(HAS_ALTZONE) - /* check if daylight saving time is in effect */ - bias = pLocalTime->tm_isdst > 0 ? altzone : timezone; -#else - /* expect daylight saving time to be one hour */ - bias = pLocalTime->tm_isdst > 0 ? timezone - 3600 : timezone; -#endif + std::int64_t localEpoch = static_cast<std::int64_t>(pLocalTimeVal->Seconds); + std::int32_t offset = osl::tz::getUtcOffsetForLocalTime(localEpoch); + std::int64_t utcEpoch = localEpoch - offset; - if ( static_cast<sal_Int64>(pLocalTimeVal->Seconds) + bias > 0 ) - { - pSystemTimeVal->Seconds = pLocalTimeVal->Seconds + bias; - pSystemTimeVal->Nanosec = pLocalTimeVal->Nanosec; + if (utcEpoch < 0) + return false; - return true; - } + pSystemTimeVal->Seconds = static_cast<sal_uInt32>(utcEpoch); + pSystemTimeVal->Nanosec = pLocalTimeVal->Nanosec; - return false; + return true; } void sal_initGlobalTimer() diff --git a/sal/osl/unx/tz.cxx b/sal/osl/unx/tz.cxx new file mode 100644 index 000000000000..5259ce86c521 --- /dev/null +++ b/sal/osl/unx/tz.cxx @@ -0,0 +1,224 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <sal/config.h> + +#include "tz.hxx" + +#include <unicode/ucal.h> +#include <cstdlib> +#include <cstring> +#include <mutex> +#include <string> + +namespace +{ +std::mutex g_mutex; + +// UTC calendar - cached, timezone-independent. +UCalendar* g_utcCal = nullptr; +bool g_utcCalInitialized = false; + +// Local-timezone calendar - recreated when $TZ changes. +UCalendar* g_localCal = nullptr; +std::string g_cachedTZ; +bool g_localCalInitialized = false; + +const UChar g_utcId[] = { 'U', 'T', 'C', 0 }; + +UCalendar* getUtcCalendar() +{ + if (!g_utcCalInitialized) + { + UErrorCode status = U_ZERO_ERROR; + g_utcCal = ucal_open(g_utcId, 3, nullptr, UCAL_GREGORIAN, &status); + g_utcCalInitialized = true; + if (U_FAILURE(status)) + g_utcCal = nullptr; + } + return g_utcCal; +} + +/// Return current $TZ value, or empty string if unset. +std::string getCurrentTZ() +{ + const char* tz = std::getenv("TZ"); + return tz ? std::string(tz) : std::string(); +} + +UCalendar* getLocalCalendar() +{ + std::string currentTZ = getCurrentTZ(); + + // If we already have a calendar and TZ hasn't changed, reuse it. + if (g_localCal && g_localCalInitialized && currentTZ == g_cachedTZ) + return g_localCal; + + // TZ changed (or first call) - (re)create the calendar. + if (g_localCal) + { + ucal_close(g_localCal); + g_localCal = nullptr; + } + + UErrorCode status = U_ZERO_ERROR; + + if (currentTZ.empty()) + { + // No TZ set - use system default. + g_localCal = ucal_open(nullptr, 0, nullptr, UCAL_GREGORIAN, &status); + } + else + { + // Strip leading ':' if present (Linux convention). + const char* tzStr = currentTZ.c_str(); + if (tzStr[0] == ':') + ++tzStr; + + // Convert ASCII timezone ID to UChar for ICU. + size_t len = std::strlen(tzStr); + std::basic_string<UChar> uTZ(len, 0); + for (size_t i = 0; i < len; ++i) + uTZ[i] = static_cast<UChar>(tzStr[i]); + + g_localCal = ucal_open(uTZ.data(), static_cast<int32_t>(uTZ.size()), nullptr, + UCAL_GREGORIAN, &status); + } + + g_localCalInitialized = true; + g_cachedTZ = currentTZ; + + if (U_FAILURE(status)) + g_localCal = nullptr; + + return g_localCal; +} + +} // anonymous namespace + +namespace osl::tz +{ +bool epochToUtc(std::int64_t epochSec, BrokenDown& out) +{ + std::lock_guard lock(g_mutex); + UCalendar* cal = getUtcCalendar(); + if (!cal) + return false; + + UErrorCode status = U_ZERO_ERROR; + UDate utcMs = static_cast<double>(epochSec) * 1000.0; + ucal_setMillis(cal, utcMs, &status); + if (U_FAILURE(status)) + return false; + + status = U_ZERO_ERROR; + out.year = ucal_get(cal, UCAL_YEAR, &status); + if (U_FAILURE(status)) + return false; + + status = U_ZERO_ERROR; + out.month = ucal_get(cal, UCAL_MONTH, &status) + 1; // ICU months are 0-based + if (U_FAILURE(status)) + return false; + + status = U_ZERO_ERROR; + out.day = ucal_get(cal, UCAL_DAY_OF_MONTH, &status); + if (U_FAILURE(status)) + return false; + + status = U_ZERO_ERROR; + out.hour = ucal_get(cal, UCAL_HOUR_OF_DAY, &status); + if (U_FAILURE(status)) + return false; + + status = U_ZERO_ERROR; + out.minute = ucal_get(cal, UCAL_MINUTE, &status); + if (U_FAILURE(status)) + return false; + + status = U_ZERO_ERROR; + out.second = ucal_get(cal, UCAL_SECOND, &status); + if (U_FAILURE(status)) + return false; + + status = U_ZERO_ERROR; + out.dayOfWeek = ucal_get(cal, UCAL_DAY_OF_WEEK, &status) - 1; // ICU: Sunday=1, we want Sunday=0 + if (U_FAILURE(status)) + return false; + + return true; +} + +bool utcToEpoch(const BrokenDown& bd, std::int64_t& outEpoch) +{ + if (bd.month < 1 || bd.month > 12) + return false; + + std::lock_guard lock(g_mutex); + UCalendar* cal = getUtcCalendar(); + if (!cal) + return false; + + UErrorCode status = U_ZERO_ERROR; + ucal_setDateTime(cal, bd.year, bd.month - 1, // ICU months are 0-based + bd.day, bd.hour, bd.minute, bd.second, &status); + if (U_FAILURE(status)) + return false; + + status = U_ZERO_ERROR; + UDate ms = ucal_getMillis(cal, &status); + if (U_FAILURE(status)) + return false; + + outEpoch = static_cast<std::int64_t>(ms / 1000.0); + return true; +} + +std::int32_t getUtcOffsetForUtcTime(std::int64_t utcEpochSec) +{ + std::lock_guard lock(g_mutex); + UCalendar* cal = getLocalCalendar(); + if (!cal) + return 0; + + UErrorCode status = U_ZERO_ERROR; + UDate utcMs = static_cast<double>(utcEpochSec) * 1000.0; + ucal_setMillis(cal, utcMs, &status); + if (U_FAILURE(status)) + return 0; + + status = U_ZERO_ERROR; + std::int32_t zoneOffset = ucal_get(cal, UCAL_ZONE_OFFSET, &status); + if (U_FAILURE(status)) + return 0; + + status = U_ZERO_ERROR; + std::int32_t dstOffset = ucal_get(cal, UCAL_DST_OFFSET, &status); + if (U_FAILURE(status)) + return 0; + + return (zoneOffset + dstOffset) / 1000; // ms -> seconds +} + +std::int32_t getUtcOffsetForLocalTime(std::int64_t localEpochSec) +{ + // Two-iteration refinement to handle DST ambiguity: + // 1. Get offset assuming localEpochSec is UTC + std::int32_t offset1 = getUtcOffsetForUtcTime(localEpochSec); + // 2. Refine: get offset at the estimated UTC time + std::int32_t offset2 = getUtcOffsetForUtcTime(localEpochSec - offset1); + // 3. If they agree, we're done. Otherwise, one more iteration. + if (offset1 == offset2) + return offset2; + return getUtcOffsetForUtcTime(localEpochSec - offset2); +} + +} // namespace osl::tz + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sal/osl/unx/tz.hxx b/sal/osl/unx/tz.hxx new file mode 100644 index 000000000000..0b4a272a6110 --- /dev/null +++ b/sal/osl/unx/tz.hxx @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include <sal/config.h> +#include <cstdint> + +namespace osl::tz +{ +struct BrokenDown +{ + int year, month, day, hour, minute, second, dayOfWeek; +}; + +/// UTC epoch seconds -> broken-down UTC. Pure math (no tz data). Replaces gmtime_r. +bool epochToUtc(std::int64_t epochSec, BrokenDown& out); + +/// Broken-down UTC -> epoch seconds. Pure math (no tz data). Replaces timegm. +bool utcToEpoch(const BrokenDown& bd, std::int64_t& outEpoch); + +/// UTC offset in seconds for a UTC timestamp. Positive = east of Greenwich. +/// Uses ICU ucal_* API with default timezone. Returns 0 on error (UTC fallback). +std::int32_t getUtcOffsetForUtcTime(std::int64_t utcEpochSec); + +/// UTC offset in seconds for a local timestamp (iterative refinement for DST). +std::int32_t getUtcOffsetForLocalTime(std::int64_t localEpochSec); + +} // namespace osl::tz + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sal/qa/osl/time/osl_Time.cxx b/sal/qa/osl/time/osl_Time.cxx new file mode 100644 index 000000000000..380df5f7ecab --- /dev/null +++ b/sal/qa/osl/time/osl_Time.cxx @@ -0,0 +1,273 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <sal/types.h> +#include <osl/time.h> + +#include <cstdlib> +#include <string> + +#include <cppunit/TestFixture.h> +#include <cppunit/extensions/HelperMacros.h> +#include <cppunit/plugin/TestPlugIn.h> + +namespace +{ +/// RAII helper to set $TZ and restore it on destruction. +class TZGuard +{ + std::string m_oldTZ; + bool m_hadTZ; + +public: + explicit TZGuard(const char* tz) + { + const char* old = std::getenv("TZ"); + m_hadTZ = (old != nullptr); + if (m_hadTZ) + m_oldTZ = old; + setenv("TZ", tz, 1); + } + ~TZGuard() + { + if (m_hadTZ) + setenv("TZ", m_oldTZ.c_str(), 1); + else + unsetenv("TZ"); + } +}; + +/// Helper: make a TimeValue from a known UTC date/time via osl API. +TimeValue makeUtcTimeValue(sal_Int16 year, sal_uInt16 month, sal_uInt16 day, sal_uInt16 hour, + sal_uInt16 minute, sal_uInt16 second) +{ + oslDateTime dt{}; + dt.Year = year; + dt.Month = month; + dt.Day = day; + dt.Hours = hour; + dt.Minutes = minute; + dt.Seconds = second; + dt.NanoSeconds = 0; + + TimeValue tv{}; + CPPUNIT_ASSERT(osl_getTimeValueFromDateTime(&dt, &tv)); + return tv; +} + +class TimeTest : public CppUnit::TestFixture +{ +public: + // --- epochToUtc / utcToEpoch round-trip via osl public API --- + + void testEpochRoundTrip() + { + // 1970-01-01 00:00:00 UTC = epoch 0 + TimeValue tv{}; + tv.Seconds = 0; + tv.Nanosec = 0; + oslDateTime dt{}; + CPPUNIT_ASSERT(osl_getDateTimeFromTimeValue(&tv, &dt)); + CPPUNIT_ASSERT_EQUAL(sal_Int16(1970), dt.Year); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(1), dt.Month); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(1), dt.Day); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(0), dt.Hours); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(4), dt.DayOfWeek); // Thursday + + // And back + TimeValue tv2{}; + CPPUNIT_ASSERT(osl_getTimeValueFromDateTime(&dt, &tv2)); + CPPUNIT_ASSERT_EQUAL(tv.Seconds, tv2.Seconds); + } + + void testKnownDates() + { + // 2000-02-29 12:00:00 UTC (leap day) + { + TimeValue tv = makeUtcTimeValue(2000, 2, 29, 12, 0, 0); + oslDateTime dt{}; + CPPUNIT_ASSERT(osl_getDateTimeFromTimeValue(&tv, &dt)); + CPPUNIT_ASSERT_EQUAL(sal_Int16(2000), dt.Year); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(2), dt.Month); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(29), dt.Day); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(12), dt.Hours); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(2), dt.DayOfWeek); // Tuesday + } + + // 2038-01-19 03:14:07 UTC (Y2K38) + { + TimeValue tv = makeUtcTimeValue(2038, 1, 19, 3, 14, 7); + oslDateTime dt{}; + CPPUNIT_ASSERT(osl_getDateTimeFromTimeValue(&tv, &dt)); + CPPUNIT_ASSERT_EQUAL(sal_Int16(2038), dt.Year); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(1), dt.Month); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(19), dt.Day); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(3), dt.Hours); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(14), dt.Minutes); + CPPUNIT_ASSERT_EQUAL(sal_uInt16(7), dt.Seconds); + } + } + + void testNanoSecPreserved() + { + TimeValue tv = makeUtcTimeValue(2024, 6, 15, 10, 30, 0); + tv.Nanosec = 123456789; + + oslDateTime dt{}; + CPPUNIT_ASSERT(osl_getDateTimeFromTimeValue(&tv, &dt)); + CPPUNIT_ASSERT_EQUAL(sal_uInt32(123456789), dt.NanoSeconds); + + TimeValue tv2{}; + CPPUNIT_ASSERT(osl_getTimeValueFromDateTime(&dt, &tv2)); + CPPUNIT_ASSERT_EQUAL(sal_uInt32(123456789), tv2.Nanosec); + } + + // --- Timezone offset tests using $TZ --- + + void testNewYorkWinter() + { + TZGuard tz("America/New_York"); + + // 2024-01-15 12:00:00 UTC -> EST = UTC-5 + TimeValue utcTv = makeUtcTimeValue(2024, 1, 15, 12, 0, 0); + TimeValue localTv{}; + CPPUNIT_ASSERT(osl_getLocalTimeFromSystemTime(&utcTv, &localTv)); + + sal_Int64 offsetSec + = static_cast<sal_Int64>(localTv.Seconds) - static_cast<sal_Int64>(utcTv.Seconds); + CPPUNIT_ASSERT_EQUAL(sal_Int64(-5 * 3600), offsetSec); + } + + void testNewYorkSummer() + { + TZGuard tz("America/New_York"); + + // 2024-07-15 12:00:00 UTC -> EDT = UTC-4 + TimeValue utcTv = makeUtcTimeValue(2024, 7, 15, 12, 0, 0); + TimeValue localTv{}; + CPPUNIT_ASSERT(osl_getLocalTimeFromSystemTime(&utcTv, &localTv)); + + sal_Int64 offsetSec + = static_cast<sal_Int64>(localTv.Seconds) - static_cast<sal_Int64>(utcTv.Seconds); + CPPUNIT_ASSERT_EQUAL(sal_Int64(-4 * 3600), offsetSec); + } + + void testTokyo() + { + TZGuard tz("Asia/Tokyo"); + + // 2024-01-15 12:00:00 UTC -> JST = UTC+9 (no DST) + TimeValue utcTv = makeUtcTimeValue(2024, 1, 15, 12, 0, 0); + TimeValue localTv{}; + CPPUNIT_ASSERT(osl_getLocalTimeFromSystemTime(&utcTv, &localTv)); + + sal_Int64 offsetSec + = static_cast<sal_Int64>(localTv.Seconds) - static_cast<sal_Int64>(utcTv.Seconds); + CPPUNIT_ASSERT_EQUAL(sal_Int64(9 * 3600), offsetSec); + } + + void testUTC() + { + TZGuard tz("UTC"); + + TimeValue utcTv = makeUtcTimeValue(2024, 6, 15, 12, 0, 0); + TimeValue localTv{}; + CPPUNIT_ASSERT(osl_getLocalTimeFromSystemTime(&utcTv, &localTv)); + + CPPUNIT_ASSERT_EQUAL(utcTv.Seconds, localTv.Seconds); + } + + void testIndia() + { + TZGuard tz("Asia/Kolkata"); + + // UTC+5:30 - tests half-hour offset + TimeValue utcTv = makeUtcTimeValue(2024, 3, 1, 0, 0, 0); + TimeValue localTv{}; + CPPUNIT_ASSERT(osl_getLocalTimeFromSystemTime(&utcTv, &localTv)); + + sal_Int64 offsetSec + = static_cast<sal_Int64>(localTv.Seconds) - static_cast<sal_Int64>(utcTv.Seconds); + CPPUNIT_ASSERT_EQUAL(sal_Int64(5 * 3600 + 30 * 60), offsetSec); + } + + // --- Round-trip: local -> system -> local --- + + void testLocalSystemRoundTrip() + { + TZGuard tz("America/Chicago"); + + // 2024-06-15 12:00:00 UTC (CDT, UTC-5) + TimeValue utcTv = makeUtcTimeValue(2024, 6, 15, 12, 0, 0); + + TimeValue localTv{}; + CPPUNIT_ASSERT(osl_getLocalTimeFromSystemTime(&utcTv, &localTv)); + + TimeValue utcTv2{}; + CPPUNIT_ASSERT(osl_getSystemTimeFromLocalTime(&localTv, &utcTv2)); + + CPPUNIT_ASSERT_EQUAL(utcTv.Seconds, utcTv2.Seconds); + } + + // --- TZ switching within a single test --- + + void testTZSwitching() + { + TimeValue utcTv = makeUtcTimeValue(2024, 1, 15, 12, 0, 0); + + // First: New York (EST, UTC-5) + { + TZGuard tz("America/New_York"); + TimeValue localTv{}; + CPPUNIT_ASSERT(osl_getLocalTimeFromSystemTime(&utcTv, &localTv)); + sal_Int64 offset + = static_cast<sal_Int64>(localTv.Seconds) - static_cast<sal_Int64>(utcTv.Seconds); + CPPUNIT_ASSERT_EQUAL(sal_Int64(-5 * 3600), offset); + } + + // Switch to Tokyo (JST, UTC+9) + { + TZGuard tz("Asia/Tokyo"); + TimeValue localTv{}; + CPPUNIT_ASSERT(osl_getLocalTimeFromSystemTime(&utcTv, &localTv)); + sal_Int64 offset + = static_cast<sal_Int64>(localTv.Seconds) - static_cast<sal_Int64>(utcTv.Seconds); + CPPUNIT_ASSERT_EQUAL(sal_Int64(9 * 3600), offset); + } + + // Switch to Berlin (CET, UTC+1 in winter) + { + TZGuard tz("Europe/Berlin"); + TimeValue localTv{}; + CPPUNIT_ASSERT(osl_getLocalTimeFromSystemTime(&utcTv, &localTv)); + sal_Int64 offset + = static_cast<sal_Int64>(localTv.Seconds) - static_cast<sal_Int64>(utcTv.Seconds); + CPPUNIT_ASSERT_EQUAL(sal_Int64(1 * 3600), offset); + } + } + + CPPUNIT_TEST_SUITE(TimeTest); + CPPUNIT_TEST(testEpochRoundTrip); + CPPUNIT_TEST(testKnownDates); + CPPUNIT_TEST(testNanoSecPreserved); + CPPUNIT_TEST(testNewYorkWinter); + CPPUNIT_TEST(testNewYorkSummer); + CPPUNIT_TEST(testTokyo); + CPPUNIT_TEST(testUTC); + CPPUNIT_TEST(testIndia); + CPPUNIT_TEST(testLocalSystemRoundTrip); + CPPUNIT_TEST(testTZSwitching); + CPPUNIT_TEST_SUITE_END(); +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TimeTest); + +} // namespace + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
