Files
AnyPS5/core/libs/tests/GuestTimeConversion.cpp
T
Mohd FaourandClaude Fable 5.1 0aae158635 fix(libc): convert times into the guest's 56-byte tm on Windows
gmtime, localtime, mktime, strftime and asctime took and returned the
host std::tm. The guest's FreeBSD tm has tm_gmtoff and tm_zone after the
nine calendar fields, so on Windows, where std::tm is 36 bytes, a title
reading those fields read past the thread-local result, and gmtime_s left
them unwritten. Windows gmtime_s also accepts only 1969-12-31 12:00 to
3001-01-19 21:59 UTC, where the guest libc converts any time.

Convert through a GuestTm with the guest layout on both hosts. Compute
the UTC fields from the day count, so every time whose year fits an int
converts, and on Windows take the local offset and zone name from the
CRT. gmtime reports the zone as UTC like the guest libc. localtime and
mktime keep the CRT's range on Windows; recorded in TechnicalDebt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 02:13:17 +02:00

141 lines
6.2 KiB
C++

#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/GuestTime.hpp"
#include <array>
#include <atomic>
#include <barrier>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <thread>
extern "C" {
GuestTm* APS5_VABI libc_gmtime_nid_postfix(const std::int64_t*);
GuestTm* APS5_VABI gmtime_nid_postfix(const std::int64_t*);
GuestTm* APS5_VABI libc_localtime_nid_postfix(const std::int64_t*);
GuestTm* APS5_VABI localtime_nid_postfix(const std::int64_t*);
GuestTm* APS5_VABI gmtime_s_nid_postfix(const std::int64_t*, GuestTm*);
GuestTm* APS5_VABI localtime_s_nid_postfix(const std::int64_t*, GuestTm*);
std::int64_t APS5_VABI mktime_nid_postfix(GuestTm*);
std::size_t APS5_VABI strftime_nid_postfix(char*, std::size_t, const char*, const GuestTm*);
}
using Converter = GuestTm* (APS5_VABI *)(const std::int64_t*);
using BufferedConverter = GuestTm* (APS5_VABI *)(const std::int64_t*, GuestTm*);
static void Require(bool value, const char* message) {
if (!value) {
std::fprintf(stderr, "%s\n", message);
std::abort();
}
}
static bool Equal(const GuestTm& left, const GuestTm& right) {
return left.tm_sec == right.tm_sec && left.tm_min == right.tm_min && left.tm_hour == right.tm_hour
&& left.tm_mday == right.tm_mday && left.tm_mon == right.tm_mon && left.tm_year == right.tm_year
&& left.tm_wday == right.tm_wday && left.tm_yday == right.tm_yday && left.tm_isdst == right.tm_isdst;
}
static void CheckConcurrent(Converter convert, BufferedConverter buffered, const char* name) {
constexpr std::array<std::int64_t, 8> timers{0, 86400, 946684800, 1078012800, 1609459200, 1709164800, 1893456000, 2145916799};
std::array<GuestTm, timers.size()> expected{};
for (std::size_t index = 0; index < timers.size(); ++index)
Require(buffered(&timers[index], &expected[index]) == &expected[index], "Buffered conversion failed");
std::barrier start(static_cast<std::ptrdiff_t>(timers.size()));
std::atomic<bool> matches{true};
std::array<std::thread, timers.size()> workers;
for (std::size_t index = 0; index < workers.size(); ++index) {
workers[index] = std::thread([&, index] {
start.arrive_and_wait();
for (int iteration = 0; iteration < 50000; ++iteration) {
const auto* result = convert(&timers[index]);
if (result == nullptr || !Equal(*result, expected[index])) {
matches = false;
break;
}
}
});
}
for (auto& worker : workers) worker.join();
Require(matches, name);
const std::int64_t invalid = std::numeric_limits<std::int64_t>::max();
Require(convert(&invalid) == nullptr, "Unrepresentable time was accepted");
}
struct UtcCase {
std::int64_t timer;
int year;
int mon;
int mday;
int hour;
int min;
int sec;
int wday;
int yday;
};
static void CheckUtc(const UtcCase& expected) {
GuestTm utc{};
std::memset(&utc, 0xAA, sizeof(utc));
Require(gmtime_s_nid_postfix(&expected.timer, &utc) == &utc, "UTC conversion failed");
Require(utc.tm_year == expected.year && utc.tm_mon == expected.mon && utc.tm_mday == expected.mday
&& utc.tm_hour == expected.hour && utc.tm_min == expected.min && utc.tm_sec == expected.sec
&& utc.tm_wday == expected.wday && utc.tm_yday == expected.yday && utc.tm_isdst == 0,
"UTC calendar fields are wrong");
Require(utc.tm_gmtoff == 0, "UTC conversion did not reset tm_gmtoff");
Require(utc.tm_zone != nullptr && std::strcmp(utc.tm_zone, "UTC") == 0, "UTC conversion did not set tm_zone");
}
int main() {
#ifdef _WIN32
_putenv_s("TZ", "UTC-2");
_tzset();
#else
setenv("TZ", "UTC-2", 1);
tzset();
#endif
constexpr std::array<UtcCase, 7> utcCases{{
{0, 70, 0, 1, 0, 0, 0, 4, 0},
{-1, 69, 11, 31, 23, 59, 59, 3, 364},
{-31536000, 69, 0, 1, 0, 0, 0, 3, 0},
{951782400, 100, 1, 29, 0, 0, 0, 2, 59},
{32535216000, 1101, 0, 1, 0, 0, 0, 4, 0},
{253402300799, 8099, 11, 31, 23, 59, 59, 5, 364},
{-62135596800, -1899, 0, 1, 0, 0, 0, 1, 0},
}};
for (const auto& utcCase : utcCases) CheckUtc(utcCase);
const std::int64_t underflow = std::numeric_limits<std::int64_t>::min();
GuestTm scratch{};
Require(gmtime_s_nid_postfix(&underflow, &scratch) == nullptr, "Unrepresentable negative time was accepted");
const std::int64_t epoch = 0;
GuestTm local{};
std::memset(&local, 0xAA, sizeof(local));
Require(localtime_s_nid_postfix(&epoch, &local) == &local && local.tm_year == 70 && local.tm_mon == 0
&& local.tm_mday == 1 && local.tm_hour == 2, "Local epoch conversion failed");
Require(local.tm_gmtoff == 7200, "Local conversion did not set tm_gmtoff");
Require(local.tm_zone != nullptr && std::strlen(local.tm_zone) > 0, "Local conversion did not set tm_zone");
GuestTm roundTrip = local;
roundTrip.tm_wday = -1;
roundTrip.tm_yday = -1;
roundTrip.tm_gmtoff = -1;
roundTrip.tm_zone = nullptr;
Require(mktime_nid_postfix(&roundTrip) == 0, "mktime did not invert localtime");
Require(roundTrip.tm_wday == 4 && roundTrip.tm_yday == 0, "mktime did not normalize the calendar fields");
Require(roundTrip.tm_gmtoff == 7200 && roundTrip.tm_zone != nullptr, "mktime did not set the zone fields");
GuestTm utc{};
Require(gmtime_s_nid_postfix(&epoch, &utc) == &utc, "UTC epoch conversion failed");
char formatted[64]{};
Require(strftime_nid_postfix(formatted, sizeof(formatted), "%Y-%m-%d %H:%M:%S", &utc) == 19
&& std::strcmp(formatted, "1970-01-01 00:00:00") == 0, "strftime did not read the guest tm");
CheckConcurrent(libc_gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent libc_gmtime returned another thread's date");
CheckConcurrent(gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent gmtime returned another thread's date");
CheckConcurrent(libc_localtime_nid_postfix, localtime_s_nid_postfix, "Concurrent libc_localtime returned another thread's date");
CheckConcurrent(localtime_nid_postfix, localtime_s_nid_postfix, "Concurrent localtime returned another thread's date");
}