SDL: Document iconv functions + add testautomation (#10131)

From 0fa2049fef2abe24bd8307ee5605edad96240848 Mon Sep 17 00:00:00 2001
From: Anonymous Maarten <[EMAIL REDACTED]>
Date: Tue, 6 Aug 2024 17:12:25 +0000
Subject: [PATCH] Document iconv functions + add testautomation (#10131)

* stdinc: document SDL_iconv* functions
* iconv: add automation tests
* iconv: don't potentially crash on invalid inputs
---
 include/SDL3/SDL_stdinc.h    |  66 +++++++++++++++++-
 src/stdlib/SDL_iconv.c       |  14 +++-
 test/testautomation_stdlib.c | 129 +++++++++++++++++++++++++++++++++++
 3 files changed, 204 insertions(+), 5 deletions(-)

diff --git a/include/SDL3/SDL_stdinc.h b/include/SDL3/SDL_stdinc.h
index 14e07509952c3..cbd374432a942 100644
--- a/include/SDL3/SDL_stdinc.h
+++ b/include/SDL3/SDL_stdinc.h
@@ -2899,14 +2899,70 @@ extern SDL_DECLSPEC float SDLCALL SDL_tanf(float x);
 #define SDL_ICONV_EILSEQ    (size_t)-3
 #define SDL_ICONV_EINVAL    (size_t)-4
 
-/* SDL_iconv_* are now always real symbols/types, not macros or inlined. */
 typedef struct SDL_iconv_data_t *SDL_iconv_t;
+
+/**
+ * This function allocates a context for the specified character set conversion.
+ *
+ * \param tocode The target character encoding, must not be NULL.
+ * \param fromcode The source character encoding, must not be NULL.
+ * \returns a handle that must be freed with SDL_iconv_close,
+ *          or SDL_ICONV_ERROR on failure.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_iconv
+ * \sa SDL_iconv_close
+ * \sa SDL_iconv_string
+ */
 extern SDL_DECLSPEC SDL_iconv_t SDLCALL SDL_iconv_open(const char *tocode,
                                                    const char *fromcode);
+
+/**
+ * This function frees a context used for character set conversion.
+ *
+ * \param cd The character set conversion handle.
+ * \returns 0 on success, or -1 on failure.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_iconv
+ * \sa SDL_iconv_open
+ * \sa SDL_iconv_string
+ */
 extern SDL_DECLSPEC int SDLCALL SDL_iconv_close(SDL_iconv_t cd);
+
+/**
+ * This function converts text between encodings, reading from and writing to a buffer.
+ * It returns the number of succesful conversions.
+ *
+ * \param cd The character set conversion context, created in SDL_iconv_open().
+ * \param inbuf Address of variable that points to the first character of the input sequence.
+ * \param inbytesleft The number of bytes in the input buffer.
+ * \param outbuf Address of variable that points to the output buffer.
+ * \param outbytesleft The number of bytes in the output buffer.
+ * \returns the number of conversions on success, else
+ *      SDL_ICONV_E2BIG is returned when the output buffer is too small, or
+ *      SDL_ICONV_EILSEQ is returned when an invalid input sequence is encountered, or
+ *      SDL_ICONV_EINVAL is returned when an incomplete input sequence is encountered.
+ *
+ * On exit:
+ * - inbuf will point to the beginning of the next multibyte sequence.
+ *   On error, this is the location of the problematic input sequence.
+ *   On success, this is the end of the input sequence.
+ * - inbytesleft will be set to the number of bytes left to convert, which will be 0 on success.
+ * - outbuf will point to the location where to store the next output byte.
+ * - outbytesleft will be set to the number of bytes left in the output buffer.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_iconv_open
+ * \sa SDL_iconv_close
+ * \sa SDL_iconv_string
+ */
 extern SDL_DECLSPEC size_t SDLCALL SDL_iconv(SDL_iconv_t cd, const char **inbuf,
-                                         size_t * inbytesleft, char **outbuf,
-                                         size_t * outbytesleft);
+                                         size_t *inbytesleft, char **outbuf,
+                                         size_t *outbytesleft);
 
 /**
  * Helper function to convert a string's encoding in one call.
@@ -2928,6 +2984,10 @@ extern SDL_DECLSPEC size_t SDLCALL SDL_iconv(SDL_iconv_t cd, const char **inbuf,
  * \returns a new string, converted to the new encoding, or NULL on error.
  *
  * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_iconv_open
+ * \sa SDL_iconv_close
+ * \sa SDL_iconv
  */
 extern SDL_DECLSPEC char * SDLCALL SDL_iconv_string(const char *tocode,
                                                const char *fromcode,
diff --git a/src/stdlib/SDL_iconv.c b/src/stdlib/SDL_iconv.c
index dfda45b480b72..b31ee0c42c198 100644
--- a/src/stdlib/SDL_iconv.c
+++ b/src/stdlib/SDL_iconv.c
@@ -39,6 +39,9 @@ SDL_iconv_t SDL_iconv_open(const char *tocode, const char *fromcode)
 
 int SDL_iconv_close(SDL_iconv_t cd)
 {
+    if ((size_t)cd == SDL_ICONV_ERROR) {
+        return -1;
+    }
     return iconv_close((iconv_t)((uintptr_t)cd));
 }
 
@@ -46,6 +49,9 @@ size_t SDL_iconv(SDL_iconv_t cd,
           const char **inbuf, size_t *inbytesleft,
           char **outbuf, size_t *outbytesleft)
 {
+    if ((size_t)cd == SDL_ICONV_ERROR) {
+        return SDL_ICONV_ERROR;
+    }
     /* iconv's second parameter may or may not be `const char const *` depending on the
        C runtime's whims. Casting to void * seems to make everyone happy, though. */
     const size_t retCode = iconv((iconv_t)((uintptr_t)cd), (void *)inbuf, inbytesleft, outbuf, outbytesleft);
@@ -236,6 +242,9 @@ size_t SDL_iconv(SDL_iconv_t cd,
     Uint32 ch = 0;
     size_t total;
 
+    if ((size_t)cd == SDL_ICONV_ERROR) {
+        return SDL_ICONV_ERROR;
+    }
     if (!inbuf || !*inbuf) {
         /* Reset the context */
         return 0;
@@ -769,9 +778,10 @@ size_t SDL_iconv(SDL_iconv_t cd,
 
 int SDL_iconv_close(SDL_iconv_t cd)
 {
-    if (cd != (SDL_iconv_t)-1) {
-        SDL_free(cd);
+    if (cd == (SDL_iconv_t)-1) {
+        return -1;
     }
+    SDL_free(cd);
     return 0;
 }
 
diff --git a/test/testautomation_stdlib.c b/test/testautomation_stdlib.c
index 44a0907264019..5e649ab391ab5 100644
--- a/test/testautomation_stdlib.c
+++ b/test/testautomation_stdlib.c
@@ -1064,6 +1064,130 @@ stdlib_overflow(void *arg)
     return TEST_COMPLETED;
 }
 
+static void format_for_description(char *buffer, size_t buflen, const char *text) {
+    if (text == NULL) {
+        SDL_strlcpy(buffer, "NULL", buflen);
+    } else {
+        SDL_snprintf(buffer, buflen, "\"%s\"", text);
+    }
+}
+
+static int
+stdlib_iconv(void *arg)
+{
+    struct {
+        SDL_bool expect_success;
+        const char *from_encoding;
+        const char *text;
+        const char *to_encoding;
+        const char *expected;
+    } inputs[] = {
+        { SDL_FALSE, "bogus-from-encoding", NULL,                           "bogus-to-encoding",   NULL },
+        { SDL_FALSE, "bogus-from-encoding", "hello world",                  "bogus-to-encoding",   NULL },
+        { SDL_FALSE, "bogus-from-encoding", "hello world",                  "ascii",               NULL },
+        { SDL_TRUE,  "utf-8",               NULL,                           "ascii",               "" },
+        { SDL_TRUE,  "utf-8",               "hello world",                  "ascii",               "hello world" },
+        { SDL_TRUE,  "utf-8",               "\xe2\x8c\xa8\xf0\x9f\x92\xbb", "utf-16le",            "\x28\x23\x3d\xd8\xbb\xdc\x00" },
+    };
+    SDL_iconv_t cd;
+    size_t i;
+
+    for (i = 0; i < SDL_arraysize(inputs); i++) {
+        char to_encoding_str[32];
+        char from_encoding_str[32];
+        char text_str[32];
+        size_t len_text = 0;
+        int r;
+        char out_buffer[6];
+        const char *in_ptr;
+        size_t in_pos;
+        char *out_ptr;
+        char *output;
+        size_t iconv_result;
+        size_t out_len;
+        SDL_bool is_error;
+        size_t out_pos;
+
+        SDLTest_AssertPass("case %d", (int)i);
+        format_for_description(to_encoding_str, SDL_arraysize(to_encoding_str), inputs[i].to_encoding);
+        format_for_description(from_encoding_str, SDL_arraysize(from_encoding_str), inputs[i].from_encoding);
+        format_for_description(text_str, SDL_arraysize(text_str), inputs[i].text);
+
+        if (inputs[i].text) {
+            len_text = SDL_strlen(inputs[i].text) + 1;
+        }
+
+        SDLTest_AssertPass("About to call SDL_iconv_open(%s, %s)", to_encoding_str, from_encoding_str);
+        cd = SDL_iconv_open(inputs[i].to_encoding, inputs[i].from_encoding);
+        if (inputs[i].expect_success) {
+            SDLTest_AssertCheck(cd != (SDL_iconv_t)SDL_ICONV_ERROR, "result must NOT be SDL_ICONV_ERROR");
+        } else {
+            SDLTest_AssertCheck(cd == (SDL_iconv_t)SDL_ICONV_ERROR, "result must be SDL_ICONV_ERROR");
+        }
+
+        in_ptr = inputs[i].text;
+        in_pos = 0;
+        out_pos = 0;
+        do {
+            size_t in_left;
+            size_t count_written;
+            size_t count_read;
+
+            in_left = len_text - in_pos;
+            out_ptr = out_buffer;
+            out_len = SDL_arraysize(out_buffer);
+            SDLTest_AssertPass("About to call SDL_iconv(cd, %s+%d, .., dest, ..)", text_str, (int)in_pos);
+            iconv_result = SDL_iconv(cd, &in_ptr, &in_left, &out_ptr, &out_len);
+            count_written = SDL_arraysize(out_buffer) - out_len;
+            count_read = in_ptr - inputs[i].text - in_pos;
+            in_pos += count_read;
+
+            is_error = iconv_result == SDL_ICONV_ERROR
+                       || iconv_result == SDL_ICONV_EILSEQ
+                       || iconv_result == SDL_ICONV_EINVAL;
+            if (inputs[i].expect_success) {
+                SDLTest_AssertCheck(!is_error, "result must NOT be an error code");
+                SDLTest_AssertCheck(count_written > 0 || inputs[i].expected[out_pos] == '\0', "%" SDL_PRIu64 " bytes have been written", (Uint64)count_written);
+                SDLTest_AssertCheck(out_pos <= SDL_strlen(inputs[i].expected), "Data written by SDL_iconv cannot be longer then reference output");
+                SDLTest_CompareMemory(out_buffer, count_written, inputs[i].expected + out_pos, count_written);
+            } else {
+                SDLTest_AssertCheck(is_error, "result must be an error code");
+                break;
+            }
+            out_pos += count_written;
+            if (count_written == 0) {
+                break;
+            }
+            if (count_read == 0) {
+                SDLTest_AssertCheck(SDL_FALSE, "SDL_iconv wrote data, but read no data");
+                break;
+            }
+        } while (!is_error && in_pos < len_text);
+
+        SDLTest_AssertPass("About to call SDL_iconv_close(cd)");
+        r = SDL_iconv_close(cd);
+        if (inputs[i].expect_success) {
+            SDLTest_AssertCheck(r == 0, "result must be 0");
+        } else {
+            SDLTest_AssertCheck(r == -1, "result must be -1");
+        }
+
+        SDLTest_AssertPass("About to call SDL_iconv_string(%s, %s, %s, %" SDL_PRIu64 ")",
+                           to_encoding_str, from_encoding_str, text_str, (Uint64)len_text);
+        output = SDL_iconv_string(inputs[i].to_encoding, inputs[i].from_encoding, inputs[i].text, len_text);
+        if (inputs[i].expect_success) {
+            SDLTest_AssertCheck(output != NULL, "result must NOT be NULL");
+            SDLTest_AssertCheck(SDL_strncmp(inputs[i].expected, output, SDL_strlen(inputs[i].expected)) == 0,
+                                "converted string should be correct");
+        } else {
+            SDLTest_AssertCheck(output == NULL, "result must be NULL");
+        }
+        SDL_free(output);
+    }
+
+    return TEST_COMPLETED;
+}
+
 /* ================= Test References ================== */
 
 /* Standard C routine test cases */
@@ -1103,6 +1227,10 @@ static const SDLTest_TestCaseReference stdlibTestOverflow = {
     stdlib_overflow, "stdlib_overflow", "Overflow detection", TEST_ENABLED
 };
 
+static const SDLTest_TestCaseReference stdlibIconv = {
+    stdlib_iconv, "stdlib_iconv", "Calls to iconv", TEST_ENABLED
+};
+
 /* Sequence of Standard C routine test cases */
 static const SDLTest_TestCaseReference *stdlibTests[] = {
     &stdlibTest_strnlen,
@@ -1114,6 +1242,7 @@ static const SDLTest_TestCaseReference *stdlibTests[] = {
     &stdlibTest_sscanf,
     &stdlibTest_aligned_alloc,
     &stdlibTestOverflow,
+    &stdlibIconv,
     NULL
 };