From 507179ee74eeb7e549d47d15ac15fdc97486632c Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Mon, 7 Sep 2026 16:28:58 -0400
Subject: [PATCH] misc: Added SDL_EncodeURL.
Fixes #16254.
---
include/SDL3/SDL_misc.h | 43 ++++++++++++++++++++++++++++++
src/dynapi/SDL_dynapi.exports | 1 +
src/dynapi/SDL_dynapi.sym | 1 +
src/dynapi/SDL_dynapi_overrides.h | 1 +
src/dynapi/SDL_dynapi_procs.h | 1 +
src/misc/SDL_url.c | 44 +++++++++++++++++++++++++++++++
test/testurl.c | 31 ++++++++++++++++++----
7 files changed, 117 insertions(+), 5 deletions(-)
diff --git a/include/SDL3/SDL_misc.h b/include/SDL3/SDL_misc.h
index 654c0055807ce..e202b2d366920 100644
--- a/include/SDL3/SDL_misc.h
+++ b/include/SDL3/SDL_misc.h
@@ -71,6 +71,49 @@ extern "C" {
*/
extern SDL_DECLSPEC bool SDLCALL SDL_OpenURL(const char *url);
+
+/**
+ * Encode a string for use in a URL.
+ *
+ * This encodes a string according to RFC3986 section 2, using
+ * "percent-encoding," which is the act of turning a single byte into a
+ * three-character sequence. For example, a space character (ASCII 32, or 0x20)
+ * is converted into the string "%20".
+ *
+ * Only certain bytes are encoded. What RFC3986 calls "unreserved characters"
+ * are not: low-ASCII alphanumeric chars, and four others: `=._~`.
+ *
+ * While using UTF-8 is encouraged, this function does not care if the string
+ * is well-formed UTF-8; it will treat the string as individual bytes to
+ * encode, with some of those bytes being unreserved characters.
+ *
+ * This function's primary use is encoding strings for use as parameters
+ * appended to a URL passed to SDL_OpenURL(), however, this can _also_ be
+ * useful on some systems for passing a filesystem path with a "file://"
+ * prefix to that function to launch a file manager. In such cases, it would
+ * be better if '/' and '\\' aren't encoded, so one can pass a null-terminated
+ * list of characters to `no_encode_chars` to add to the list of characters
+ * that aren't encoded:
+ *
+ * ```c
+ * char *encoded = SDL_EncodeURL(MyFilePath, "/\\");
+ * ```
+ *
+ * These characters will be added to the normal "unreserved characters" list
+ * and not encoded. Passing NULL is valid and adds no characters to the list.
+ *
+ * \param str the string to encode.
+ * \param no_encode_chars a string of extra characters that are not to be encoded.
+ * \returns the encoded string or NULL on failure; call SDL_GetError() for more
+ * information. This should be freed with SDL_free() when it is no
+ * longer needed.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.6.0.
+ */
+extern SDL_DECLSPEC char * SDLCALL SDL_EncodeURL(const char *str, const char *no_encode_chars);
+
/* Ends C function definitions when using C++ */
#ifdef __cplusplus
}
diff --git a/src/dynapi/SDL_dynapi.exports b/src/dynapi/SDL_dynapi.exports
index b223f40a89972..657e43332bae7 100644
--- a/src/dynapi/SDL_dynapi.exports
+++ b/src/dynapi/SDL_dynapi.exports
@@ -1317,3 +1317,4 @@ _SDL_RequestOpenHarmonyPermission
_SDL_GetAudioDeviceProperties
_SDL_SetSoftwareRendererWindow
_SDL_SetSoftwareRendererSurface
+_SDL_EncodeURL
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 5d699b2f3202b..9ea8c9d7336c0 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1318,6 +1318,7 @@ SDL3_0.0.0 {
SDL_GetAudioDeviceProperties;
SDL_SetSoftwareRendererWindow;
SDL_SetSoftwareRendererSurface;
+ SDL_EncodeURL;
# 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 8ca5f71b8478d..2df1ee9216d2e 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1344,3 +1344,4 @@
#define SDL_GetAudioDeviceProperties SDL_GetAudioDeviceProperties_REAL
#define SDL_SetSoftwareRendererWindow SDL_SetSoftwareRendererWindow_REAL
#define SDL_SetSoftwareRendererSurface SDL_SetSoftwareRendererSurface_REAL
+#define SDL_EncodeURL SDL_EncodeURL_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index d34980d2f4b7a..d84e8166acfa2 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1352,3 +1352,4 @@ SDL_DYNAPI_PROC(bool,SDL_RequestOpenHarmonyPermission,(const char *a,SDL_Request
SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetAudioDeviceProperties,(SDL_AudioDeviceID a),(a),return)
SDL_DYNAPI_PROC(bool,SDL_SetSoftwareRendererWindow,(SDL_Renderer *a,SDL_Window *b),(a,b),return)
SDL_DYNAPI_PROC(bool,SDL_SetSoftwareRendererSurface,(SDL_Renderer *a,SDL_Surface *b),(a,b),return)
+SDL_DYNAPI_PROC(char*,SDL_EncodeURL,(const char *a,const char *b),(a,b),return)
diff --git a/src/misc/SDL_url.c b/src/misc/SDL_url.c
index 7ec2883074b67..3b814f784690f 100644
--- a/src/misc/SDL_url.c
+++ b/src/misc/SDL_url.c
@@ -29,3 +29,47 @@ bool SDL_OpenURL(const char *url)
}
return SDL_SYS_OpenURL(url);
}
+
+char *SDL_EncodeURL(const char *str, const char *no_encode_chars)
+{
+ const size_t slen = SDL_strlen(str) + 1;
+ size_t allocation = slen + 64; // at least this long plus a little more, in case one allocation covers it.
+ size_t dsti = 0;
+ char *retval = (char *) SDL_malloc(allocation);
+ if (!retval) {
+ return NULL;
+ }
+
+ for (size_t i = 0; i < slen; i++) {
+ if (dsti >= (allocation - 4)) {
+ allocation += 64;
+ char *ptr = (char *) SDL_realloc(retval, allocation);
+ if (!ptr) {
+ SDL_free(retval);
+ return NULL;
+ }
+ retval = ptr;
+ }
+
+ const char ch = str[i];
+
+ if ( ((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z')) ||
+ ((ch >= '0') && (ch <= '9')) ||
+ ((ch == '=') || (ch == '.') || (ch == '_') || (ch == '~') || (ch == '\0')) ||
+ (no_encode_chars && (SDL_strchr(no_encode_chars, ch) != NULL)) ) {
+ retval[dsti++] = ch; // unreserved char, null terminator char, or explicitly requested not to encode.
+ } else {
+ SDL_snprintf(&retval[dsti], 4, "%%%02X", (unsigned int) ch);
+ dsti += 3;
+ }
+ }
+
+ // shrink the allocation, if possible.
+ char *ptr = (char *) SDL_realloc(retval, SDL_strlen(retval) + 1);
+ if (ptr) {
+ retval = ptr;
+ }
+
+ return retval;
+}
+
diff --git a/test/testurl.c b/test/testurl.c
index ef61eb2f6264f..5be870bf91167 100644
--- a/test/testurl.c
+++ b/test/testurl.c
@@ -13,14 +13,25 @@
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_test.h>
-static void tryOpenURL(const char *url)
+static void tryOpenURL(const char *url, bool urlencode, const char *no_encode_chars)
{
+ char *encoded = NULL;
+ if (urlencode) {
+ encoded = SDL_EncodeURL(url, no_encode_chars);
+ if (!encoded) {
+ SDL_Log("URL encoding failed! %s", SDL_GetError());
+ }
+ url = encoded;
+ }
+
SDL_Log("Opening '%s' ...", url);
if (SDL_OpenURL(url)) {
SDL_Log(" success!");
} else {
SDL_Log(" failed! %s", SDL_GetError());
}
+
+ SDL_free(encoded);
}
int main(int argc, char **argv)
@@ -28,6 +39,8 @@ int main(int argc, char **argv)
const char *url = NULL;
SDLTest_CommonState *state = SDLTest_CommonCreateState(argv, 0);
bool use_gui = false;
+ bool urlencode = false;
+ const char *no_encode_chars = NULL;
/* Parse commandline */
for (int i = 1; i < argc;) {
@@ -41,11 +54,19 @@ int main(int argc, char **argv)
} else if (SDL_strcasecmp(argv[i], "--gui") == 0) {
use_gui = true;
consumed = 1;
+ } else if (SDL_strcasecmp(argv[i], "--urlencode") == 0) {
+ urlencode = true;
+ consumed = 1;
+ } else if (SDL_strcasecmp(argv[i], "--no-encode-chars") == 0) {
+ no_encode_chars = argv[i + 1];
+ consumed = 2;
}
}
if (consumed <= 0) {
static const char *options[] = {
- "[--gui]"
+ "[--gui]",
+ "[--urlencode]",
+ "[--no-encode-chars ...]",
"[URL [...]]",
NULL,
};
@@ -61,7 +82,7 @@ int main(int argc, char **argv)
}
if (!use_gui) {
- tryOpenURL(url);
+ tryOpenURL(url, urlencode, no_encode_chars);
} else {
SDL_Event event;
bool quit = false;
@@ -69,10 +90,10 @@ int main(int argc, char **argv)
while (!quit) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
- tryOpenURL(url);
+ tryOpenURL(url, urlencode, no_encode_chars);
} else if (event.type == SDL_EVENT_KEY_DOWN) {
if (event.key.key == SDLK_SPACE) {
- tryOpenURL(url);
+ tryOpenURL(url, urlencode, no_encode_chars);
} else if (event.key.key == SDLK_ESCAPE) {
quit = true;
}