SDL: Added SDL_IOFromDynamicMem()

From df0f5deddfd6ebb94a6a0ed828c809d5f5dcdb61 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 17 Mar 2024 17:11:20 -0700
Subject: [PATCH] Added SDL_IOFromDynamicMem()

---
 include/SDL3/SDL_iostream.h       |  31 +++++++--
 src/dynapi/SDL_dynapi.sym         |   1 +
 src/dynapi/SDL_dynapi_overrides.h |   1 +
 src/dynapi/SDL_dynapi_procs.h     |   1 +
 src/file/SDL_iostream.c           | 111 ++++++++++++++++++++++++++++--
 test/testautomation_iostream.c    |  63 +++++++++++++++--
 6 files changed, 192 insertions(+), 16 deletions(-)

diff --git a/include/SDL3/SDL_iostream.h b/include/SDL3/SDL_iostream.h
index 675d5f79c1e99..0e64e88936fc7 100644
--- a/include/SDL3/SDL_iostream.h
+++ b/include/SDL3/SDL_iostream.h
@@ -189,8 +189,6 @@ typedef struct SDL_IOStream SDL_IOStream;
  *
  * \since This function is available since SDL 3.0.0.
  *
- * \sa SDL_IOFromConstMem
- * \sa SDL_IOFromMem
  * \sa SDL_CloseIO
  * \sa SDL_ReadIO
  * \sa SDL_SeekIO
@@ -201,7 +199,7 @@ extern DECLSPEC SDL_IOStream *SDLCALL SDL_IOFromFile(const char *file, const cha
 
 #define SDL_PROP_IOSTREAM_WINDOWS_HANDLE_POINTER "SDL.iostream.windows.handle"
 #define SDL_PROP_IOSTREAM_STDIO_HANDLE_POINTER "SDL.iostream.stdio.handle"
-#define SDL_PROP_IOSTREAM_ANDROID_AASSET_POINTER "SDL.opstream.android.aasset"
+#define SDL_PROP_IOSTREAM_ANDROID_AASSET_POINTER "SDL.iostream.android.aasset"
 
 /**
  * Use this function to prepare a read-write memory buffer for use with
@@ -226,8 +224,6 @@ extern DECLSPEC SDL_IOStream *SDLCALL SDL_IOFromFile(const char *file, const cha
  * \since This function is available since SDL 3.0.0.
  *
  * \sa SDL_IOFromConstMem
- * \sa SDL_IOFromFile
- * \sa SDL_IOFromMem
  * \sa SDL_CloseIO
  * \sa SDL_ReadIO
  * \sa SDL_SeekIO
@@ -260,8 +256,6 @@ extern DECLSPEC SDL_IOStream *SDLCALL SDL_IOFromMem(void *mem, size_t size);
  *
  * \since This function is available since SDL 3.0.0.
  *
- * \sa SDL_IOFromConstMem
- * \sa SDL_IOFromFile
  * \sa SDL_IOFromMem
  * \sa SDL_CloseIO
  * \sa SDL_ReadIO
@@ -270,6 +264,29 @@ extern DECLSPEC SDL_IOStream *SDLCALL SDL_IOFromMem(void *mem, size_t size);
  */
 extern DECLSPEC SDL_IOStream *SDLCALL SDL_IOFromConstMem(const void *mem, size_t size);
 
+/**
+ * Use this function to create an SDL_IOStream that is backed by dynamically allocated memory.
+ *
+ * This supports the following properties to provide access to the memory and control over allocations:
+ * - `SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER`: a pointer to the internal memory of the stream. This can be set to NULL to transfer ownership of the memory to the application, which should free the memory with SDL_free(). If this is done, the next operation on the stream must be SDL_CloseIO().
+ * - `SDL_PROP_IOSTREAM_DYNAMIC_CHUNKSIZE_NUMBER`: memory will be allocated in multiples of this size, defaulting to 1024.
+ *
+ * \returns a pointer to a new SDL_IOStream structure, or NULL if it fails;
+ *          call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_CloseIO
+ * \sa SDL_ReadIO
+ * \sa SDL_SeekIO
+ * \sa SDL_TellIO
+ * \sa SDL_WriteIO
+ */
+extern DECLSPEC SDL_IOStream *SDLCALL SDL_IOFromDynamicMem(void);
+
+#define SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER    "SDL.iostream.dynamic.memory"
+#define SDL_PROP_IOSTREAM_DYNAMIC_CHUNKSIZE_NUMBER  "SDL.iostream.dynamic.chunksize"
+
 /* @} *//* IOFrom functions */
 
 
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index eb41c61db6063..9da8e0c8c661e 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1000,6 +1000,7 @@ SDL3_0.0.0 {
     SDL_RenameStoragePath;
     SDL_GetStoragePathInfo;
     SDL_FileTimeFromWindows;
+    SDL_IOFromDynamicMem;
     # 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 7d69981bfd6a7..c0dbb212fa0a0 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1025,3 +1025,4 @@
 #define SDL_RenameStoragePath SDL_RenameStoragePath_REAL
 #define SDL_GetStoragePathInfo SDL_GetStoragePathInfo_REAL
 #define SDL_FileTimeFromWindows SDL_FileTimeFromWindows_REAL
+#define SDL_IOFromDynamicMem SDL_IOFromDynamicMem_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 4266fea3339d5..ca2434ad053ce 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1050,3 +1050,4 @@ SDL_DYNAPI_PROC(int,SDL_RemoveStoragePath,(SDL_Storage *a, const char *b),(a,b),
 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)
+SDL_DYNAPI_PROC(SDL_IOStream*,SDL_IOFromDynamicMem,(void),(),return)
diff --git a/src/file/SDL_iostream.c b/src/file/SDL_iostream.c
index 050157efab5bb..5f726ca780f39 100644
--- a/src/file/SDL_iostream.c
+++ b/src/file/SDL_iostream.c
@@ -495,17 +495,18 @@ static size_t mem_io(void *userdata, void *dst, const void *src, size_t size)
 
 static size_t SDLCALL mem_read(void *userdata, void *ptr, size_t size, SDL_IOStatus *status)
 {
-    const IOStreamMemData *iodata = (IOStreamMemData *) userdata;
+    IOStreamMemData *iodata = (IOStreamMemData *) userdata;
     return mem_io(userdata, ptr, iodata->here, size);
 }
 
 static size_t SDLCALL mem_write(void *userdata, const void *ptr, size_t size, SDL_IOStatus *status)
 {
-    const IOStreamMemData *iodata = (IOStreamMemData *) userdata;
+    IOStreamMemData *iodata = (IOStreamMemData *) userdata;
     return mem_io(userdata, iodata->here, ptr, size);
 }
 
-static int SDLCALL mem_close(void *userdata) {
+static int SDLCALL mem_close(void *userdata)
+{
     SDL_free(userdata);
     return 0;
 }
@@ -731,6 +732,109 @@ SDL_IOStream *SDL_IOFromConstMem(const void *mem, size_t size)
     return iostr;
 }
 
+typedef struct IOStreamDynamicMemData
+{
+    SDL_IOStream *stream;
+    IOStreamMemData data;
+    Uint8 *end;
+} IOStreamDynamicMemData;
+
+static Sint64 SDLCALL dynamic_mem_size(void *userdata)
+{
+    IOStreamDynamicMemData *iodata = (IOStreamDynamicMemData *) userdata;
+    return mem_size(&iodata->data);
+}
+
+static Sint64 SDLCALL dynamic_mem_seek(void *userdata, Sint64 offset, int whence)
+{
+    IOStreamDynamicMemData *iodata = (IOStreamDynamicMemData *) userdata;
+    return mem_seek(&iodata->data, offset, whence);
+}
+
+static size_t SDLCALL dynamic_mem_read(void *userdata, void *ptr, size_t size, SDL_IOStatus *status)
+{
+    IOStreamDynamicMemData *iodata = (IOStreamDynamicMemData *) userdata;
+    return mem_io(&iodata->data, ptr, iodata->data.here, size);
+}
+
+static int dynamic_mem_realloc(IOStreamDynamicMemData *iodata, size_t size)
+{
+    size_t chunksize = (size_t)SDL_GetNumberProperty(SDL_GetIOProperties(iodata->stream), SDL_PROP_IOSTREAM_DYNAMIC_CHUNKSIZE_NUMBER, 0);
+    if (!chunksize) {
+        chunksize = 1024;
+    }
+
+    // We're intentionally allocating more memory than needed so it can be null terminated
+    size_t chunks = (((iodata->end - iodata->data.base) + size) / chunksize) + 1;
+    size_t length = (chunks * chunksize);
+    Uint8 *base = (Uint8 *)SDL_realloc(iodata->data.base, length);
+    if (!base) {
+        return -1;
+    }
+
+    size_t here_offset = (iodata->data.here - iodata->data.base);
+    size_t stop_offset = (iodata->data.stop - iodata->data.base);
+    iodata->data.base = base;
+    iodata->data.here = base + here_offset;
+    iodata->data.stop = base + stop_offset;
+    iodata->end = base + length;
+    return SDL_SetProperty(SDL_GetIOProperties(iodata->stream), SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER, base);
+}
+
+static size_t SDLCALL dynamic_mem_write(void *userdata, const void *ptr, size_t size, SDL_IOStatus *status)
+{
+    IOStreamDynamicMemData *iodata = (IOStreamDynamicMemData *) userdata;
+    if (size > (iodata->data.stop - iodata->data.here)) {
+        if (size > (iodata->end - iodata->data.here)) {
+            if (dynamic_mem_realloc(iodata, size) < 0) {
+                return 0;
+            }
+        }
+        iodata->data.stop = iodata->data.here + size;
+    }
+    return mem_io(&iodata->data, iodata->data.here, ptr, size);
+}
+
+static int SDLCALL dynamic_mem_close(void *userdata)
+{
+    const IOStreamDynamicMemData *iodata = (IOStreamDynamicMemData *) userdata;
+    void *mem = SDL_GetProperty(SDL_GetIOProperties(iodata->stream), SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER, NULL);
+    if (mem) {
+        SDL_free(mem);
+    }
+    SDL_free(userdata);
+    return 0;
+}
+
+SDL_IOStream *SDL_IOFromDynamicMem(void)
+{
+    IOStreamDynamicMemData *iodata = (IOStreamDynamicMemData *) SDL_malloc(sizeof (*iodata));
+    if (!iodata) {
+        return NULL;
+    }
+
+    SDL_IOStreamInterface iface;
+    SDL_zero(iface);
+    iface.size = dynamic_mem_size;
+    iface.seek = dynamic_mem_seek;
+    iface.read = dynamic_mem_read;
+    iface.write = dynamic_mem_write;
+    iface.close = dynamic_mem_close;
+
+    iodata->data.base = NULL;
+    iodata->data.here = NULL;
+    iodata->data.stop = NULL;
+    iodata->end = NULL;
+
+    SDL_IOStream *iostr = SDL_OpenIO(&iface, iodata);
+    if (iostr) {
+        iodata->stream = iostr;
+    } else {
+        SDL_free(iodata);
+    }
+    return iostr;
+}
+
 SDL_IOStatus SDL_GetIOStatus(SDL_IOStream *context)
 {
     if (!context) {
@@ -740,7 +844,6 @@ SDL_IOStatus SDL_GetIOStatus(SDL_IOStream *context)
     return context->status;
 }
 
-
 SDL_IOStream *SDL_OpenIO(const SDL_IOStreamInterface *iface, void *userdata)
 {
     if (!iface) {
diff --git a/test/testautomation_iostream.c b/test/testautomation_iostream.c
index 8ff135c31ba98..0fe3f8bf55163 100644
--- a/test/testautomation_iostream.c
+++ b/test/testautomation_iostream.c
@@ -312,6 +312,55 @@ static int iostrm_testConstMem(void *arg)
     return TEST_COMPLETED;
 }
 
+/**
+ * Tests dynamic memory
+ *
+ * \sa SDL_IOFromDynamicMem
+ * \sa SDL_CloseIO
+ */
+static int iostrm_testDynamicMem(void *arg)
+{
+    SDL_IOStream *rw;
+    SDL_PropertiesID props;
+    char *mem;
+    int result;
+
+    /* Open */
+    rw = SDL_IOFromDynamicMem();
+    SDLTest_AssertPass("Call to SDL_IOFromDynamicMem() succeeded");
+    SDLTest_AssertCheck(rw != NULL, "Verify opening memory with SDL_IOFromDynamicMem does not return NULL");
+
+    /* Bail out if NULL */
+    if (rw == NULL) {
+        return TEST_ABORTED;
+    }
+
+    /* Set the chunk size to 1 byte */
+    props = SDL_GetIOProperties(rw);
+    SDL_SetNumberProperty(props, SDL_PROP_IOSTREAM_DYNAMIC_CHUNKSIZE_NUMBER, 1);
+
+    /* Run generic tests */
+    testGenericIOStreamValidations(rw, SDL_TRUE);
+
+    /* Get the dynamic memory and verify it */
+    mem = (char *)SDL_GetProperty(props, SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER, NULL);
+    SDLTest_AssertPass("Call to SDL_GetProperty(props, SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER, NULL) succeeded");
+    SDLTest_AssertCheck(mem != NULL, "Verify memory value is not NULL");
+    mem[SDL_SizeIO(rw)] = '\0';
+    SDLTest_AssertCheck(SDL_strcmp(mem, IOStreamHelloWorldTestString) == 0, "Verify memory value is correct");
+
+    /* Take the memory and free it ourselves */
+    SDL_SetProperty(props, SDL_PROP_IOSTREAM_DYNAMIC_MEMORY_POINTER, NULL);
+    SDL_free(mem);
+
+    /* Close */
+    result = SDL_CloseIO(rw);
+    SDLTest_AssertPass("Call to SDL_CloseIO() succeeded");
+    SDLTest_AssertCheck(result == 0, "Verify result value is 0; got: %d", result);
+
+    return TEST_COMPLETED;
+}
+
 /**
  * Tests reading from file.
  *
@@ -614,29 +663,33 @@ static const SDLTest_TestCaseReference iostrmTest3 = {
 };
 
 static const SDLTest_TestCaseReference iostrmTest4 = {
-    (SDLTest_TestCaseFp)iostrm_testFileRead, "iostrm_testFileRead", "Tests reading from a file", TEST_ENABLED
+    (SDLTest_TestCaseFp)iostrm_testDynamicMem, "iostrm_testDynamicMem", "Tests opening dynamic memory", TEST_ENABLED
 };
 
 static const SDLTest_TestCaseReference iostrmTest5 = {
-    (SDLTest_TestCaseFp)iostrm_testFileWrite, "iostrm_testFileWrite", "Test writing to a file", TEST_ENABLED
+    (SDLTest_TestCaseFp)iostrm_testFileRead, "iostrm_testFileRead", "Tests reading from a file", TEST_ENABLED
 };
 
 static const SDLTest_TestCaseReference iostrmTest6 = {
-    (SDLTest_TestCaseFp)iostrm_testAllocFree, "iostrm_testAllocFree", "Test alloc and free of RW context", TEST_ENABLED
+    (SDLTest_TestCaseFp)iostrm_testFileWrite, "iostrm_testFileWrite", "Test writing to a file", TEST_ENABLED
 };
 
 static const SDLTest_TestCaseReference iostrmTest7 = {
-    (SDLTest_TestCaseFp)iostrm_testFileWriteReadEndian, "iostrm_testFileWriteReadEndian", "Test writing and reading via the Endian aware functions", TEST_ENABLED
+    (SDLTest_TestCaseFp)iostrm_testAllocFree, "iostrm_testAllocFree", "Test alloc and free of RW context", TEST_ENABLED
 };
 
 static const SDLTest_TestCaseReference iostrmTest8 = {
+    (SDLTest_TestCaseFp)iostrm_testFileWriteReadEndian, "iostrm_testFileWriteReadEndian", "Test writing and reading via the Endian aware functions", TEST_ENABLED
+};
+
+static const SDLTest_TestCaseReference iostrmTest9 = {
     (SDLTest_TestCaseFp)iostrm_testCompareRWFromMemWithRWFromFile, "iostrm_testCompareRWFromMemWithRWFromFile", "Compare RWFromMem and RWFromFile IOStream for read and seek", TEST_ENABLED
 };
 
 /* Sequence of IOStream test cases */
 static const SDLTest_TestCaseReference *iostrmTests[] = {
     &iostrmTest1, &iostrmTest2, &iostrmTest3, &iostrmTest4, &iostrmTest5, &iostrmTest6,
-    &iostrmTest7, &iostrmTest8, NULL
+    &iostrmTest7, &iostrmTest8, &iostrmTest9, NULL
 };
 
 /* IOStream test suite (global) */