SDL: Refactored SDL_CreateJoystickName() into a general SDL_CreateDeviceName()

From 5d776c070a92efadde3469ed17881d78fbc273fd Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 21 Feb 2025 08:28:57 -0800
Subject: [PATCH] Refactored SDL_CreateJoystickName() into a general
 SDL_CreateDeviceName()

---
 src/SDL_utils.c                       | 151 +++++++++++++++++++++
 src/SDL_utils_c.h                     |   2 +
 src/joystick/SDL_joystick.c           | 148 +--------------------
 src/video/windows/SDL_windowsevents.c | 181 ++++++++++----------------
 4 files changed, 226 insertions(+), 256 deletions(-)

diff --git a/src/SDL_utils.c b/src/SDL_utils.c
index b9a42e2370f4b..c1ceacb189d09 100644
--- a/src/SDL_utils.c
+++ b/src/SDL_utils.c
@@ -24,6 +24,9 @@
 #include <unistd.h>
 #endif
 
+#include "joystick/SDL_joystick_c.h" // For SDL_GetGamepadTypeFromVIDPID()
+
+
 // Common utility functions that aren't in the public API
 
 int SDL_powerof2(int x)
@@ -396,3 +399,151 @@ const char *SDL_GetPersistentString(const char *string)
     }
     return result;
 }
+
+static int PrefixMatch(const char *a, const char *b)
+{
+    int matchlen = 0;
+    while (*a && *b) {
+        if (SDL_tolower((unsigned char)*a++) == SDL_tolower((unsigned char)*b++)) {
+            ++matchlen;
+        } else {
+            break;
+        }
+    }
+    return matchlen;
+}
+
+char *SDL_CreateDeviceName(Uint16 vendor, Uint16 product, const char *vendor_name, const char *product_name)
+{
+    static struct
+    {
+        const char *prefix;
+        const char *replacement;
+    } replacements[] = {
+        { "ASTRO Gaming", "ASTRO" },
+        { "Bensussen Deutsch & Associates,Inc.(BDA)", "BDA" },
+        { "Guangzhou Chicken Run Network Technology Co., Ltd.", "GameSir" },
+        { "HORI CO.,LTD", "HORI" },
+        { "HORI CO.,LTD.", "HORI" },
+        { "Mad Catz Inc.", "Mad Catz" },
+        { "Nintendo Co., Ltd.", "Nintendo" },
+        { "NVIDIA Corporation ", "" },
+        { "Performance Designed Products", "PDP" },
+        { "QANBA USA, LLC", "Qanba" },
+        { "QANBA USA,LLC", "Qanba" },
+        { "Unknown ", "" },
+    };
+    char *name;
+    size_t i, len;
+
+    if (!vendor_name) {
+        vendor_name = "";
+    }
+    if (!product_name) {
+        product_name = "";
+    }
+
+    while (*vendor_name == ' ') {
+        ++vendor_name;
+    }
+    while (*product_name == ' ') {
+        ++product_name;
+    }
+
+    if (*vendor_name && *product_name) {
+        len = (SDL_strlen(vendor_name) + 1 + SDL_strlen(product_name) + 1);
+        name = (char *)SDL_malloc(len);
+        if (name) {
+            (void)SDL_snprintf(name, len, "%s %s", vendor_name, product_name);
+        }
+    } else if (*product_name) {
+        name = SDL_strdup(product_name);
+    } else if (vendor || product) {
+        // Couldn't find a controller name, try to give it one based on device type
+        switch (SDL_GetGamepadTypeFromVIDPID(vendor, product, NULL, true)) {
+        case SDL_GAMEPAD_TYPE_XBOX360:
+            name = SDL_strdup("Xbox 360 Controller");
+            break;
+        case SDL_GAMEPAD_TYPE_XBOXONE:
+            name = SDL_strdup("Xbox One Controller");
+            break;
+        case SDL_GAMEPAD_TYPE_PS3:
+            name = SDL_strdup("PS3 Controller");
+            break;
+        case SDL_GAMEPAD_TYPE_PS4:
+            name = SDL_strdup("PS4 Controller");
+            break;
+        case SDL_GAMEPAD_TYPE_PS5:
+            name = SDL_strdup("DualSense Wireless Controller");
+            break;
+        case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO:
+            name = SDL_strdup("Nintendo Switch Pro Controller");
+            break;
+        default:
+            len = (6 + 1 + 6 + 1);
+            name = (char *)SDL_malloc(len);
+            if (name) {
+                (void)SDL_snprintf(name, len, "0x%.4x/0x%.4x", vendor, product);
+            }
+            break;
+        }
+    } else {
+        name = SDL_strdup("Controller");
+    }
+
+    if (!name) {
+        return NULL;
+    }
+
+    // Trim trailing whitespace
+    for (len = SDL_strlen(name); (len > 0 && name[len - 1] == ' '); --len) {
+        // continue
+    }
+    name[len] = '\0';
+
+    // Compress duplicate spaces
+    for (i = 0; i < (len - 1);) {
+        if (name[i] == ' ' && name[i + 1] == ' ') {
+            SDL_memmove(&name[i], &name[i + 1], (len - i));
+            --len;
+        } else {
+            ++i;
+        }
+    }
+
+    // Perform any manufacturer replacements
+    for (i = 0; i < SDL_arraysize(replacements); ++i) {
+        size_t prefixlen = SDL_strlen(replacements[i].prefix);
+        if (SDL_strncasecmp(name, replacements[i].prefix, prefixlen) == 0) {
+            size_t replacementlen = SDL_strlen(replacements[i].replacement);
+            if (replacementlen <= prefixlen) {
+                SDL_memcpy(name, replacements[i].replacement, replacementlen);
+                SDL_memmove(name + replacementlen, name + prefixlen, (len - prefixlen) + 1);
+                len -= (prefixlen - replacementlen);
+            } else {
+                // FIXME: Need to handle the expand case by reallocating the string
+            }
+            break;
+        }
+    }
+
+    /* Remove duplicate manufacturer or product in the name
+     * e.g. Razer Razer Raiju Tournament Edition Wired
+     */
+    for (i = 1; i < (len - 1); ++i) {
+        int matchlen = PrefixMatch(name, &name[i]);
+        while (matchlen > 0) {
+            if (name[matchlen] == ' ' || name[matchlen] == '-') {
+                SDL_memmove(name, name + matchlen + 1, len - matchlen);
+                break;
+            }
+            --matchlen;
+        }
+        if (matchlen > 0) {
+            // We matched the manufacturer's name and removed it
+            break;
+        }
+    }
+
+    return name;
+}
diff --git a/src/SDL_utils_c.h b/src/SDL_utils_c.h
index 557dad49a59d0..e8e98f241e9c3 100644
--- a/src/SDL_utils_c.h
+++ b/src/SDL_utils_c.h
@@ -73,4 +73,6 @@ extern void SDL_SetObjectsInvalid(void);
 
 extern const char *SDL_GetPersistentString(const char *string);
 
+extern char *SDL_CreateDeviceName(Uint16 vendor, Uint16 product, const char *vendor_name, const char *product_name);
+
 #endif // SDL_utils_h_
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index a8bbe0c23f71b..9221bce9cb5b9 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -2546,158 +2546,14 @@ void SDL_GetJoystickGUIDInfo(SDL_GUID guid, Uint16 *vendor, Uint16 *product, Uin
     }
 }
 
-static int PrefixMatch(const char *a, const char *b)
-{
-    int matchlen = 0;
-    while (*a && *b) {
-        if (SDL_tolower((unsigned char)*a++) == SDL_tolower((unsigned char)*b++)) {
-            ++matchlen;
-        } else {
-            break;
-        }
-    }
-    return matchlen;
-}
-
 char *SDL_CreateJoystickName(Uint16 vendor, Uint16 product, const char *vendor_name, const char *product_name)
 {
-    static struct
-    {
-        const char *prefix;
-        const char *replacement;
-    } replacements[] = {
-        { "ASTRO Gaming", "ASTRO" },
-        { "Bensussen Deutsch & Associates,Inc.(BDA)", "BDA" },
-        { "Guangzhou Chicken Run Network Technology Co., Ltd.", "GameSir" },
-        { "HORI CO.,LTD", "HORI" },
-        { "HORI CO.,LTD.", "HORI" },
-        { "Mad Catz Inc.", "Mad Catz" },
-        { "Nintendo Co., Ltd.", "Nintendo" },
-        { "NVIDIA Corporation ", "" },
-        { "Performance Designed Products", "PDP" },
-        { "QANBA USA, LLC", "Qanba" },
-        { "QANBA USA,LLC", "Qanba" },
-        { "Unknown ", "" },
-    };
-    const char *custom_name;
-    char *name;
-    size_t i, len;
-
-    custom_name = GuessControllerName(vendor, product);
+    const char *custom_name = GuessControllerName(vendor, product);
     if (custom_name) {
         return SDL_strdup(custom_name);
     }
 
-    if (!vendor_name) {
-        vendor_name = "";
-    }
-    if (!product_name) {
-        product_name = "";
-    }
-
-    while (*vendor_name == ' ') {
-        ++vendor_name;
-    }
-    while (*product_name == ' ') {
-        ++product_name;
-    }
-
-    if (*vendor_name && *product_name) {
-        len = (SDL_strlen(vendor_name) + 1 + SDL_strlen(product_name) + 1);
-        name = (char *)SDL_malloc(len);
-        if (name) {
-            (void)SDL_snprintf(name, len, "%s %s", vendor_name, product_name);
-        }
-    } else if (*product_name) {
-        name = SDL_strdup(product_name);
-    } else if (vendor || product) {
-        // Couldn't find a controller name, try to give it one based on device type
-        switch (SDL_GetGamepadTypeFromVIDPID(vendor, product, NULL, true)) {
-        case SDL_GAMEPAD_TYPE_XBOX360:
-            name = SDL_strdup("Xbox 360 Controller");
-            break;
-        case SDL_GAMEPAD_TYPE_XBOXONE:
-            name = SDL_strdup("Xbox One Controller");
-            break;
-        case SDL_GAMEPAD_TYPE_PS3:
-            name = SDL_strdup("PS3 Controller");
-            break;
-        case SDL_GAMEPAD_TYPE_PS4:
-            name = SDL_strdup("PS4 Controller");
-            break;
-        case SDL_GAMEPAD_TYPE_PS5:
-            name = SDL_strdup("DualSense Wireless Controller");
-            break;
-        case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO:
-            name = SDL_strdup("Nintendo Switch Pro Controller");
-            break;
-        default:
-            len = (6 + 1 + 6 + 1);
-            name = (char *)SDL_malloc(len);
-            if (name) {
-                (void)SDL_snprintf(name, len, "0x%.4x/0x%.4x", vendor, product);
-            }
-            break;
-        }
-    } else {
-        name = SDL_strdup("Controller");
-    }
-
-    if (!name) {
-        return NULL;
-    }
-
-    // Trim trailing whitespace
-    for (len = SDL_strlen(name); (len > 0 && name[len - 1] == ' '); --len) {
-        // continue
-    }
-    name[len] = '\0';
-
-    // Compress duplicate spaces
-    for (i = 0; i < (len - 1);) {
-        if (name[i] == ' ' && name[i + 1] == ' ') {
-            SDL_memmove(&name[i], &name[i + 1], (len - i));
-            --len;
-        } else {
-            ++i;
-        }
-    }
-
-    // Perform any manufacturer replacements
-    for (i = 0; i < SDL_arraysize(replacements); ++i) {
-        size_t prefixlen = SDL_strlen(replacements[i].prefix);
-        if (SDL_strncasecmp(name, replacements[i].prefix, prefixlen) == 0) {
-            size_t replacementlen = SDL_strlen(replacements[i].replacement);
-            if (replacementlen <= prefixlen) {
-                SDL_memcpy(name, replacements[i].replacement, replacementlen);
-                SDL_memmove(name + replacementlen, name + prefixlen, (len - prefixlen) + 1);
-                len -= (prefixlen - replacementlen);
-            } else {
-                // FIXME: Need to handle the expand case by reallocating the string
-            }
-            break;
-        }
-    }
-
-    /* Remove duplicate manufacturer or product in the name
-     * e.g. Razer Razer Raiju Tournament Edition Wired
-     */
-    for (i = 1; i < (len - 1); ++i) {
-        int matchlen = PrefixMatch(name, &name[i]);
-        while (matchlen > 0) {
-            if (name[matchlen] == ' ' || name[matchlen] == '-') {
-                SDL_memmove(name, name + matchlen + 1, len - matchlen);
-                break;
-            }
-            --matchlen;
-        }
-        if (matchlen > 0) {
-            // We matched the manufacturer's name and removed it
-            break;
-        }
-    }
-
-    return name;
+    return SDL_CreateDeviceName(vendor, product, vendor_name, product_name);
 }
 
 SDL_GUID SDL_CreateJoystickGUID(Uint16 bus, Uint16 vendor, Uint16 product, Uint16 version, const char *vendor_name, const char *product_name, Uint8 driver_signature, Uint8 driver_data)
diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c
index 727f3a4e70dd7..897d999bb5c37 100644
--- a/src/video/windows/SDL_windowsevents.c
+++ b/src/video/windows/SDL_windowsevents.c
@@ -861,130 +861,85 @@ static bool HasDeviceID(Uint32 deviceID, const Uint32 *list, int count)
 }
 
 #if !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES)
-static void GetDeviceName(HANDLE hDevice, HDEVINFO devinfo, const char *instance, char *name, size_t len)
+static char *GetDeviceName(HANDLE hDevice, HDEVINFO devinfo, const char *instance, bool hid_loaded)
 {
-    name[0] = '\0';
-
-    SP_DEVINFO_DATA data;
-    SDL_zero(data);
-    data.cbSize = sizeof(data);
-    for (DWORD i = 0;; ++i) {
-        if (!SetupDiEnumDeviceInfo(devinfo, i, &data)) {
-            if (GetLastError() == ERROR_NO_MORE_ITEMS) {
-                break;
-            } else {
-                continue;
-            }
-        }
+    char *name = NULL;
 
-        char DeviceInstanceId[64];
-        if (!SetupDiGetDeviceInstanceIdA(devinfo, &data, DeviceInstanceId, sizeof(DeviceInstanceId), NULL))
-            continue;
+    // These are 126 for USB, but can be longer for Bluetooth devices
+    WCHAR vend[256], prod[256];
+    vend[0] = 0;
+    prod[0] = 0;
 
-        if (SDL_strcasecmp(instance, DeviceInstanceId) == 0) {
-            SetupDiGetDeviceRegistryPropertyA(devinfo, &data, SPDRP_DEVICEDESC, NULL, (PBYTE)name, (DWORD)len, NULL);
-            break;
+    HIDD_ATTRIBUTES attr;
+    attr.VendorID = 0;
+    attr.ProductID = 0;
+    attr.Size = sizeof(attr);
+
+    if (hid_loaded) {
+        char devName[MAX_PATH + 1];
+        UINT cap = sizeof(devName) - 1;
+        UINT len = GetRawInputDeviceInfoA(hDevice, RIDI_DEVICENAME, devName, &cap);
+        if (len != (UINT)-1) {
+            devName[len] = '\0';
+
+            // important: for devices with exclusive access mode as per
+            // https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/top-level-collections-opened-by-windows-for-system-use
+            // they can only be opened with a desired access of none instead of generic read.
+            HANDLE hFile = CreateFileA(devName, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
+            if (hFile != INVALID_HANDLE_VALUE) {
+                SDL_HidD_GetAttributes(hFile, &attr);
+                SDL_HidD_GetManufacturerString(hFile, vend, sizeof(vend));
+                SDL_HidD_GetProductString(hFile, prod, sizeof(prod));
+                CloseHandle(hFile);
+            }
         }
     }
 
-    size_t desc_len = 0;
-    for (size_t i = 0; i < len; i++) {
-        if (name[i] == '\0') {
-            desc_len = i;
-            break;
+    if (prod[0]) {
+        char *vendor_name = WIN_StringToUTF8W(vend);
+        char *product_name = WIN_StringToUTF8W(prod);
+        if (product_name) {
+            name = SDL_CreateDeviceName(attr.VendorID, attr.ProductID, vendor_name, product_name);
         }
+        SDL_free(vendor_name);
+        SDL_free(product_name);
     }
 
-    char devName[MAX_PATH + 1];
-    devName[MAX_PATH] = '\0';
-    UINT devName_cap = MAX_PATH;
-    UINT devName_len = GetRawInputDeviceInfoA(hDevice, RIDI_DEVICENAME, devName, &devName_cap);
-    if (0 != ~devName_len) {
-        devName[devName_len] = '\0';
-    }
-    
-    if (desc_len + 3 < len && WIN_LoadHIDDLL()) { // reference counted
-        // important: for devices with exclusive access mode as per
-        // https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/top-level-collections-opened-by-windows-for-system-use
-        // they can only be opened with a desired access of none instead of generic read.
-        HANDLE hFile = CreateFileA(devName, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
-        if (hFile != INVALID_HANDLE_VALUE) {
-            HIDD_ATTRIBUTES attr;
-            WCHAR vend[128], prod[128];
-            attr.VendorID = 0;
-            attr.ProductID = 0;
-            attr.Size = sizeof(attr);
-            vend[0] = 0;
-            prod[0] = 0;
-            SDL_HidD_GetAttributes(hFile, &attr);
-            SDL_HidD_GetManufacturerString(hFile, vend, sizeof(vend));
-            SDL_HidD_GetProductString(hFile, prod, sizeof(prod));
-            CloseHandle(hFile);
-
-            size_t vend_len, prod_len;
-            for (vend_len = 0; vend_len < 128; vend_len++) {
-                if (vend[vend_len] == 0) {
-                    break;
-                }
-            }
-            for (prod_len = 0; prod_len < 128; prod_len++) {
-                if (prod[prod_len] == 0) {
+    if (!name) {
+        SP_DEVINFO_DATA data;
+        SDL_zero(data);
+        data.cbSize = sizeof(data);
+        for (DWORD i = 0;; ++i) {
+            if (!SetupDiEnumDeviceInfo(devinfo, i, &data)) {
+                if (GetLastError() == ERROR_NO_MORE_ITEMS) {
                     break;
+                } else {
+                    continue;
                 }
             }
-            if (vend_len > 0 || prod_len > 0) {
-                name[desc_len] = ' ';
-                name[desc_len+1] = '(';
-                size_t start = desc_len + 2;
-                size_t n = start;
-                for (size_t i = 0; i < vend_len; i++) {
-                    n = start + i;
-                    if (n < len) {
-                        name[n] = (char)(vend[i] & 0x7f); // TODO: do proper WCHAR conversion
-                    }
-                }
-                if (vend_len > 0) {
-                    n += 1;
-                    if (n < len) {
-                        name[n] = ' ';
-                    }
-                    n += 1;
-                }
-                size_t m = n;
-                for (size_t i = 0; i < prod_len; i++) {
-                    m = n + i;
-                    if (m < len) {
-                        name[m] = (char)(prod[i] & 0x7f); // TODO: do proper WCHAR conversion
-                    }
-                }
-                {
-                    m += 1;
-                    if (m < len) {
-                        name[m] = ')';
-                    }
-                    m += 1;
-                    if (m < len) {
-                        name[m] = '\0';
-                    }
-                }              
-            } else if (attr.VendorID && attr.ProductID) {
-                char buf[17];
-                int written = SDL_snprintf(buf, sizeof(buf), " (0x%.4x/0x%.4x)", attr.VendorID, attr.ProductID);
-                buf[16] = '\0'; // just to be safe
-                if (written > 0 && desc_len > 0) {
-                    for (size_t i = 0; i < sizeof(buf); i++) {
-                        size_t j = desc_len + i;
-                        if (j < len) {
-                            name[j] = buf[j];
-                        }
+
+            char DeviceInstanceId[64];
+            if (!SetupDiGetDeviceInstanceIdA(devinfo, &data, DeviceInstanceId, sizeof(DeviceInstanceId), NULL))
+                continue;
+
+            if (SDL_strcasecmp(instance, DeviceInstanceId) == 0) {
+                DWORD size = 0;
+                if (SetupDiGetDeviceRegistryPropertyW(devinfo, &data, SPDRP_DEVICEDESC, NULL, (PBYTE)prod, sizeof(prod), &size)) {
+                    // Make sure the device description is null terminated
+                    size /= sizeof(*prod);
+                    if (size >= SDL_arraysize(prod)) {
+                        // Truncated description...
+                        size = (SDL_arraysize(prod) - 1);
                     }
+                    prod[size] = 0;
+
+                    name = WIN_StringToUTF8W(prod);
                 }
+                break;
             }
         }
-        WIN_UnloadHIDDLL();
     }
-
-    name[len-1] = '\0'; // just to be safe
+    return name;
 }
 
 void WIN_CheckKeyboardAndMouseHotplug(SDL_VideoDevice *_this, bool initial_check)
@@ -1030,6 +985,7 @@ void WIN_CheckKeyboardAndMouseHotplug(SDL_VideoDevice *_this, bool initial_check
     old_keyboards = SDL_GetKeyboards(&old_keyboard_count);
     old_mice = SDL_GetMice(&old_mouse_count);
 
+    bool hid_loaded = WIN_LoadHIDDLL();
     for (UINT i = 0; i < raw_device_count; i++) {
         RID_DEVICE_INFO rdi;
         char devName[MAX_PATH] = { 0 };
@@ -1037,7 +993,7 @@ void WIN_CheckKeyboardAndMouseHotplug(SDL_VideoDevice *_this, bool initial_check
         UINT nameSize = SDL_arraysize(devName);
         int vendor = 0, product = 0;
         DWORD dwType = raw_devices[i].dwType;
-        char *instance, *ptr, name[256];
+        char *instance, *ptr, *name;
 
         if (dwType != RIM_TYPEKEYBOARD && dwType != RIM_TYPEMOUSE) {
             continue;
@@ -1075,8 +1031,9 @@ void WIN_CheckKeyboardAndMouseHotplug(SDL_VideoDevice *_this, bool initial_check
                 SDL_KeyboardID keyboardID = (Uint32)(uintptr_t)raw_devices[i].hDevice;
                 AddDeviceID(keyboardID, &new_keyboards, &new_keyboard_count);
                 if (!HasDeviceID(keyboardID, old_keyboards, old_keyboard_count)) {
-                    GetDeviceName(raw_devices[i].hDevice, devinfo, instance, name, sizeof(name));
+                    name = GetDeviceName(raw_devices[i].hDevice, devinfo, instance, hid_loaded);
                     SDL_AddKeyboard(keyboardID, name, send_event);
+                    SDL_free(name);
                 }
             }
             break;
@@ -1085,8 +1042,9 @@ void WIN_CheckKeyboardAndMouseHotplug(SDL_VideoDevice *_this, bool initial_check
                 SDL_MouseID mouseID = (Uint32)(uintptr_t)raw_devices[i].hDevice;
                 AddDeviceID(mouseID, &new_mice, &new_mouse_count);
                 if (!HasDeviceID(mouseID, old_mice, old_mouse_count)) {
-                    GetDeviceName(raw_devices[i].hDevice, devinfo, instance, name, sizeof(name));
+                    name = GetDeviceName(raw_devices[i].hDevice, devinfo, instance, hid_loaded);
                     SDL_AddMouse(mouseID, name, send_event);
+                    SDL_free(name);
                 }
             }
             break;
@@ -1094,6 +1052,9 @@ void WIN_CheckKeyboardAndMouseHotplug(SDL_VideoDevice *_this, bool initial_check
             break;
         }
     }
+    if (hid_loaded) {
+        WIN_UnloadHIDDLL();
+    }
 
     for (int i = old_keyboard_count; i--;) {
         if (!HasDeviceID(old_keyboards[i], new_keyboards, new_keyboard_count)) {