Skip to content
Open
40 changes: 40 additions & 0 deletions core/libs/prx/libc/include/SonyCrt.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <ctime>
#include <mutex>
#include <stdexcept>
#include <string>

#include "SceTypes.hpp"
#include "prx/libc/include/GuestTime.hpp"
Expand Down Expand Up @@ -41,6 +42,45 @@ inline SonyConstraintHandler& SonyConstraintHandlerSlot() {

namespace {

struct SonyHostTimeZone {
std::int64_t standardEast;
std::int64_t daylight;
};

inline SonyHostTimeZone SonyHostZoneAt_nid_no_patch(std::int64_t utcTime) {
std::tm local{};
#ifdef _WIN32
constexpr std::int64_t cycleSeconds = 146097LL * 86400LL;
constexpr std::int64_t lastHostSecond = 32535215999LL;
const std::int64_t cycles = utcTime >= 0 && utcTime <= lastHostSecond ? 0 : (utcTime >= 0 ? utcTime / cycleSeconds : -((-(utcTime + 1)) / cycleSeconds + 1));
const std::time_t t = static_cast<std::time_t>(utcTime - cycles * cycleSeconds);
_tzset();
if (localtime_s(&local, &t) != 0) throw std::runtime_error(std::string(__func__) + ": no host local time for " + std::to_string(utcTime));
long standardBias = 0;
long daylightBias = 0;
_get_timezone(&standardBias);
_get_dstbias(&daylightBias);
return {-static_cast<std::int64_t>(standardBias), local.tm_isdst > 0 ? -static_cast<std::int64_t>(daylightBias) : 0};
#else
const std::time_t t = static_cast<std::time_t>(utcTime);
::tzset();
if (localtime_r(&t, &local) == nullptr) throw std::runtime_error(std::string(__func__) + ": no host local time for " + std::to_string(utcTime));
const std::int64_t daylight = local.tm_isdst > 0 ? static_cast<std::int64_t>(local.tm_gmtoff) + ::timezone : 0;
return {static_cast<std::int64_t>(local.tm_gmtoff) - daylight, daylight};
#endif
}

inline std::int64_t SonyHostDaylightAmount_nid_no_patch(std::int64_t utcTime) {
constexpr std::int64_t monthSeconds = 2629746;
for (int month = 0; month <= 11; ++month) {
for (const int direction : {1, -1}) {
const SonyHostTimeZone zone = SonyHostZoneAt_nid_no_patch(utcTime + direction * month * monthSeconds);
if (zone.daylight != 0) return zone.daylight;
}
}
return 0;
}

template<typename TInteger>
inline TInteger ParseInteger(const char* text, char** end, int base, TInteger (*parse)(const char*, char**, int)) {
#ifdef __APPLE__
Expand Down
46 changes: 30 additions & 16 deletions core/libs/prx/libc/src/Time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,25 @@ constexpr const char* MONTH_NAMES[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
constexpr std::size_t ASCTIME_SIZE = sizeof("Sun Jan-2147483648 -2147483648:-2147483648:-2147483648 -2147481748\n");
constexpr std::int64_t SECONDS_PER_DAY = 86400;
constexpr std::int64_t DAYS_FROM_MARCH_TO_YEAR_END = 306;
#if defined(_WIN32) || defined(__APPLE__)
constexpr std::int64_t CALENDAR_CYCLE_YEARS = 400;

std::int64_t calendarCycles(std::int64_t value, std::int64_t cycle) {
return value >= 0 ? value / cycle : -((-(value + 1)) / cycle + 1);
}

std::int64_t utcSeconds(const std::tm& time) {
const std::int64_t years = calendarCycles(time.tm_mon, 12);
const std::int64_t month = time.tm_mon - years * 12 + 1;
const std::int64_t year = std::int64_t{time.tm_year} + 1900 + years - (month <= 2 ? 1 : 0);
const std::int64_t era = calendarCycles(year, CALENDAR_CYCLE_YEARS);
const std::int64_t yearOfEra = year - era * CALENDAR_CYCLE_YEARS;
const std::int64_t dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5;
const std::int64_t dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear;
const std::int64_t days = era * 146097 + dayOfEra - 719468 + time.tm_mday - 1;
return days * SECONDS_PER_DAY + std::int64_t{time.tm_hour} * 3600 + std::int64_t{time.tm_min} * 60 + time.tm_sec;
}

#if defined(_WIN32) || defined(__APPLE__)
constexpr std::int64_t CALENDAR_CYCLE_SECONDS = 146097 * SECONDS_PER_DAY;
#ifdef _WIN32
constexpr int HOST_ORDINARY_FIRST_YEAR = 70;
Expand All @@ -45,10 +62,6 @@ constexpr std::int64_t HOST_FIRST_SECOND = -2208988800;
constexpr std::int64_t HOST_LAST_SECOND = std::numeric_limits<std::int64_t>::max();
#endif

std::int64_t calendarCycles(std::int64_t value, std::int64_t cycle) {
return value >= 0 ? value / cycle : -((-(value + 1)) / cycle + 1);
}

#ifdef __APPLE__
bool timeOfLocalSeconds(std::int64_t local, std::int64_t& time) {
if (local < std::numeric_limits<std::int64_t>::min() + SECONDS_PER_DAY) return false;
Expand All @@ -65,17 +78,6 @@ bool timeOfLocalSeconds(std::int64_t local, std::int64_t& time) {
}
#endif

std::int64_t utcSeconds(const std::tm& time) {
const std::int64_t years = calendarCycles(time.tm_mon, 12);
const std::int64_t month = time.tm_mon - years * 12 + 1;
const std::int64_t year = std::int64_t{time.tm_year} + 1900 + years - (month <= 2 ? 1 : 0);
const std::int64_t era = calendarCycles(year, CALENDAR_CYCLE_YEARS);
const std::int64_t yearOfEra = year - era * CALENDAR_CYCLE_YEARS;
const std::int64_t dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5;
const std::int64_t dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear;
const std::int64_t days = era * 146097 + dayOfEra - 719468 + time.tm_mday - 1;
return days * SECONDS_PER_DAY + std::int64_t{time.tm_hour} * 3600 + std::int64_t{time.tm_min} * 60 + time.tm_sec;
}
#endif

void copyCalendarFields(const std::tm& host, GuestTm& guest) {
Expand Down Expand Up @@ -551,6 +553,18 @@ GuestTm* APS5_VABI gmtime_nid_postfix(const int64_t* timer) {
int64_t APS5_VABI libc_mktime_nid_postfix(GuestTm* timeptr) {
const ZoneScope zone;
std::tm host = toHostTm(*timeptr);
if (host.tm_isdst > 0) {
const auto local = utcSeconds(host);
GuestTm normalized{};
if (utcCalendarFields(local, normalized)) {
const int saved = *__error_nid_postfix();
const auto guess = SonyHostZoneAt_nid_no_patch(local);
const auto zone = SonyHostZoneAt_nid_no_patch(local - guess.standardEast - guess.daylight);
const auto daylight = SonyHostDaylightAmount_nid_no_patch(local - zone.standardEast - zone.daylight);
*__error_nid_postfix() = saved;
if (daylight == 0) host.tm_isdst = -1;
}
}
host.tm_wday = -1;
int64_t moved = 0;
#if defined(_WIN32) || defined(__APPLE__)
Expand Down
36 changes: 4 additions & 32 deletions core/libs/prx/libkernel/Time/Time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "prx/libkernel/Pthread/include/Cancel.hpp"

#include "prx/libc/include/General.hpp"
#include "prx/libc/include/SonyCrt.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libkernel/KernelErrors.hpp"
#include "prx/libkernel/Pthread/include/Pthread.hpp"
Expand Down Expand Up @@ -762,43 +763,14 @@ int SetTimeOfDay_nid_no_patch(const KernelTimeval* tv, const KernelTimezone* tz)

static constexpr int SceKernelErrorEfault = static_cast<int>(0x8002000e);

struct HostZone {
std::int64_t standardEast;
std::int64_t daylight;
};
using HostZone = SonyHostTimeZone;

static HostZone HostZoneAt(std::int64_t utcTime) {
std::tm local{};
#ifdef _WIN32
constexpr std::int64_t cycleSeconds = 146097LL * 86400LL;
constexpr std::int64_t lastHostSecond = 32535215999LL;
const std::int64_t cycles = utcTime >= 0 && utcTime <= lastHostSecond ? 0 : (utcTime >= 0 ? utcTime / cycleSeconds : -((-(utcTime + 1)) / cycleSeconds + 1));
const std::time_t t = static_cast<std::time_t>(utcTime - cycles * cycleSeconds);
_tzset();
if (localtime_s(&local, &t) != 0) throw std::runtime_error(std::string(__func__) + ": no host local time for " + std::to_string(utcTime));
long standardBias = 0;
long daylightBias = 0;
_get_timezone(&standardBias);
_get_dstbias(&daylightBias);
return {-static_cast<std::int64_t>(standardBias), local.tm_isdst > 0 ? -static_cast<std::int64_t>(daylightBias) : 0};
#else
const std::time_t t = static_cast<std::time_t>(utcTime);
::tzset();
if (localtime_r(&t, &local) == nullptr) throw std::runtime_error(std::string(__func__) + ": no host local time for " + std::to_string(utcTime));
const std::int64_t daylight = local.tm_isdst > 0 ? static_cast<std::int64_t>(local.tm_gmtoff) + ::timezone : 0;
return {static_cast<std::int64_t>(local.tm_gmtoff) - daylight, daylight};
#endif
return SonyHostZoneAt_nid_no_patch(utcTime);
}

static std::int64_t HostDaylightAmount(std::int64_t utcTime) {
constexpr std::int64_t monthSeconds = 2629746;
for (int month = 0; month <= 11; ++month) {
for (const int direction : {1, -1}) {
const HostZone zone = HostZoneAt(utcTime + direction * month * monthSeconds);
if (zone.daylight != 0) return zone.daylight;
}
}
return 0;
return SonyHostDaylightAmount_nid_no_patch(utcTime);
}

static KernelTimesec ZoneState(std::int64_t time, const HostZone& zone) {
Expand Down
65 changes: 63 additions & 2 deletions core/libs/tests/GuestMktime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,67 @@ static void CheckNormalization(Converter convert, std::int64_t timer) {
Require(Equal(input, expected), "mktime did not normalize a valid timestamp");
}

static void CheckFailure(Converter convert) {
static void CheckUnavailableDaylightHint(Converter convert, int year) {
for (const int month : {0, 6}) {
GuestTm canonical{};
canonical.tm_year = year;
canonical.tm_mon = month;
canonical.tm_mday = 15;
canonical.tm_hour = 12;
const auto expected = convert(&canonical);
Require(expected != -1, "standard calendar conversion failed");
for (const int hint : {1, 2, std::numeric_limits<int>::max()}) {
GuestTm input = canonical;
input.tm_isdst = hint;
*__error_nid_postfix() = 7;
Require(convert(&input) == expected, "unavailable daylight hint changed the timestamp");
Require(Equal(input, canonical), "unavailable daylight hint changed normalized fields");
Require(*__error_nid_postfix() == 7, "daylight lookup changed errno");
}
}
}

static void CheckAvailableDaylightHints() {
Require(setenv_nid_postfix("TZ", "EST5EDT", 1) == 0, "Could not set the time zone");
tzset_nid_postfix();
constexpr std::array<Converter, 2> converters{libc_mktime_nid_postfix, mktime_nid_postfix};
const struct { int month; int hint; std::int64_t expected; int hour; int daylight; } cases[]{
{0, 1, 1705334400, 11, 0},
{6, 0, 1721062800, 13, 1},
{6, 1, 1721059200, 12, 1}
};
for (auto convert : converters) {
for (const auto& item : cases) {
GuestTm input{};
input.tm_year = 124;
input.tm_mon = item.month;
input.tm_mday = 15;
input.tm_hour = 12;
input.tm_isdst = item.hint;
Require(convert(&input) == item.expected, "available daylight hint was ignored");
Require(input.tm_hour == item.hour && input.tm_isdst == item.daylight, "available daylight fields differ");
}
}
}

#ifndef _WIN32
static void CheckHistoricalDaylightAbsence(const char* zone, int year) {
Require(setenv_nid_postfix("TZ", zone, 1) == 0, "Could not set the time zone");
tzset_nid_postfix();
CheckUnavailableDaylightHint(libc_mktime_nid_postfix, year);
CheckUnavailableDaylightHint(mktime_nid_postfix, year);
}

#endif

static void CheckFailure(Converter convert, int hint = -1) {
GuestTm input{};
input.tm_year = std::numeric_limits<int>::max();
input.tm_mon = std::numeric_limits<int>::max();
input.tm_mday = 1;
input.tm_wday = 4;
input.tm_yday = 12;
input.tm_isdst = -1;
input.tm_isdst = hint;
input.tm_gmtoff = 123;
input.tm_zone = "unchanged";
const GuestTm original = input;
Expand All @@ -69,10 +122,18 @@ static void CheckZone(const char* zone) {
CheckNormalization(convert, timer);
}
CheckFailure(convert);
CheckFailure(convert, 1);
for (const int year : {40, 70, 124}) CheckUnavailableDaylightHint(convert, year);
}
}

int main() {
CheckZone("IST-5:30");
CheckAvailableDaylightHints();
#ifndef _WIN32
CheckHistoricalDaylightAbsence("Australia/Lord_Howe", 40);
CheckHistoricalDaylightAbsence("Europe/Moscow", 124);
#endif
CheckZone("UTC0");
CheckZone("UTC-2");
CheckZone("UTC5");
Expand Down
1 change: 1 addition & 0 deletions docs/dev/debt/libc.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@
- [Guest file names on Windows](../../../core/libs/prx/libc/src/General.cpp) (libc) are not escaped or mapped before the host call, so names Windows treats specially differ from FreeBSD, where only `/` and NUL are special. `a*b`, `a?b`, `a<b>c`, `a|b` and `a"b` fail with `EINVAL` (22) where FreeBSD creates them. `trail.` and `trail ` both create the host file `trail`, so three FreeBSD names are one file here. `NUL` and a reopen of it succeed, but no file is created. `con`, `aux.txt` and `COM1` create ordinary files. Colons are under Colons in guest names. Measured on Windows 11 (10.0.26200), MinGW-w64 GCC 15.2.0 r7, with `open` (`O_CREAT|O_EXCL|O_WRONLY`), a one-byte write and a listing of the host directory; not measured on Linux or a console. A fix needs a guest-to-host name mapping.
- [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount
- [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A payload process on a PS5 (firmware 13.60) read both names empty with `TZ` unset, and its `%Z` printed nothing whatever `tm_zone` held; the names a title process sees are not known
- [mktime daylight hints](../../../core/libs/prx/libc/src/Time.cpp) (libc) still differs from the title-bundled libraries for some gaps, folds and negative daylight transitions. The availability query shares libkernel host time-zone rules; console transition policy and queries at the integer-year boundaries remain unverified. A host lookup that cannot represent its candidate times propagates its failure.
- [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts.
- [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow a payload process on a PS5 (firmware 13.60) in the C locale. Out-of-range fields in `%G`, `%g`, `%V`, `%U` and `%W` follow the SDK 7.00 `libc.prx` that PPSA06416 ships, read from its disassembly and run, and are measured on a console only for the cases in the test; it counts a year divisible by 100 as leap only when `tm_year / 400` is 100 or -300. `%k`, `%l`, `%P`, `%s`, `%+`, `%v`, flags and widths are copied as written, `E` or `O` before an unknown conversion is dropped, and a format ending in either throws, where the console crashed. Negative fields print 0 and fields wrap at 100 (`%j` at 1000, `%u` and `%w` at 10); `%Y` and `%G` are the year mod 10000, `%C` is `tm_year / 100 + 19`. A negative weekday or month gives `Sun` or `Jan`. `%z` and `%Z` ignore `tm_gmtoff` and `tm_zone` and use the guest zone and `tzname`
- [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) converts the format to UTF-8, runs this library's `strftime` and converts the result back, where the console's `libSceLibcInternal.sprx` (0x854e0) copies literal wide characters and expands each conversion straight into the buffer; the results match for the formats tried, and it has the `strftime` gaps above. A result that does not fit returns 0 with the buffer untouched, where the console leaves the characters written so far. A null buffer returns 0, where the console writes through it, and a size too large for the narrow buffer returns 0 with `EINVAL`, where the console formats as usual. A null format or `tm` throws
Expand Down
Loading