From 3347841c719ff29bfd719a732141f6b9a5410879 Mon Sep 17 00:00:00 2001
From: Simon McVittie <smcv@collabora.com>
Date: Wed, 28 Oct 2020 18:55:01 +0000
Subject: [PATCH] input-device: Add interface flags

The name is to leave space for "type flags" (joystick, etc.) later.

Signed-off-by: Simon McVittie <smcv@collabora.com>
---
 bin/input-monitor.c                         |  41 ++++++++
 steam-runtime-tools/direct-input-device.c   |  76 +++++++++++++++
 steam-runtime-tools/input-device-internal.h |  12 +++
 steam-runtime-tools/input-device.c          | 102 ++++++++++++++++++++
 steam-runtime-tools/input-device.h          |  24 +++++
 steam-runtime-tools/udev-input-device.c     |  36 +++++++
 tests/input-device.c                        |  10 ++
 tests/mock-input-device.c                   |  12 +++
 tests/mock-input-device.h                   |   2 +
 9 files changed, 315 insertions(+)

diff --git a/bin/input-monitor.c b/bin/input-monitor.c
index 49f092b68..036b1b13a 100644
--- a/bin/input-monitor.c
+++ b/bin/input-monitor.c
@@ -45,6 +45,41 @@ static gboolean opt_one_line = FALSE;
 static gboolean opt_seq = FALSE;
 static gboolean opt_verbose = FALSE;
 
+static void
+jsonify_flags (JsonBuilder *builder,
+               GType flags_type,
+               unsigned int values)
+{
+  GFlagsClass *class;
+  GFlagsValue *flags_value;
+
+  g_return_if_fail (G_TYPE_IS_FLAGS (flags_type));
+
+  class = g_type_class_ref (flags_type);
+
+  while (values != 0)
+    {
+      flags_value = g_flags_get_first_value (class, values);
+
+      if (flags_value == NULL)
+        break;
+
+      json_builder_add_string_value (builder, flags_value->value_nick);
+      values &= ~flags_value->value;
+    }
+
+  if (values)
+    {
+      gchar *rest = g_strdup_printf ("0x%x", values);
+
+      json_builder_add_string_value (builder, rest);
+
+      g_free (rest);
+    }
+
+  g_type_class_unref (class);
+}
+
 static void
 print_json (JsonBuilder *builder)
 {
@@ -124,6 +159,12 @@ added (SrtInputDeviceMonitor *monitor,
           g_auto(GStrv) udev_properties = NULL;
           const char *sys_path;
 
+          json_builder_set_member_name (builder, "interface_flags");
+          json_builder_begin_array (builder);
+          jsonify_flags (builder, SRT_TYPE_INPUT_DEVICE_INTERFACE_FLAGS,
+                         srt_input_device_get_interface_flags (dev));
+          json_builder_end_array (builder);
+
           json_builder_set_member_name (builder, "dev_node");
           json_builder_add_string_value (builder,
                                          srt_input_device_get_dev_node (dev));
diff --git a/steam-runtime-tools/direct-input-device.c b/steam-runtime-tools/direct-input-device.c
index 7cd31f930..b182082c7 100644
--- a/steam-runtime-tools/direct-input-device.c
+++ b/steam-runtime-tools/direct-input-device.c
@@ -170,6 +170,23 @@ struct _SrtDirectInputDevice
   {
     gchar *sys_path;
   } usb_device_ancestor;
+
+  struct
+  {
+    guint32 bus_type;
+    guint32 product_id;
+    guint32 vendor_id;
+    guint32 version;
+  } evdev;
+
+  struct
+  {
+    guint32 bus_type;
+    guint32 product_id;
+    guint32 vendor_id;
+  } hid;
+
+  SrtInputDeviceInterfaceFlags iface_flags;
 };
 
 struct _SrtDirectInputDeviceClass
@@ -241,6 +258,14 @@ _srt_direct_input_device_class_init (SrtDirectInputDeviceClass *cls)
   object_class->finalize = srt_direct_input_device_finalize;
 }
 
+static SrtInputDeviceInterfaceFlags
+srt_direct_input_device_get_interface_flags (SrtInputDevice *device)
+{
+  SrtDirectInputDevice *self = SRT_DIRECT_INPUT_DEVICE (device);
+
+  return self->iface_flags;
+}
+
 static const char *
 srt_direct_input_device_get_dev_node (SrtInputDevice *device)
 {
@@ -294,6 +319,7 @@ srt_direct_input_device_iface_init (SrtInputDeviceInterface *iface)
 {
 #define IMPLEMENT(x) iface->x = srt_direct_input_device_ ## x
 
+  IMPLEMENT (get_interface_flags);
   IMPLEMENT (get_dev_node);
   IMPLEMENT (get_sys_path);
   IMPLEMENT (get_subsystem);
@@ -441,6 +467,9 @@ add_device (SrtDirectInputDeviceMonitor *self,
       return;
     }
 
+  if (subsystem == quark_hidraw)
+    device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID;
+
   g_debug ("Trying to open %s", devnode);
 
   fd = open (devnode, O_RDONLY | O_NONBLOCK | ALWAYS_OPEN_FLAGS);
@@ -449,6 +478,36 @@ add_device (SrtDirectInputDeviceMonitor *self,
     {
       /* The permissions are already OK for use */
       g_debug ("Opened %s", devnode);
+      device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE;
+
+      if (_srt_get_identity_from_raw_hid (fd,
+                                          &device->hid.bus_type,
+                                          &device->hid.vendor_id,
+                                          &device->hid.product_id))
+        {
+          g_debug ("%s is raw HID: bus type 0x%04x, vendor 0x%04x, product 0x%04x",
+                   devnode,
+                   device->hid.bus_type,
+                   device->hid.vendor_id,
+                   device->hid.product_id);
+          device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID;
+        }
+
+      if (_srt_get_identity_from_evdev (fd,
+                                        &device->evdev.bus_type,
+                                        &device->evdev.vendor_id,
+                                        &device->evdev.product_id,
+                                        &device->evdev.version))
+        {
+          g_debug ("%s is evdev: bus type 0x%04x, vendor 0x%04x, product 0x%04x, version 0x%04x",
+                   devnode,
+                   device->evdev.bus_type,
+                   device->evdev.vendor_id,
+                   device->evdev.product_id,
+                   device->evdev.version);
+          device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT;
+        }
+
       close (fd);
     }
   else
@@ -459,6 +518,23 @@ add_device (SrtDirectInputDeviceMonitor *self,
       return;
     }
 
+  fd = open (devnode, O_RDWR | O_NONBLOCK | ALWAYS_OPEN_FLAGS);
+
+  if (fd >= 0)
+    {
+      g_debug ("Opened %s rw", devnode);
+      device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE;
+      close (fd);
+    }
+
+  if ((device->iface_flags & (SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT
+                              | SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID)) == 0)
+    {
+      g_debug ("%s is neither evdev nor raw HID, ignoring", devnode);
+      g_object_unref (device);
+      return;
+    }
+
   device->hid_ancestor.sys_path = get_ancestor_with_subsystem_devtype (device->sys_path,
                                                               "hid", NULL,
                                                               NULL);
diff --git a/steam-runtime-tools/input-device-internal.h b/steam-runtime-tools/input-device-internal.h
index 4b5589ff1..820f34fa2 100644
--- a/steam-runtime-tools/input-device-internal.h
+++ b/steam-runtime-tools/input-device-internal.h
@@ -32,6 +32,8 @@ struct _SrtInputDeviceInterface
 {
   GTypeInterface parent;
 
+  SrtInputDeviceInterfaceFlags (*get_interface_flags) (SrtInputDevice *device);
+
   const char * (*get_dev_node) (SrtInputDevice *device);
   const char * (*get_sys_path) (SrtInputDevice *device);
   const char * (*get_subsystem) (SrtInputDevice *device);
@@ -73,6 +75,16 @@ void _srt_input_device_monitor_emit_removed (SrtInputDeviceMonitor *monitor,
                                              SrtInputDevice *device);
 void _srt_input_device_monitor_emit_all_for_now (SrtInputDeviceMonitor *monitor);
 
+
+gboolean _srt_get_identity_from_evdev (int fd,
+                                       guint32 *bus_type,
+                                       guint32 *vendor,
+                                       guint32 *product,
+                                       guint32 *version);
+gboolean _srt_get_identity_from_raw_hid (int fd,
+                                         guint32 *bus_type,
+                                         guint32 *vendor,
+                                         guint32 *product);
 gchar *_srt_input_device_uevent_field (const char *text,
                                        const char *key);
 gboolean _srt_input_device_uevent_field_equals (const char *text,
diff --git a/steam-runtime-tools/input-device.c b/steam-runtime-tools/input-device.c
index 3054cc2ff..753847648 100644
--- a/steam-runtime-tools/input-device.c
+++ b/steam-runtime-tools/input-device.c
@@ -26,6 +26,9 @@
 #include "steam-runtime-tools/input-device.h"
 #include "steam-runtime-tools/input-device-internal.h"
 
+#include <linux/hidraw.h>
+#include <linux/input.h>
+
 #include <libglnx.h>
 
 #include "steam-runtime-tools/enums.h"
@@ -60,6 +63,30 @@ srt_input_device_default_get_null_strv (SrtInputDevice *device)
   return NULL;
 }
 
+/**
+ * srt_input_device_get_interface_flags:
+ * @device: An object implementing #SrtInputDeviceInterface
+ *
+ * Return flags describing how the input device can be used.
+ *
+ * Returns: Flags describing the input device.
+ */
+SrtInputDeviceInterfaceFlags
+srt_input_device_get_interface_flags (SrtInputDevice *device)
+{
+  SrtInputDeviceInterface *iface = SRT_INPUT_DEVICE_GET_INTERFACE (device);
+
+  g_return_val_if_fail (iface != NULL, SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE);
+
+  return iface->get_interface_flags (device);
+}
+
+static SrtInputDeviceInterfaceFlags
+srt_input_device_default_get_interface_flags (SrtInputDevice *device)
+{
+  return SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE;
+}
+
 /**
  * srt_input_device_get_dev_node:
  * @device: An object implementing #SrtInputDeviceInterface
@@ -373,6 +400,7 @@ srt_input_device_default_init (SrtInputDeviceInterface *iface)
 #define IMPLEMENT2(x, y) iface->x = srt_input_device_default_ ## y
 #define IMPLEMENT(x) IMPLEMENT2 (x, x)
 
+  IMPLEMENT (get_interface_flags);
   IMPLEMENT2 (get_dev_node, get_null_string);
   IMPLEMENT2 (get_sys_path, get_null_string);
   IMPLEMENT2 (get_subsystem, get_null_string);
@@ -759,6 +787,80 @@ _srt_input_device_monitor_emit_all_for_now (SrtInputDeviceMonitor *monitor)
   g_signal_emit (monitor, monitor_signal_all_for_now, 0);
 }
 
+gboolean
+_srt_get_identity_from_evdev (int fd,
+                              guint32 *bus_type,
+                              guint32 *vendor,
+                              guint32 *product,
+                              guint32 *version)
+{
+  struct input_id iid = {};
+
+  if (bus_type != NULL)
+    *bus_type = 0;
+
+  if (vendor != NULL)
+    *vendor = 0;
+
+  if (product != NULL)
+    *product = 0;
+
+  if (ioctl (fd, EVIOCGID, &iid) < 0)
+    {
+      g_debug ("EVIOCGID: %s", g_strerror (errno));
+      return FALSE;
+    }
+
+  if (bus_type != NULL)
+    *bus_type = iid.bustype;
+
+  if (vendor != NULL)
+    *vendor = iid.vendor;
+
+  if (product != NULL)
+    *product = iid.product;
+
+  if (version != NULL)
+    *version = iid.version;
+
+  return TRUE;
+}
+
+gboolean
+_srt_get_identity_from_raw_hid (int fd,
+                                guint32 *bus_type,
+                                guint32 *vendor,
+                                guint32 *product)
+{
+  struct hidraw_devinfo devinfo = {};
+
+  if (bus_type != NULL)
+    *bus_type = 0;
+
+  if (vendor != NULL)
+    *vendor = 0;
+
+  if (product != NULL)
+    *product = 0;
+
+  if (ioctl (fd, HIDIOCGRAWINFO, &devinfo) < 0)
+    {
+      g_debug ("HIDIOCGRAWINFO: %s", g_strerror (errno));
+      return FALSE;
+    }
+
+  if (bus_type != NULL)
+    *bus_type = devinfo.bustype;
+
+  if (vendor != NULL)
+    *vendor = devinfo.vendor;
+
+  if (product != NULL)
+    *product = devinfo.product;
+
+  return TRUE;
+}
+
 /*
  * Returns: The field @key from @text, or %NULL if not found.
  */
diff --git a/steam-runtime-tools/input-device.h b/steam-runtime-tools/input-device.h
index 037e4c992..0f1b13493 100644
--- a/steam-runtime-tools/input-device.h
+++ b/steam-runtime-tools/input-device.h
@@ -33,6 +33,28 @@
 
 #include "steam-runtime-tools/macros.h"
 
+/**
+ * SrtInputDeviceInterfaceFlags:
+ * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT: evdev event device nodes,
+ *  typically /dev/input/event*
+ * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID: Raw USB or Bluetooth HID
+ *  device nodes, typically /dev/hidraw*
+ * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE: Only report device nodes that appear to be
+ *  openable in read-only mode
+ * @SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE: Only report device nodes that appear to be
+ *  openable in read/write mode
+ *
+ * Flags describing the interface offered by an input device.
+ */
+typedef enum
+{
+  SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT = (1 << 0),
+  SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID = (1 << 1),
+  SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE = (1 << 2),
+  SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE = (1 << 3),
+  SRT_INPUT_DEVICE_INTERFACE_FLAGS_NONE = 0
+} SrtInputDeviceInterfaceFlags;
+
 #define SRT_TYPE_INPUT_DEVICE (srt_input_device_get_type ())
 #define SRT_INPUT_DEVICE(x) (G_TYPE_CHECK_INSTANCE_CAST ((x), SRT_TYPE_INPUT_DEVICE, SrtInputDevice))
 #define SRT_IS_INPUT_DEVICE(x) (G_TYPE_CHECK_INSTANCE_TYPE ((x), SRT_TYPE_INPUT_DEVICE))
@@ -44,6 +66,8 @@ typedef struct _SrtInputDeviceInterface SrtInputDeviceInterface;
 _SRT_PUBLIC
 GType srt_input_device_get_type (void);
 
+_SRT_PUBLIC
+SrtInputDeviceInterfaceFlags srt_input_device_get_interface_flags (SrtInputDevice *device);
 _SRT_PUBLIC
 const char *srt_input_device_get_dev_node (SrtInputDevice *device);
 _SRT_PUBLIC
diff --git a/steam-runtime-tools/udev-input-device.c b/steam-runtime-tools/udev-input-device.c
index ca3594cb8..04bb48244 100644
--- a/steam-runtime-tools/udev-input-device.c
+++ b/steam-runtime-tools/udev-input-device.c
@@ -36,6 +36,8 @@
 #include "steam-runtime-tools/input-device-internal.h"
 #include "steam-runtime-tools/utils-internal.h"
 
+#define ALWAYS_OPEN_FLAGS (O_CLOEXEC | O_NOCTTY)
+
 static void srt_udev_input_device_iface_init (SrtInputDeviceInterface *iface);
 static void srt_udev_input_device_monitor_iface_init (SrtInputDeviceMonitorInterface *iface);
 static void srt_udev_input_device_monitor_initable_iface_init (GInitableIface *iface);
@@ -123,6 +125,8 @@ struct _SrtUdevInputDevice
   {
     struct udev_device *dev;                  /* borrowed from child dev */
   } usb_device_ancestor;
+
+  SrtInputDeviceInterfaceFlags iface_flags;
 };
 
 struct _SrtUdevInputDeviceClass
@@ -193,6 +197,14 @@ _srt_udev_input_device_class_init (SrtUdevInputDeviceClass *cls)
   object_class->finalize = srt_udev_input_device_finalize;
 }
 
+static SrtInputDeviceInterfaceFlags
+srt_udev_input_device_get_interface_flags (SrtInputDevice *device)
+{
+  SrtUdevInputDevice *self = SRT_UDEV_INPUT_DEVICE (device);
+
+  return self->iface_flags;
+}
+
 static const char *
 srt_udev_input_device_get_dev_node (SrtInputDevice *device)
 {
@@ -320,6 +332,7 @@ srt_udev_input_device_iface_init (SrtInputDeviceInterface *iface)
 {
 #define IMPLEMENT(x) iface->x = srt_udev_input_device_ ## x
 
+  IMPLEMENT (get_interface_flags);
   IMPLEMENT (get_dev_node);
   IMPLEMENT (get_sys_path);
   IMPLEMENT (get_subsystem);
@@ -535,6 +548,7 @@ add_device (SrtUdevInputDeviceMonitor *self,
   gboolean is_evdev = FALSE;
   gboolean is_hidraw = FALSE;
   const char *slash;
+  int fd;
 
   syspath = symbols.udev_device_get_syspath (dev);
   devnode = symbols.udev_device_get_devnode (dev);
@@ -587,6 +601,28 @@ add_device (SrtUdevInputDeviceMonitor *self,
   device = g_object_new (SRT_TYPE_UDEV_INPUT_DEVICE, NULL);
   device->dev = symbols.udev_device_ref (dev);
 
+  if (is_evdev)
+    device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT;
+
+  if (is_hidraw)
+    device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_RAW_HID;
+
+  fd = open (devnode, O_RDONLY | O_NONBLOCK | ALWAYS_OPEN_FLAGS);
+
+  if (fd >= 0)
+    {
+      device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE;
+      close (fd);
+    }
+
+  fd = open (devnode, O_RDWR | O_NONBLOCK | ALWAYS_OPEN_FLAGS);
+
+  if (fd >= 0)
+    {
+      device->iface_flags |= SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE;
+      close (fd);
+    }
+
   device->hid_ancestor.dev = symbols.udev_device_get_parent_with_subsystem_devtype (device->dev,
                                                                                     "hid",
                                                                                     NULL);
diff --git a/tests/input-device.c b/tests/input-device.c
index af8493858..c1389154e 100644
--- a/tests/input-device.c
+++ b/tests/input-device.c
@@ -108,6 +108,8 @@ test_input_device_usb (Fixture *f,
   g_autofree gchar *input_uevent = NULL;
   g_autofree gchar *usb_uevent = NULL;
 
+  mock_device->iface_flags = (SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT
+                              | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE);
   mock_device->dev_node = g_strdup ("/dev/input/event0");
   mock_device->sys_path = g_strdup ("/sys/devices/mock/usb/hid/input/input0/event0");
   mock_device->subsystem = g_strdup ("input");
@@ -128,6 +130,9 @@ test_input_device_usb (Fixture *f,
   /* TODO: Fill in somewhat realistic details for a USB-attached
    * Steam Controller */
 
+  g_assert_cmpuint (srt_input_device_get_interface_flags (device), ==,
+                    SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT
+                    | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE);
   g_assert_cmpstr (srt_input_device_get_dev_node (device), ==,
                    "/dev/input/event0");
   g_assert_cmpstr (srt_input_device_get_sys_path (device), ==,
@@ -196,6 +201,11 @@ device_added_cb (SrtInputDeviceMonitor *monitor,
       g_autofree gchar *usb_uevent = NULL;
       g_auto(GStrv) udev_properties = NULL;
 
+      g_assert_cmpuint (srt_input_device_get_interface_flags (device), ==,
+                        SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT
+                        | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE
+                        | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE);
+
       uevent = srt_input_device_dup_uevent (device);
       g_assert_cmpstr (uevent, ==, "ONE=1\nTWO=2\n");
 
diff --git a/tests/mock-input-device.c b/tests/mock-input-device.c
index 3a2bda45d..31e08abf1 100644
--- a/tests/mock-input-device.c
+++ b/tests/mock-input-device.c
@@ -86,6 +86,14 @@ mock_input_device_class_init (MockInputDeviceClass *cls)
   object_class->finalize = mock_input_device_finalize;
 }
 
+static SrtInputDeviceInterfaceFlags
+mock_input_device_get_interface_flags (SrtInputDevice *device)
+{
+  MockInputDevice *self = MOCK_INPUT_DEVICE (device);
+
+  return self->iface_flags;
+}
+
 static const char *
 mock_input_device_get_dev_node (SrtInputDevice *device)
 {
@@ -179,6 +187,7 @@ mock_input_device_iface_init (SrtInputDeviceInterface *iface)
 {
 #define IMPLEMENT(x) iface->x = mock_input_device_ ## x
 
+  IMPLEMENT (get_interface_flags);
   IMPLEMENT (get_dev_node);
   IMPLEMENT (get_sys_path);
   IMPLEMENT (get_subsystem);
@@ -341,6 +350,9 @@ add_steam_controller (MockInputDeviceMonitor *self,
   g_autoptr(MockInputDevice) device = mock_input_device_new ();
   g_autoptr(GPtrArray) arr = g_ptr_array_new_full (1, g_free);
 
+  device->iface_flags = (SRT_INPUT_DEVICE_INTERFACE_FLAGS_EVENT
+                         | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READABLE
+                         | SRT_INPUT_DEVICE_INTERFACE_FLAGS_READ_WRITE);
   device->dev_node = g_strdup_printf ("/dev/input/event%s", tail);
   device->sys_path = g_strdup_printf ("/sys/devices/mock/usb/hid/input/input0/event%s",
                                       tail);
diff --git a/tests/mock-input-device.h b/tests/mock-input-device.h
index b3255af37..11c3e32dd 100644
--- a/tests/mock-input-device.h
+++ b/tests/mock-input-device.h
@@ -72,6 +72,8 @@ struct _MockInputDevice
     gchar *sys_path;
     gchar *uevent;
   } usb_device_ancestor;
+
+  SrtInputDeviceInterfaceFlags iface_flags;
 };
 
 struct _MockInputDeviceClass
-- 
GitLab