SDL: Added SDL_CopyProperties()

From 5d48f9a63a0053d3fdaa168a3b4d527cf74fab90 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 2 Feb 2024 14:19:02 -0800
Subject: [PATCH] Added SDL_CopyProperties()

---
 include/SDL3/SDL_properties.h     | 16 +++++++
 src/SDL_properties.c              | 72 +++++++++++++++++++++++++++++
 src/dynapi/SDL_dynapi.sym         |  1 +
 src/dynapi/SDL_dynapi_overrides.h |  1 +
 src/dynapi/SDL_dynapi_procs.h     |  1 +
 test/testautomation_properties.c  | 77 +++++++++++++++++++++++++++++--
 6 files changed, 164 insertions(+), 4 deletions(-)

diff --git a/include/SDL3/SDL_properties.h b/include/SDL3/SDL_properties.h
index c5d3daca6439..d434ba2e39a3 100644
--- a/include/SDL3/SDL_properties.h
+++ b/include/SDL3/SDL_properties.h
@@ -81,6 +81,22 @@ extern DECLSPEC SDL_PropertiesID SDLCALL SDL_GetGlobalProperties(void);
  */
 extern DECLSPEC SDL_PropertiesID SDLCALL SDL_CreateProperties(void);
 
+/**
+ * Copy a set of properties
+ *
+ * Copy all the properties from one set of properties to another, with the exception of properties requiring cleanup (set using SDL_SetPropertyWithCleanup()), which will not be copied. Any property that already exists on `dst` will be overwritten.
+ *
+ * \param src the properties to copy
+ * \param dst the destination properties
+ * \returns 0 on success or a negative error code on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.0.0.
+ */
+extern DECLSPEC int SDLCALL SDL_CopyProperties(SDL_PropertiesID src, SDL_PropertiesID dst);
+
 /**
  * Lock a set of properties
  *
diff --git a/src/SDL_properties.c b/src/SDL_properties.c
index d59cfc632d6c..50cb2cd682d4 100644
--- a/src/SDL_properties.c
+++ b/src/SDL_properties.c
@@ -188,6 +188,78 @@ SDL_PropertiesID SDL_CreateProperties(void)
     return 0;
 }
 
+int SDL_CopyProperties(SDL_PropertiesID src, SDL_PropertiesID dst)
+{
+    SDL_Properties *src_properties = NULL;
+    SDL_Properties *dst_properties = NULL;
+    int result = 0;
+
+    if (!src) {
+        return SDL_InvalidParamError("src");
+    }
+    if (!dst) {
+        return SDL_InvalidParamError("dst");
+    }
+
+    SDL_LockMutex(SDL_properties_lock);
+    SDL_FindInHashTable(SDL_properties, (const void *)(uintptr_t)src, (const void **)&src_properties);
+    SDL_FindInHashTable(SDL_properties, (const void *)(uintptr_t)dst, (const void **)&dst_properties);
+    SDL_UnlockMutex(SDL_properties_lock);
+
+    if (!src_properties) {
+        return SDL_InvalidParamError("src");
+    }
+    if (!dst_properties) {
+        return SDL_InvalidParamError("dst");
+    }
+
+    SDL_LockMutex(src_properties->lock);
+    SDL_LockMutex(dst_properties->lock);
+    {
+        void *iter;
+        const void *key, *value;
+
+        iter = NULL;
+        while (SDL_IterateHashTable(src_properties->props, &key, &value, &iter)) {
+            const char *src_name = (const char *)key;
+            const SDL_Property *src_property = (const SDL_Property *)value;
+            char *dst_name;
+            SDL_Property *dst_property;
+
+            if (src_property->cleanup) {
+                /* Can't copy properties with cleanup functions, we don't know how to duplicate the data */
+                continue;
+            }
+
+            SDL_RemoveFromHashTable(dst_properties->props, src_name);
+
+            dst_name = SDL_strdup(src_name);
+            if (!dst_name) {
+                result = -1;
+                continue;
+            }
+            dst_property = (SDL_Property *)SDL_malloc(sizeof(*dst_property));
+            if (!dst_property) {
+                SDL_free(dst_name);
+                result = -1;
+                continue;
+            }
+            SDL_copyp(dst_property, src_property);
+            if (src_property->type == SDL_PROPERTY_TYPE_STRING) {
+                dst_property->value.string_value = SDL_strdup(src_property->value.string_value);
+            }
+            if (!SDL_InsertIntoHashTable(dst_properties->props, dst_name, dst_property)) {
+                SDL_FreePropertyWithCleanup(dst_name, dst_property, NULL, SDL_FALSE);
+                result = -1;
+            }
+        }
+    }
+    SDL_UnlockMutex(dst_properties->lock);
+    SDL_UnlockMutex(src_properties->lock);
+
+    return result;
+}
+
 int SDL_LockProperties(SDL_PropertiesID props)
 {
     SDL_Properties *properties = NULL;
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 1dc5a5405ef2..631fe95d66e6 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -969,6 +969,7 @@ SDL3_0.0.0 {
     SDL_SetSurfaceColorspace;
     SDL_GetSurfaceColorspace;
     SDL_ConvertSurfaceFormatAndColorspace;
+    SDL_CopyProperties;
     # 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 42a2e4846f92..4f479e8c15c7 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -994,3 +994,4 @@
 #define SDL_SetSurfaceColorspace SDL_SetSurfaceColorspace_REAL
 #define SDL_GetSurfaceColorspace SDL_GetSurfaceColorspace_REAL
 #define SDL_ConvertSurfaceFormatAndColorspace SDL_ConvertSurfaceFormatAndColorspace_REAL
+#define SDL_CopyProperties SDL_CopyProperties_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index b6221bb51d90..2fac10f7fa52 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1019,3 +1019,4 @@ SDL_DYNAPI_PROC(int,SDL_ConvertPixelsAndColorspace,(int a, int b, Uint32 c, SDL_
 SDL_DYNAPI_PROC(int,SDL_SetSurfaceColorspace,(SDL_Surface *a, SDL_Colorspace b),(a,b),return)
 SDL_DYNAPI_PROC(int,SDL_GetSurfaceColorspace,(SDL_Surface *a, SDL_Colorspace *b),(a,b),return)
 SDL_DYNAPI_PROC(SDL_Surface*,SDL_ConvertSurfaceFormatAndColorspace,(SDL_Surface *a, Uint32 b, SDL_Colorspace c),(a,b,c),return)
+SDL_DYNAPI_PROC(int,SDL_CopyProperties,(SDL_PropertiesID a, SDL_PropertiesID b),(a,b),return)
diff --git a/test/testautomation_properties.c b/test/testautomation_properties.c
index 1fa53d022cc2..e825c3b45501 100644
--- a/test/testautomation_properties.c
+++ b/test/testautomation_properties.c
@@ -213,6 +213,67 @@ static int properties_testBasic(void *arg)
     return TEST_COMPLETED;
 }
 
+/**
+ * Test copy functionality
+ */
+static void SDLCALL copy_cleanup(void *userdata, void *value)
+{
+}
+static int properties_testCopy(void *arg)
+{
+    SDL_PropertiesID a, b;
+    int num;
+    const char *string;
+    void *data;
+    int result;
+
+    a = SDL_CreateProperties();
+    SDL_SetNumberProperty(a, "num", 1);
+    SDL_SetStringProperty(a, "string", "foo");
+    SDL_SetProperty(a, "data", &a);
+    SDL_SetPropertyWithCleanup(a, "cleanup", &a, copy_cleanup, &a);
+
+    b = SDL_CreateProperties();
+    SDL_SetNumberProperty(b, "num", 2);
+
+    SDLTest_AssertPass("Call to SDL_CopyProperties(a, 0)");
+    result = SDL_CopyProperties(a, 0);
+    SDLTest_AssertCheck(result == -1,
+                        "SDL_CopyProperties() result, got %d, expected -1", result);
+
+    SDLTest_AssertPass("Call to SDL_CopyProperties(0, b)");
+    result = SDL_CopyProperties(0, b);
+    SDLTest_AssertCheck(result == -1,
+                        "SDL_CopyProperties() result, got %d, expected -1", result);
+
+    SDLTest_AssertPass("Call to SDL_CopyProperties(a, b)");
+    result = SDL_CopyProperties(a, b);
+    SDLTest_AssertCheck(result == 0,
+        "SDL_CopyProperties() result, got %d, expected 0", result);
+
+    SDL_DestroyProperties(a);
+
+    num = SDL_GetNumberProperty(b, "num", 0);
+    SDLTest_AssertCheck(num == 1,
+        "Checking number property, got %d, expected 1", num);
+
+    string = SDL_GetStringProperty(b, "string", NULL);
+    SDLTest_AssertCheck(string && SDL_strcmp(string, "foo") == 0,
+        "Checking string property, got \"%s\", expected \"foo\"", string);
+
+    data = SDL_GetProperty(b, "data", NULL);
+    SDLTest_AssertCheck(data == &a,
+        "Checking data property, got %p, expected %p", data, &a);
+
+    data = SDL_GetProperty(b, "cleanup", NULL);
+    SDLTest_AssertCheck(data == NULL,
+        "Checking cleanup property, got %p, expected NULL", data);
+
+    SDL_DestroyProperties(b);
+
+    return TEST_COMPLETED;
+}
+
 /**
  * Test cleanup functionality
  */
@@ -324,21 +385,29 @@ static int properties_testLocking(void *arg)
 /* ================= Test References ================== */
 
 /* Properties test cases */
-static const SDLTest_TestCaseReference propertiesTest1 = {
+static const SDLTest_TestCaseReference propertiesTestBasic = {
     (SDLTest_TestCaseFp)properties_testBasic, "properties_testBasic", "Test basic property functionality", TEST_ENABLED
 };
 
-static const SDLTest_TestCaseReference propertiesTest2 = {
+static const SDLTest_TestCaseReference propertiesTestCopy = {
+    (SDLTest_TestCaseFp)properties_testCopy, "properties_testCopy", "Test property copy functionality", TEST_ENABLED
+};
+
+static const SDLTest_TestCaseReference propertiesTestCleanup = {
     (SDLTest_TestCaseFp)properties_testCleanup, "properties_testCleanup", "Test property cleanup functionality", TEST_ENABLED
 };
 
-static const SDLTest_TestCaseReference propertiesTest3 = {
+static const SDLTest_TestCaseReference propertiesTestLocking = {
     (SDLTest_TestCaseFp)properties_testLocking, "properties_testLocking", "Test property locking functionality", TEST_ENABLED
 };
 
 /* Sequence of Properties test cases */
 static const SDLTest_TestCaseReference *propertiesTests[] = {
-    &propertiesTest1, &propertiesTest2, &propertiesTest3, NULL
+    &propertiesTestBasic,
+    &propertiesTestCopy,
+    &propertiesTestCleanup,
+    &propertiesTestLocking,
+    NULL
 };
 
 /* Properties test suite (global) */