SDL: Added SDL_OpenFileStorage() for local file storage

From ec3ba387d1fff6c935925ae7c79dabcce01fbb5b Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 16 Mar 2024 09:08:00 -0700
Subject: [PATCH] Added SDL_OpenFileStorage() for local file storage

---
 include/SDL3/SDL_storage.h               | 24 +++++++++++-
 src/dynapi/SDL_dynapi.sym                |  1 +
 src/dynapi/SDL_dynapi_overrides.h        |  1 +
 src/dynapi/SDL_dynapi_procs.h            |  1 +
 src/storage/SDL_storage.c                |  5 +++
 src/storage/SDL_sysstorage.h             |  2 +
 src/storage/generic/SDL_genericstorage.c | 49 ++++++++++++++++++++++--
 7 files changed, 78 insertions(+), 5 deletions(-)

diff --git a/include/SDL3/SDL_storage.h b/include/SDL3/SDL_storage.h
index b57832cae006c..1892b8cf29bab 100644
--- a/include/SDL3/SDL_storage.h
+++ b/include/SDL3/SDL_storage.h
@@ -85,8 +85,8 @@ extern DECLSPEC SDL_Storage *SDLCALL SDL_OpenTitleStorage(const char *override,
  *
  * While title storage can generally be kept open throughout runtime, user
  * storage should only be opened when the client is ready to read/write files.
- * This allows the backend to properly batch R/W operations and flush them
- * when the container has been closed; ensuring safe and optimal save I/O.
+ * This allows the backend to properly batch file operations and flush them when
+ * the container has been closed; ensuring safe and optimal save I/O.
  *
  * \param org the name of your organization
  * \param app the name of your application
@@ -107,6 +107,26 @@ extern DECLSPEC SDL_Storage *SDLCALL SDL_OpenTitleStorage(const char *override,
  */
 extern DECLSPEC SDL_Storage *SDLCALL SDL_OpenUserStorage(const char *org, const char *app, SDL_PropertiesID props);
 
+/**
+ * Opens up a container for local filesystem storage.
+ *
+ * This is provided for development and tools. Portable applications should use SDL_OpenTitleStorage() for access to game data and SDL_OpenUserStorage() for access to user data.
+ *
+ * \param path the base path prepended to all storage paths, or NULL for no base path
+ * \returns a filesystem storage container on success or NULL on failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_OpenStorage
+ * \sa SDL_CloseStorage
+ * \sa SDL_StorageReady
+ * \sa SDL_GetStorageFileSize
+ * \sa SDL_ReadStorageFile
+ * \sa SDL_WriteStorageFile
+ * \sa SDL_GetStorageSpaceRemaining
+ */
+extern DECLSPEC SDL_Storage *SDLCALL SDL_OpenFileStorage(const char *path);
+
 /**
  * Opens up a container using a client-provided storage interface.
  *
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index e233484a0a453..eec9befc08f8a 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -993,6 +993,7 @@ SDL3_0.0.0 {
     SDL_RenamePath;
     SDL_GetPathInfo;
     SDL_FileTimeToWindows;
+    SDL_OpenFileStorage;
     # 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 f12c82ea581fc..8c0fd4b7ee8d7 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1018,3 +1018,4 @@
 #define SDL_RenamePath SDL_RenamePath_REAL
 #define SDL_GetPathInfo SDL_GetPathInfo_REAL
 #define SDL_FileTimeToWindows SDL_FileTimeToWindows_REAL
+#define SDL_OpenFileStorage SDL_OpenFileStorage_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index fa2c50937a91b..c9880ef5f29fb 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1043,3 +1043,4 @@ SDL_DYNAPI_PROC(int,SDL_RemovePath,(const char *a),(a),return)
 SDL_DYNAPI_PROC(int,SDL_RenamePath,(const char *a, const char *b),(a,b),return)
 SDL_DYNAPI_PROC(int,SDL_GetPathInfo,(const char *a, SDL_PathInfo *b),(a,b),return)
 SDL_DYNAPI_PROC(void,SDL_FileTimeToWindows,(Sint64 a, Uint32 *b, Uint32 *c),(a,b,c),)
+SDL_DYNAPI_PROC(SDL_Storage*,SDL_OpenFileStorage,(const char *a),(a),return)
diff --git a/src/storage/SDL_storage.c b/src/storage/SDL_storage.c
index 2f0f0d7c849ff..4d496caead519 100644
--- a/src/storage/SDL_storage.c
+++ b/src/storage/SDL_storage.c
@@ -137,6 +137,11 @@ SDL_Storage *SDL_OpenUserStorage(const char *org, const char *app, SDL_Propertie
     return storage;
 }
 
+SDL_Storage *SDL_OpenFileStorage(const char *path)
+{
+    return GENERIC_OpenFileStorage(path);
+}
+
 SDL_Storage *SDL_OpenStorage(const SDL_StorageInterface *iface, void *userdata)
 {
     SDL_Storage *storage;
diff --git a/src/storage/SDL_sysstorage.h b/src/storage/SDL_sysstorage.h
index c16c5cc25175c..363e2f0b6d89e 100644
--- a/src/storage/SDL_sysstorage.h
+++ b/src/storage/SDL_sysstorage.h
@@ -46,4 +46,6 @@ extern TitleStorageBootStrap GENERIC_titlebootstrap;
 extern UserStorageBootStrap GENERIC_userbootstrap;
 extern UserStorageBootStrap STEAM_userbootstrap;
 
+extern SDL_Storage *GENERIC_OpenFileStorage(const char *path);
+
 #endif /* SDL_sysstorage_h_ */
diff --git a/src/storage/generic/SDL_genericstorage.c b/src/storage/generic/SDL_genericstorage.c
index 26296be9e4028..e1088b160f97a 100644
--- a/src/storage/generic/SDL_genericstorage.c
+++ b/src/storage/generic/SDL_genericstorage.c
@@ -25,10 +25,16 @@
 
 static char *GENERIC_INTERNAL_CreateFullPath(const char *base, const char *relative)
 {
-    size_t fulllen = SDL_strlen(base) + SDL_strlen(relative) + 1;
-    char *result = (char*) SDL_malloc(fulllen);
+    size_t len = 0;
+
+    if (base) {
+        len += SDL_strlen(base);
+    }
+    len += SDL_strlen(relative) + 1;
+
+    char *result = (char*) SDL_malloc(len);
     if (result != NULL) {
-        SDL_snprintf(result, fulllen, "%s%s", base, relative);
+        SDL_snprintf(result, len, "%s%s", base, relative);
     }
     return result;
 }
@@ -186,3 +192,40 @@ UserStorageBootStrap GENERIC_userbootstrap = {
     "SDL generic user storage driver",
     GENERIC_User_Create
 };
+
+static const SDL_StorageInterface GENERIC_file_iface = {
+    GENERIC_StorageClose,
+    GENERIC_StorageReady,
+    GENERIC_StorageFileSize,
+    GENERIC_StorageReadFile,
+    GENERIC_StorageWriteFile,
+    GENERIC_StorageSpaceRemaining
+};
+
+SDL_Storage *GENERIC_OpenFileStorage(const char *path)
+{
+    SDL_Storage *result;
+    size_t len = 0;
+    char *basepath = NULL;
+
+    if (path) {
+        len += SDL_strlen(path);
+    }
+    if (len > 0) {
+        if (path[len-1] == '/') {
+            basepath = SDL_strdup(path);
+            if (!basepath) {
+                return NULL;
+            }
+        } else {
+            if (SDL_asprintf(&basepath, "%s/", path) < 0) {
+                return NULL;
+            }
+        }
+    }
+    result = SDL_OpenStorage(&GENERIC_file_iface, basepath);
+    if (result == NULL) {
+        SDL_free(basepath);
+    }
+    return result;
+}