SDL: hidapi/mac: Only enumerate IOHIDDevices that are likely to be joysticks

From feb7178e6667cad35aad897fcd19fda1b4fc32fc Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 25 May 2023 08:47:51 -0700
Subject: [PATCH] hidapi/mac: Only enumerate IOHIDDevices that are likely to be
 joysticks

Touching HID devices with keyboard usages will trigger a keyboard capture
permission prompt on macOS 11+. See #4887

Like the IOKit joystick backend, we accept HID devices that have joystick,
gamepad, or multi-axis controller usages. We also allow the Valve VID for
the Steam Controller, just like the Windows HIDAPI implementation does.

Signed-off-by: Cameron Gutman <aicommander@gmail.com>
Signed-off-by: Sam Lantinga <slouken@libsdl.org>
---
 src/hidapi/mac/hid.c | 86 ++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 80 insertions(+), 6 deletions(-)

diff --git a/src/hidapi/mac/hid.c b/src/hidapi/mac/hid.c
index 280b3a9cbd5c..4801579b874b 100644
--- a/src/hidapi/mac/hid.c
+++ b/src/hidapi/mac/hid.c
@@ -38,6 +38,11 @@
 
 #include "hidapi_darwin.h"
 
+/* Only matching controllers is a more safe option and prevents input monitoring permissions dialogs */
+#ifndef HIDAPI_ONLY_ENUMERATE_CONTROLLERS
+#define HIDAPI_ONLY_ENUMERATE_CONTROLLERS 0
+#endif
+
 /* As defined in AppKit.h, but we don't need the entire AppKit for a single constant. */
 extern const double NSAppKitVersionNumber;
 
@@ -700,6 +705,51 @@ static struct hid_device_info *create_device_info(IOHIDDeviceRef device)
 	return root;
 }
 
+static CFDictionaryRef create_usage_match(const UInt32 page, const UInt32 usage, int *okay)
+{
+    CFDictionaryRef retval = NULL;
+    CFNumberRef pageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page);
+    CFNumberRef usageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);
+    const void *keys[2] = { (void *) CFSTR(kIOHIDDeviceUsagePageKey), (void *) CFSTR(kIOHIDDeviceUsageKey) };
+    const void *vals[2] = { (void *) pageNumRef, (void *) usageNumRef };
+
+    if (pageNumRef && usageNumRef) {
+        retval = CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+    }
+
+    if (pageNumRef) {
+        CFRelease(pageNumRef);
+    }
+    if (usageNumRef) {
+        CFRelease(usageNumRef);
+    }
+
+    if (!retval) {
+        *okay = 0;
+    }
+
+    return retval;
+}
+
+static CFDictionaryRef create_vendor_match(const UInt32 vendor, int *okay)
+{
+    CFDictionaryRef retval = NULL;
+    CFNumberRef vidNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &vendor);
+    const void *keys[1] = { (void *) CFSTR(kIOHIDVendorIDKey) };
+    const void *vals[1] = { (void *) vidNumRef };
+
+    if (vidNumRef) {
+        retval = CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+		CFRelease(vidNumRef);
+    }
+
+    if (!retval) {
+        *okay = 0;
+    }
+
+    return retval;
+}
+
 struct hid_device_info  HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id)
 {
 	struct hid_device_info *root = NULL; /* return object */
@@ -717,9 +767,8 @@ struct hid_device_info  HID_API_EXPORT *hid_enumerate(unsigned short vendor_id,
 	process_pending_events();
 
 	/* Get a list of the Devices */
-	CFMutableDictionaryRef matching = NULL;
 	if (vendor_id != 0 || product_id != 0) {
-		matching = CFDictionaryCreateMutable(kCFAllocatorDefault, kIOHIDOptionsTypeNone, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+		CFMutableDictionaryRef matching = CFDictionaryCreateMutable(kCFAllocatorDefault, kIOHIDOptionsTypeNone, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
 
 		if (matching && vendor_id != 0) {
 			CFNumberRef v = CFNumberCreate(kCFAllocatorDefault, kCFNumberShortType, &vendor_id);
@@ -732,10 +781,35 @@ struct hid_device_info  HID_API_EXPORT *hid_enumerate(unsigned short vendor_id,
 			CFDictionarySetValue(matching, CFSTR(kIOHIDProductIDKey), p);
 			CFRelease(p);
 		}
-	}
-	IOHIDManagerSetDeviceMatching(hid_mgr, matching);
-	if (matching != NULL) {
-		CFRelease(matching);
+
+		IOHIDManagerSetDeviceMatching(hid_mgr, matching);
+		if (matching != NULL) {
+			CFRelease(matching);
+		}
+	} else if (HIDAPI_ONLY_ENUMERATE_CONTROLLERS) {
+		const UInt32 VALVE_USB_VID = 0x28DE;
+		int okay = 1;
+		const void *vals[] = {
+			(void *) create_usage_match(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, &okay),
+			(void *) create_usage_match(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad, &okay),
+			(void *) create_usage_match(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController, &okay),
+			(void *) create_vendor_match(VALVE_USB_VID, &okay),
+		};
+		CFIndex numElements = sizeof(vals) / sizeof(vals[0]);
+		CFArrayRef matching = okay ? CFArrayCreate(kCFAllocatorDefault, vals, numElements, &kCFTypeArrayCallBacks) : NULL;
+
+		for (i = 0; i < numElements; i++) {
+			if (vals[i]) {
+				CFRelease((CFTypeRef) vals[i]);
+			}
+		}
+
+		IOHIDManagerSetDeviceMatchingMultiple(hid_mgr, matching);
+		if (matching != NULL) {
+			CFRelease(matching);
+		}
+	} else {
+		IOHIDManagerSetDeviceMatching(hid_mgr, NULL);
 	}
 
 	CFSetRef device_set = IOHIDManagerCopyDevices(hid_mgr);