#include "SceTypes.hpp" #include #include #include #include #include #include #ifdef _WIN32 #include #include #endif extern "C" { int APS5_VABI utimes_nid_postfix(const char*, const KernelTimeval*); int APS5_VABI lutimes_nid_postfix(const char*, const KernelTimeval*); int APS5_VABI futimes_nid_postfix(int, const KernelTimeval*); int APS5_VABI sceKernelUtimes_nid_postfix(const char*, const KernelTimeval*); int APS5_VABI stat_nid_postfix(const char*, FileStat*); int APS5_VABI lstat_nid_postfix(const char*, FileStat*); int APS5_VABI sceKernelFstat(int, FileStat*); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value, const char* message) { if (!value) { std::fprintf(stderr, "%s (guest errno %d)\n", message, *__error_nid_postfix()); std::exit(1); } } static bool Same(const KernelTimespec& actual, const KernelTimeval& expected) { return actual.tv_sec == expected.tv_sec && actual.tv_nsec == expected.tv_usec * 1000; } static void CheckTimes(const std::string& path, int fd, const KernelTimeval* expected) { FileStat byPath{}, byFd{}, byKernel{}; Require(stat_nid_postfix(path.c_str(), &byPath) == 0, "stat"); Require(fstat_nid_disambig1_nid_postfix(fd, &byFd) == 0, "fstat"); Require(sceKernelFstat(fd, &byKernel) == 0, "sceKernelFstat"); for (const auto* status : {&byPath, &byFd, &byKernel}) { Require(Same(status->st_atim, expected[0]), "access time preserves microseconds"); Require(Same(status->st_mtim, expected[1]), "modification time preserves microseconds"); } #ifdef _WIN32 FILETIME access{}, modified{}; Require(GetFileTime(reinterpret_cast(_get_osfhandle(fd)), nullptr, &access, &modified), "native GetFileTime"); const auto ticks = [](FILETIME time) { return (static_cast(time.dwHighDateTime) << 32) | time.dwLowDateTime; }; Require(ticks(access) == static_cast(expected[0].tv_sec + 11644473600LL) * 10000000 + expected[0].tv_usec * 10, "native access time preserves microseconds"); Require(ticks(modified) == static_cast(expected[1].tv_sec + 11644473600LL) * 10000000 + expected[1].tv_usec * 10, "native modification time preserves microseconds"); #endif } int main() { const auto root = std::filesystem::path("anyps5-file-times-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root), "create fixture directory"); const auto path = (root / "file.txt").string(); { std::ofstream file(path); file << "timestamp fixture"; } const int fd = open_nid_postfix(path.c_str(), 0, 0); Require(fd >= 0, "open read-only descriptor"); const KernelTimeval first[2]{{1000000000, 123456}, {1000000001, 500000}}; Require(utimes_nid_postfix(path.c_str(), first) == 0, "utimes fractional timestamp"); CheckTimes(path, fd, first); const KernelTimeval second[2]{{1000000002, 1}, {1000000003, 999999}}; Require(futimes_nid_postfix(fd, second) == 0, "futimes on read-only descriptor"); CheckTimes(path, fd, second); Require(sceKernelUtimes_nid_postfix(path.c_str(), first) == 0, "sceKernelUtimes fractional timestamp"); CheckTimes(path, fd, first); Require(lutimes_nid_postfix(path.c_str(), second) == 0, "lutimes fractional timestamp"); CheckTimes(path, fd, second); for (int index = 0; index < 2; ++index) { for (const auto micros : {-1LL, 1000000LL}) { KernelTimeval invalid[2]{first[0], first[1]}; invalid[index].tv_usec = micros; Require(utimes_nid_postfix(path.c_str(), invalid) == -1 && *__error_nid_postfix() == 22, "utimes rejects invalid microseconds"); Require(futimes_nid_postfix(fd, invalid) == -1 && *__error_nid_postfix() == 22, "futimes rejects invalid microseconds"); Require(sceKernelUtimes_nid_postfix(path.c_str(), invalid) == static_cast(0x80020016u), "sceKernelUtimes rejects invalid microseconds"); Require(lutimes_nid_postfix(path.c_str(), invalid) == -1 && *__error_nid_postfix() == 22, "lutimes rejects invalid microseconds"); CheckTimes(path, fd, second); } } #ifdef _WIN32 const auto handle = CreateFileW(std::filesystem::path(path).c_str(), FILE_WRITE_ATTRIBUTES | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr); Require(handle != INVALID_HANDLE_VALUE, "open native timestamp handle"); constexpr std::uint64_t ticks = 116444736000000000ULL + 1000000000ULL * 10000000 + 1234567; const FILETIME precise{static_cast(ticks), static_cast(ticks >> 32)}; Require(SetFileTime(handle, &precise, &precise, &precise), "set native 100-nanosecond timestamps"); FILE_BASIC_INFO native{}; Require(GetFileInformationByHandleEx(handle, FileBasicInfo, &native, sizeof(native)), "read native basic info"); FileStat status{}; Require(stat_nid_postfix(path.c_str(), &status) == 0, "stat native timestamps"); Require(status.st_birthtim.tv_sec == 1000000000 && status.st_birthtim.tv_nsec == 123456700, "creation time retains native precision"); Require(status.st_atim.tv_sec == 1000000000 && status.st_atim.tv_nsec == 123456700, "access time retains native precision"); Require(status.st_mtim.tv_sec == 1000000000 && status.st_mtim.tv_nsec == 123456700, "modification time retains native precision"); Require(status.st_ctim.tv_sec == native.ChangeTime.QuadPart / 10000000 - 11644473600LL && status.st_ctim.tv_nsec == native.ChangeTime.QuadPart % 10000000 * 100, "change time uses native metadata change time"); Require(CloseHandle(handle), "close native timestamp handle"); const KernelTimeval preEpoch[2]{{-1, 500000}, {0, 1}}; Require(utimes_nid_postfix(path.c_str(), preEpoch) == 0, "timestamps around Unix epoch"); CheckTimes(path, fd, preEpoch); Require(utimes_nid_postfix(path.c_str(), first) == 0, "restore valid times"); for (const auto seconds : {std::numeric_limits::min(), std::numeric_limits::max(), -11644473601LL, -11644473600LL}) { const KernelTimeval invalid[2]{first[0], {seconds, 0}}; Require(utimes_nid_postfix(path.c_str(), invalid) == -1 && *__error_nid_postfix() == 22, "reject unrepresentable Windows timestamp"); Require(futimes_nid_postfix(fd, invalid) == -1 && *__error_nid_postfix() == 22, "futimes rejects unrepresentable Windows timestamp"); CheckTimes(path, fd, first); } #endif const auto before = std::chrono::system_clock::now(); Require(futimes_nid_postfix(fd, nullptr) == 0, "futimes current time"); const auto after = std::chrono::system_clock::now(); FileStat now{}; Require(stat_nid_postfix(path.c_str(), &now) == 0, "stat current time"); const auto recorded = std::chrono::seconds(now.st_mtim.tv_sec) + std::chrono::nanoseconds(now.st_mtim.tv_nsec); Require(recorded >= before.time_since_epoch() - std::chrono::seconds(1) && recorded <= after.time_since_epoch() + std::chrono::seconds(1), "null times use current clock"); Require(utimes_nid_postfix(path.c_str(), nullptr) == 0, "utimes current time"); Require(close_nid_postfix(fd) == 0, "close fixture descriptor"); const auto directory = (root / "directory").string(); Require(std::filesystem::create_directory(directory), "create timestamp directory"); const int directoryFd = open_nid_postfix(directory.c_str(), 0, 0); Require(directoryFd >= 0, "open directory descriptor"); Require(utimes_nid_postfix(directory.c_str(), first) == 0, "directory utimes"); FileStat directoryStatus{}; Require(stat_nid_postfix(directory.c_str(), &directoryStatus) == 0 && Same(directoryStatus.st_mtim, first[1]), "directory stat precision"); Require(futimes_nid_postfix(directoryFd, second) == 0, "directory futimes"); Require(sceKernelFstat(directoryFd, &directoryStatus) == 0 && Same(directoryStatus.st_mtim, second[1]), "directory fstat precision"); Require(close_nid_postfix(directoryFd) == 0, "close directory descriptor"); Require(utimes_nid_postfix((root / "missing").string().c_str(), first) == -1 && *__error_nid_postfix() == 2, "missing path reports ENOENT"); Require(lutimes_nid_postfix(nullptr, first) == -1 && *__error_nid_postfix() == 14, "lutimes rejects null path"); Require(lutimes_nid_postfix("", first) == -1 && *__error_nid_postfix() == 2, "lutimes rejects empty path"); Require(lutimes_nid_postfix((root / "missing").string().c_str(), first) == -1 && *__error_nid_postfix() == 2, "lutimes missing path reports ENOENT"); Require(utimes_nid_postfix(path.c_str(), second) == 0, "utimes target to second"); std::error_code symlinkError; const auto linkPath = (root / "link.txt").string(); std::filesystem::create_symlink(path, linkPath, symlinkError); if (!symlinkError) { Require(lutimes_nid_postfix(linkPath.c_str(), first) == 0, "lutimes modifies symlink"); FileStat targetStatus{}; Require(stat_nid_postfix(path.c_str(), &targetStatus) == 0 && Same(targetStatus.st_mtim, second[1]), "target file unchanged"); #ifdef _WIN32 const HANDLE handle = CreateFileW(std::filesystem::path(linkPath).c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); Require(handle != INVALID_HANDLE_VALUE, "open symlink"); FILETIME modified{}; Require(GetFileTime(handle, nullptr, nullptr, &modified), "get symlink time"); CloseHandle(handle); const auto ticks = [](FILETIME time) { return (static_cast(time.dwHighDateTime) << 32) | time.dwLowDateTime; }; Require(ticks(modified) == static_cast(first[1].tv_sec + 11644473600LL) * 10000000 + first[1].tv_usec * 10, "symlink mtime updated"); #else FileStat linkStatus{}; Require(lstat_nid_postfix(linkPath.c_str(), &linkStatus) == 0 && Same(linkStatus.st_mtim, first[1]), "symlink mtime updated"); #endif } int pipe[2]; Require(pipe_nid_postfix(pipe) == 0, "create pipe"); FileStat pipeStatus{}; Require(sceKernelFstat(pipe[0], &pipeStatus) == 0, "pipe stat still works"); #ifdef _WIN32 Require(futimes_nid_postfix(pipe[0], first) == -1 && *__error_nid_postfix() == 22, "pipe timestamp update fails"); #endif Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0, "close pipe"); std::filesystem::remove_all(root); }