SDL: SDL file times are 64-bit integers representing nanoseconds since the Unix epoch

From 747300b3562b259645731eaa6a78782c1ae7e6fd Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 17 Mar 2024 13:11:13 -0700
Subject: [PATCH] SDL file times are 64-bit integers representing nanoseconds
 since the Unix epoch

---
 include/SDL3/SDL_filesystem.h         | 41 ++++++++++++++++++---------
 include/SDL3/SDL_timer.h              | 10 ++++---
 src/dynapi/SDL_dynapi.sym             |  1 +
 src/dynapi/SDL_dynapi_overrides.h     |  1 +
 src/dynapi/SDL_dynapi_procs.h         |  1 +
 src/filesystem/SDL_filesystem.c       | 33 ++++++++++++++-------
 src/filesystem/posix/SDL_sysfsops.c   |  9 ++----
 src/filesystem/windows/SDL_sysfsops.c | 18 ++----------
 8 files changed, 65 insertions(+), 49 deletions(-)

diff --git a/include/SDL3/SDL_filesystem.h b/include/SDL3/SDL_filesystem.h
index c98c1b1a04b43..5ae90d3d8009e 100644
--- a/include/SDL3/SDL_filesystem.h
+++ b/include/SDL3/SDL_filesystem.h
@@ -244,16 +244,19 @@ typedef enum SDL_PathType
     SDL_PATHTYPE_OTHER /**< something completely different like a device node (not a symlink, those are always followed) */
 } SDL_PathType;
 
-/* SDL file timestamps are 64-bit integers representing seconds since the Unix epoch (Jan 1, 1970) */
-typedef Sint64 SDL_FileTimestamp;
+/* SDL file times are 64-bit integers representing nanoseconds since the Unix epoch (Jan 1, 1970)
+ *
+ * They can be converted between to POSIX time_t values with SDL_NS_TO_SECONDS() and SDL_SECONDS_TO_NS(), and between Windows FILETIME values with SDL_FileTimeToWindows() and SDL_FileTimeFromWindows()
+ */
+typedef Sint64 SDL_FileTime;
 
 typedef struct SDL_PathInfo
 {
-    SDL_PathType type;              /* the path type */
-    Uint64 size;                    /* the file size in bytes */
-    SDL_FileTimestamp create_time;  /* the time when the path was created */
-    SDL_FileTimestamp modify_time;  /* the last time the path was modified */
-    SDL_FileTimestamp access_time;  /* the last time the path was read */
+    SDL_PathType type;          /* the path type */
+    Uint64 size;                /* the file size in bytes */
+    SDL_FileTime create_time;   /* the time when the path was created */
+    SDL_FileTime modify_time;   /* the last time the path was modified */
+    SDL_FileTime access_time;   /* the last time the path was read */
 } SDL_PathInfo;
 
 /**
@@ -318,17 +321,29 @@ extern DECLSPEC int SDLCALL SDL_RenamePath(const char *oldpath, const char *newp
  */
 extern DECLSPEC int SDLCALL SDL_GetPathInfo(const char *path, SDL_PathInfo *info);
 
-/* some helper functions ... */
-
-/* Converts an SDL file timestamp into a Windows FILETIME (100-nanosecond intervals since January 1, 1601). Fills in the two 32-bit values of the FILETIME structure.
+/* Converts an SDL file time into a Windows FILETIME (100-nanosecond intervals since January 1, 1601).
+ *
+ * This function fills in the two 32-bit values of the FILETIME structure.
  *
  * \param ftime the time to convert
- * \param low a pointer filled in with the low portion of the Windows FILETIME value
- * \param high a pointer filled in with the high portion of the Windows FILETIME value
+ * \param dwLowDateTime a pointer filled in with the low portion of the Windows FILETIME value
+ * \param dwHighDateTime a pointer filled in with the high portion of the Windows FILETIME value
+ *
+ * \since This function is available since SDL 3.0.0.
+ */
+extern DECLSPEC void SDLCALL SDL_FileTimeToWindows(SDL_FileTime ftime, Uint32 *dwLowDateTime, Uint32 *dwHighDateTime);
+
+/* Converts a Windows FILETIME (100-nanosecond intervals since January 1, 1601) to an SDL file time
+ *
+ * This function takes the two 32-bit values of the FILETIME structure as parameters.
+ *
+ * \param dwLowDateTime the low portion of the Windows FILETIME value
+ * \param dwHighDateTime the high portion of the Windows FILETIME value
+ * \returns the converted file time
  *
  * \since This function is available since SDL 3.0.0.
  */
-extern DECLSPEC void SDLCALL SDL_FileTimeToWindows(Sint64 ftime, Uint32 *low, Uint32 *high);
+extern DECLSPEC SDL_FileTime SDLCALL SDL_FileTimeFromWindows(Uint32 dwLowDateTime, Uint32 dwHighDateTime);
 
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus
diff --git a/include/SDL3/SDL_timer.h b/include/SDL3/SDL_timer.h
index e824ad7f7e099..d317bd81f0abc 100644
--- a/include/SDL3/SDL_timer.h
+++ b/include/SDL3/SDL_timer.h
@@ -45,10 +45,12 @@ extern "C" {
 #define SDL_NS_PER_SECOND   1000000000LL
 #define SDL_NS_PER_MS       1000000
 #define SDL_NS_PER_US       1000
-#define SDL_MS_TO_NS(MS)    (((Uint64)(MS)) * SDL_NS_PER_MS)
-#define SDL_NS_TO_MS(NS)    ((NS) / SDL_NS_PER_MS)
-#define SDL_US_TO_NS(US)    (((Uint64)(US)) * SDL_NS_PER_US)
-#define SDL_NS_TO_US(NS)    ((NS) / SDL_NS_PER_US)
+#define SDL_SECONDS_TO_NS(S)    (((Uint64)(S)) * SDL_NS_PER_SECOND)
+#define SDL_NS_TO_SECONDS(NS)   ((NS) / SDL_NS_PER_SECOND)
+#define SDL_MS_TO_NS(MS)        (((Uint64)(MS)) * SDL_NS_PER_MS)
+#define SDL_NS_TO_MS(NS)        ((NS) / SDL_NS_PER_MS)
+#define SDL_US_TO_NS(US)        (((Uint64)(US)) * SDL_NS_PER_US)
+#define SDL_NS_TO_US(NS)        ((NS) / SDL_NS_PER_US)
 
 /**
  * Get the number of milliseconds since SDL library initialization.
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index c95e658c34170..eb41c61db6063 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -999,6 +999,7 @@ SDL3_0.0.0 {
     SDL_RemoveStoragePath;
     SDL_RenameStoragePath;
     SDL_GetStoragePathInfo;
+    SDL_FileTimeFromWindows;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index f31ea6a323928..7d69981bfd6a7 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1024,3 +1024,4 @@
 #define SDL_RemoveStoragePath SDL_RemoveStoragePath_REAL
 #define SDL_RenameStoragePath SDL_RenameStoragePath_REAL
 #define SDL_GetStoragePathInfo SDL_GetStoragePathInfo_REAL
+#define SDL_FileTimeFromWindows SDL_FileTimeFromWindows_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 9202049c43970..4266fea3339d5 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1049,3 +1049,4 @@ SDL_DYNAPI_PROC(int,SDL_EnumerateStorageDirectory,(SDL_Storage *a, const char *b
 SDL_DYNAPI_PROC(int,SDL_RemoveStoragePath,(SDL_Storage *a, const char *b),(a,b),return)
 SDL_DYNAPI_PROC(int,SDL_RenameStoragePath,(SDL_Storage *a, const char *b, const char *c),(a,b,c),return)
 SDL_DYNAPI_PROC(int,SDL_GetStoragePathInfo,(SDL_Storage *a, const char *b, SDL_PathInfo *c),(a,b,c),return)
+SDL_DYNAPI_PROC(SDL_FileTime,SDL_FileTimeFromWindows,(Uint32 a, Uint32 b),(a,b),return)
diff --git a/src/filesystem/SDL_filesystem.c b/src/filesystem/SDL_filesystem.c
index 014d658846150..aa5c4ddd354e4 100644
--- a/src/filesystem/SDL_filesystem.c
+++ b/src/filesystem/SDL_filesystem.c
@@ -22,27 +22,38 @@
 #include "SDL_internal.h"
 #include "SDL_sysfilesystem.h"
 
-void SDL_FileTimeToWindows(Sint64 ftime, Uint32 *low, Uint32 *high)
+static const Sint64 delta_1601_epoch_100ns = 11644473600ll * 10000000ll; // [100 ns] (100 ns units between 1/1/1601 and 1/1/1970, 11644473600 seconds)
+
+void SDL_FileTimeToWindows(SDL_FileTime ftime, Uint32 *dwLowDateTime, Uint32 *dwHighDateTime)
 {
-    const Sint64 delta_1601_epoch_s = 11644473600ull; // [seconds] (seconds between 1/1/1601 and 1/1/1970, 11644473600 seconds)
+    Uint64 wtime;
 
-    Sint64 cvt = (ftime + delta_1601_epoch_s) * (SDL_NS_PER_SECOND / 100ull); // [100ns] (adjust to epoch and convert nanoseconds to 1/100th nanosecond units).
+    // Convert ftime to 100ns units
+    Sint64 ftime_100ns = (ftime / 100);
 
-    // Windows FILETIME is unsigned, so if we're trying to show a timestamp from before before the
-    // Windows epoch, (Jan 1, 1601), clamp it to zero so it doesn't go way into the future.
-    if (cvt < 0) {
-        cvt = 0;
+    if (ftime_100ns < 0 && -ftime_100ns > delta_1601_epoch_100ns) {
+        // If we're trying to show a timestamp from before before the Windows epoch, (Jan 1, 1601), clamp it to zero
+        wtime = 0;
+    } else {
+        wtime = (Uint64)(delta_1601_epoch_100ns + ftime_100ns);
     }
 
-    if (low) {
-        *low = (Uint32) cvt;
+    if (dwLowDateTime) {
+        *dwLowDateTime = (Uint32)wtime;
     }
 
-    if (high) {
-        *high = (Uint32) (cvt >> 32);
+    if (dwHighDateTime) {
+        *dwHighDateTime = (Uint32)(wtime >> 32);
     }
 }
 
+SDL_FileTime SDL_FileTimeFromWindows(Uint32 dwLowDateTime, Uint32 dwHighDateTime)
+{
+    Uint64 wtime = (((Uint64)dwHighDateTime << 32) | dwLowDateTime);
+
+    return (Sint64)(wtime - delta_1601_epoch_100ns) * 100;
+}
+
 int SDL_RemovePath(const char *path)
 {
     if (!path) {
diff --git a/src/filesystem/posix/SDL_sysfsops.c b/src/filesystem/posix/SDL_sysfsops.c
index f793888c3b458..20ffce90568c0 100644
--- a/src/filesystem/posix/SDL_sysfsops.c
+++ b/src/filesystem/posix/SDL_sysfsops.c
@@ -124,12 +124,9 @@ int SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
         info->size = (Uint64) statbuf.st_size;
     }
 
-    // SDL file time is seconds since the Unix epoch, so we're already good here.
-    // Note that this will fail on machines with 32-bit time_t in 2038, but that's not
-    // an SDL bug; those machines need to be fixed or everything will fail in the same way.
-    info->create_time = (Sint64) statbuf.st_ctime;
-    info->modify_time = (Sint64) statbuf.st_mtime;
-    info->access_time = (Sint64) statbuf.st_atime;
+    info->create_time = (SDL_FileTime)SDL_SECONDS_TO_NS(statbuf.st_ctime);
+    info->modify_time = (SDL_FileTime)SDL_SECONDS_TO_NS(statbuf.st_mtime);
+    info->access_time = (SDL_FileTime)SDL_SECONDS_TO_NS(statbuf.st_atime);
 
     return 0;
 }
diff --git a/src/filesystem/windows/SDL_sysfsops.c b/src/filesystem/windows/SDL_sysfsops.c
index cea3ac295bf85..177d4ba805e3d 100644
--- a/src/filesystem/windows/SDL_sysfsops.c
+++ b/src/filesystem/windows/SDL_sysfsops.c
@@ -140,18 +140,6 @@ int SDL_SYS_CreateDirectory(const char *path)
     return !rc ? WIN_SetError("Couldn't create directory") : 0;
 }
 
-static Sint64 FileTimeToSDLTime(const FILETIME *ft)
-{
-    const Uint64 delta_1601_epoch_100ns = 11644473600ull * 10000000ull; // [100ns] (100-ns chunks between 1/1/1601 and 1/1/1970, 11644473600 seconds * 10000000)
-    ULARGE_INTEGER large;
-    large.LowPart = ft->dwLowDateTime;
-    large.HighPart = ft->dwHighDateTime;
-    if (large.QuadPart == 0) {
-        return 0;  // unsupported on this filesystem...0 is fine, I guess.
-    }
-    return (Sint64) ((((Uint64)large.QuadPart) - delta_1601_epoch_100ns) / (SDL_NS_PER_SECOND / 100ull));  // [secs] (adjust to epoch and convert 1/100th nanosecond units to seconds).
-}
-
 int SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
 {
     WCHAR *wpath = WIN_UTF8ToString(path);
@@ -177,9 +165,9 @@ int SDL_SYS_GetPathInfo(const char *path, SDL_PathInfo *info)
         info->size = ((((Uint64) winstat.nFileSizeHigh) << 32) | winstat.nFileSizeLow);
     }
 
-    info->create_time = FileTimeToSDLTime(&winstat.ftCreationTime);
-    info->modify_time = FileTimeToSDLTime(&winstat.ftLastWriteTime);
-    info->access_time = FileTimeToSDLTime(&winstat.ftLastAccessTime);
+    info->create_time = SDL_FileTimeFromWindows(winstat.ftCreationTime.dwLowDateTime, winstat.ftCreationTime.dwHighDateTime);
+    info->modify_time = SDL_FileTimeFromWindows(winstat.ftLastWriteTime.dwLowDateTime, winstat.ftLastWriteTime.dwHighDateTime);
+    info->access_time = SDL_FileTimeFromWindows(winstat.ftLastAccessTime.dwLowDateTime, winstat.ftLastAccessTime.dwHighDateTime);
 
     return 1;
 }