diff --git a/CMakeLists.txt b/CMakeLists.txt index f53375a..c10c755 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -713,6 +713,7 @@ set(APP_HEADERS src/util/serial_job_lane.h src/util/fetch_backoff.h src/util/coalescing_writer.h + src/util/local_time.h src/util/background_sampler.h src/util/amount_format.h src/util/base64.h diff --git a/src/ui/windows/chat_tab.cpp b/src/ui/windows/chat_tab.cpp index 146a288..1a87041 100644 --- a/src/ui/windows/chat_tab.cpp +++ b/src/ui/windows/chat_tab.cpp @@ -10,6 +10,7 @@ #include "../../data/address_book.h" #include "../../chat/chat_service.h" #include "../../util/i18n.h" +#include "../../util/local_time.h" // reentrant localtime (chat day keys / time labels) #include "../../util/address_validation.h" // isShieldedAddress — chat requires a z-address recipient #include "../../util/platform.h" // getConfigDir + writeFileAtomically — conversation export (Q11) #include "../../config/settings.h" // per-conversation mute (Q10) @@ -134,11 +135,10 @@ std::string shorten(const std::string& s, std::size_t head = 12, std::size_t tai std::string formatTime(std::int64_t ts) { if (ts <= 0) return ""; - std::time_t t = static_cast(ts); - std::tm* tm = std::localtime(&t); // UI thread only - if (!tm) return ""; + std::tm tmv; + if (!util::localTime(static_cast(ts), tmv)) return ""; char buf[40]; - std::strftime(buf, sizeof(buf), s_chat_time_12h ? "%Y-%m-%d %I:%M %p" : "%Y-%m-%d %H:%M", tm); + std::strftime(buf, sizeof(buf), s_chat_time_12h ? "%Y-%m-%d %I:%M %p" : "%Y-%m-%d %H:%M", &tmv); return buf; } @@ -146,21 +146,19 @@ std::string formatTime(std::int64_t ts) { // now lives in a per-day separator, so the group header doesn't repeat it). UI thread only. std::string chatTimeOnly(std::int64_t ts) { if (ts <= 0) return ""; - std::time_t t = static_cast(ts); - std::tm* tm = std::localtime(&t); - if (!tm) return ""; + std::tm tmv; + if (!util::localTime(static_cast(ts), tmv)) return ""; char buf[24]; - std::strftime(buf, sizeof(buf), s_chat_time_12h ? "%I:%M %p" : "%H:%M", tm); + std::strftime(buf, sizeof(buf), s_chat_time_12h ? "%I:%M %p" : "%H:%M", &tmv); return buf; } // Local calendar-day key (YYYYMMDD) for detecting day boundaries between messages. UI thread only. std::int64_t chatDayKey(std::int64_t ts) { - std::time_t t = static_cast(ts); - std::tm* tm = std::localtime(&t); - if (!tm) return -1; - return static_cast(tm->tm_year + 1900) * 10000 + - static_cast(tm->tm_mon + 1) * 100 + tm->tm_mday; + std::tm tmv; + if (!util::localTime(static_cast(ts), tmv)) return -1; + return static_cast(tmv.tm_year + 1900) * 10000 + + static_cast(tmv.tm_mon + 1) * 100 + tmv.tm_mday; } // Date-separator label: "Today" / "Yesterday" / "Jul 16, 2026". UI thread only. @@ -168,11 +166,10 @@ std::string chatDaySeparator(std::int64_t ts, std::int64_t todayKey, std::int64_ const std::int64_t k = chatDayKey(ts); if (k == todayKey) return TR("chat_today"); if (k == yestKey) return TR("chat_yesterday"); - std::time_t t = static_cast(ts); - std::tm* tm = std::localtime(&t); - if (!tm) return ""; + std::tm tmv; + if (!util::localTime(static_cast(ts), tmv)) return ""; char buf[32]; - std::strftime(buf, sizeof(buf), "%b %d, %Y", tm); + std::strftime(buf, sizeof(buf), "%b %d, %Y", &tmv); return buf; } @@ -185,11 +182,10 @@ std::string relativeTime(std::int64_t ts) { if (d < 3600) return std::to_string(d / 60) + "m"; if (d < 86400) return std::to_string(d / 3600) + "h"; if (d < 7 * 86400) return std::to_string(d / 86400) + "d"; - std::time_t t = static_cast(ts); - std::tm* tm = std::localtime(&t); - if (!tm) return ""; + std::tm tmv; + if (!util::localTime(static_cast(ts), tmv)) return ""; char buf[16]; - std::strftime(buf, sizeof(buf), "%b %d", tm); + std::strftime(buf, sizeof(buf), "%b %d", &tmv); return buf; } diff --git a/src/util/local_time.h b/src/util/local_time.h new file mode 100644 index 0000000..8abbfe4 --- /dev/null +++ b/src/util/local_time.h @@ -0,0 +1,22 @@ +// DragonX Wallet - reentrant local-time helper (no per-call stat of /etc/localtime on glibc). +#pragma once +#include +namespace dragonx::util { +// Fills `out` with the local broken-down time of `t`. Returns false on failure. Thread-safe. +inline bool localTime(std::time_t t, std::tm& out) { +#ifdef _WIN32 + return localtime_s(&out, &t) == 0; +#else + return localtime_r(&t, &out) != nullptr; +#endif +} +// Re-read the TZ database / TZ env (glibc localtime_r does not do this on its own after the first call). +// Call once per cache rebuild, never per item. +inline void refreshTimezone() { +#ifdef _WIN32 + _tzset(); +#else + tzset(); +#endif +} +} // namespace dragonx::util diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 51b40ce..ce5f509 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -64,6 +64,7 @@ #include "data/seed_migration_resume.h" #include "util/address_validation.h" #include "util/seed_phrase.h" +#include "util/local_time.h" #include "util/image_decode_core.h" #include "daemon/daemon_startup_diagnosis.h" #include "util/amount_format.h" @@ -14408,6 +14409,26 @@ void testConsolidationRunSimulation() EXPECT_TRUE(runVerdict(sum) == RunVerdict::Clean); } +namespace { +void testLocalTime() +{ +#ifndef _WIN32 + const char* prev = std::getenv("TZ"); + const bool hadTz = prev != nullptr; + const std::string savedTz = hadTz ? prev : ""; + setenv("TZ", "XXX-3", 1); dragonx::util::refreshTimezone(); // POSIX TZ: UTC+3, no tzdata needed + std::tm t{}; + EXPECT_TRUE(dragonx::util::localTime(0, t)); + EXPECT_EQ(t.tm_year, 70); EXPECT_EQ(t.tm_mday, 1); EXPECT_EQ(t.tm_hour, 3); + setenv("TZ", "YYY5", 1); dragonx::util::refreshTimezone(); // UTC-5: refreshTimezone() must pick it up + EXPECT_TRUE(dragonx::util::localTime(0, t)); + EXPECT_EQ(t.tm_mday, 31); EXPECT_EQ(t.tm_hour, 19); + if (hadTz) setenv("TZ", savedTz.c_str(), 1); else unsetenv("TZ"); + dragonx::util::refreshTimezone(); +#endif +} +} // namespace + int main() { testConnectionConfig(); @@ -14644,6 +14665,7 @@ int main() testImageDecodeCore(); testImageDecodeJobs(); testGeneratedResourceBehavior(); + testLocalTime(); if (g_failures != 0) { std::cerr << g_failures << " test assertion(s) failed\n";