From b0d9d0076209fa3de1458cafb5eda494dfc226d1 Mon Sep 17 00:00:00 2001
From: Matthew Mirvish <matthew@mm12.xyz>
Date: Thu, 8 Apr 2021 16:46:01 +0200
Subject: [PATCH] Add support for udev based device discovery

This is primarily useful for SPI devices. These devices sometimes needs
a combination of an SPI and HID device, so discovery is a bit more
complicated.
---
 doc/libfprint-2-sections.txt  |   2 +
 libfprint/fp-context.c        | 109 ++++++++++++++++++++++++++++++++++
 libfprint/fp-device-private.h |  21 ++++---
 libfprint/fp-device.c         |  45 ++++++++++++++
 libfprint/fp-device.h         |   2 +
 libfprint/fpi-device.c        |  33 ++++++++++
 libfprint/fpi-device.h        |  24 ++++++++
 libfprint/meson.build         |   1 +
 meson.build                   |  14 ++++-
 9 files changed, 242 insertions(+), 9 deletions(-)

diff --git a/doc/libfprint-2-sections.txt b/doc/libfprint-2-sections.txt
index 61dd985..37fddf3 100644
--- a/doc/libfprint-2-sections.txt
+++ b/doc/libfprint-2-sections.txt
@@ -132,7 +132,9 @@ FpDeviceClass
 FpTimeoutFunc
 FpiDeviceAction
 FpIdEntry
+FpiDeviceUdevSubtypeFlags
 fpi_device_get_usb_device
+fpi_device_get_udev_data
 fpi_device_get_virtual_env
 fpi_device_get_current_action
 fpi_device_retry_new
diff --git a/libfprint/fp-context.c b/libfprint/fp-context.c
index 584b0b7..ea99e06 100644
--- a/libfprint/fp-context.c
+++ b/libfprint/fp-context.c
@@ -24,6 +24,18 @@
 #include "fpi-device.h"
 #include <gusb.h>
 
+#include <config.h>
+
+#ifdef HAVE_UDEV
+#include <sys/ioctl.h>
+#include <sys/unistd.h>
+#include <sys/types.h>
+#include <sys/fcntl.h>
+#include <sys/stat.h>
+#include <linux/hidraw.h>
+#include <gudev/gudev.h>
+#endif
+
 /**
  * SECTION: fp-context
  * @title: FpContext
@@ -434,6 +446,103 @@ fp_context_enumerate (FpContext *context)
         }
     }
 
+
+#ifdef HAVE_UDEV
+  {
+    g_autoptr(GUdevClient) udev_client = g_udev_client_new (NULL);
+
+    /* This uses a very simple algorithm to allocate devices to drivers and assumes that no two drivers will want the same device. Future improvements
+     * could add a usb_discover style udev_discover that returns a score, however for internal devices the potential overlap should be very low between
+     * separate drivers.
+     */
+
+    g_autoptr(GList) spidev_devices = g_udev_client_query_by_subsystem (udev_client, "spidev");
+    g_autoptr(GList) hidraw_devices = g_udev_client_query_by_subsystem (udev_client, "hidraw");
+
+    /* for each potential driver, try to match all requested resources. */
+    for (i = 0; i < priv->drivers->len; i++)
+      {
+        GType driver = g_array_index (priv->drivers, GType, i);
+        g_autoptr(FpDeviceClass) cls = g_type_class_ref (driver);
+        const FpIdEntry *entry;
+
+        if (cls->type != FP_DEVICE_TYPE_UDEV)
+          continue;
+
+        for (entry = cls->id_table; entry->udev_types; entry++)
+          {
+            GList *matched_spidev = NULL, *matched_hidraw = NULL;
+
+            if (entry->udev_types & FPI_DEVICE_UDEV_SUBTYPE_SPIDEV)
+              {
+                for (matched_spidev = spidev_devices; matched_spidev; matched_spidev = matched_spidev->next)
+                  {
+                    const gchar * sysfs = g_udev_device_get_sysfs_path (matched_spidev->data);
+                    if (!sysfs)
+                      continue;
+                    if (strstr (sysfs, entry->spi_acpi_id))
+                      break;
+                  }
+                /* If match was not found exit */
+                if (matched_spidev == NULL)
+                  continue;
+              }
+            if (entry->udev_types & FPI_DEVICE_UDEV_SUBTYPE_HIDRAW)
+              {
+                for (matched_hidraw = hidraw_devices; matched_hidraw; matched_hidraw = matched_hidraw->next)
+                  {
+                    const gchar * devnode = g_udev_device_get_device_file (matched_hidraw->data);
+                    int temp_hid = -1, res;
+                    struct hidraw_devinfo info;
+
+                    if (!devnode)
+                      continue;
+
+                    temp_hid = open (devnode, O_RDWR);
+                    if (temp_hid < 0)
+                      continue;
+
+                    res = ioctl (temp_hid, HIDIOCGRAWINFO, &info);
+                    close (temp_hid);
+                    if (res < 0)
+                      continue;
+                    if (info.vendor == entry->hid_id.vid && info.product == entry->hid_id.pid)
+                      break;
+                  }
+                /* If match was not found exit */
+                if (matched_hidraw == NULL)
+                  continue;
+              }
+            priv->pending_devices++;
+            g_async_initable_new_async (driver,
+                                        G_PRIORITY_LOW,
+                                        priv->cancellable,
+                                        async_device_init_done_cb,
+                                        context,
+                                        "fpi-driver-data", entry->driver_data,
+                                        "fpi-udev-data-spidev", (matched_spidev ? g_udev_device_get_device_file (matched_spidev->data) : NULL),
+                                        "fpi-udev-data-hidraw", (matched_hidraw ? g_udev_device_get_device_file (matched_hidraw->data) : NULL),
+                                        NULL);
+            /* remove entries from list to avoid conflicts */
+            if (matched_spidev)
+              {
+                g_object_unref (matched_spidev->data);
+                spidev_devices = g_list_delete_link (spidev_devices, matched_spidev);
+              }
+            if (matched_hidraw)
+              {
+                g_object_unref (matched_hidraw->data);
+                hidraw_devices = g_list_delete_link (hidraw_devices, matched_hidraw);
+              }
+          }
+      }
+
+    /* free all unused elemnts in both lists */
+    g_list_foreach (spidev_devices, (GFunc) g_object_unref, NULL);
+    g_list_foreach (hidraw_devices, (GFunc) g_object_unref, NULL);
+  }
+#endif
+
   while (priv->pending_devices)
     g_main_context_iteration (NULL, TRUE);
 }
diff --git a/libfprint/fp-device-private.h b/libfprint/fp-device-private.h
index b8d5291..47230cc 100644
--- a/libfprint/fp-device-private.h
+++ b/libfprint/fp-device-private.h
@@ -28,18 +28,23 @@ typedef struct
 
   GUsbDevice  *usb_device;
   const gchar *virtual_env;
+  struct
+  {
+    gchar *spidev_path;
+    gchar *hidraw_path;
+  } udev_data;
 
-  gboolean     is_removed;
-  gboolean     is_open;
+  gboolean   is_removed;
+  gboolean   is_open;
 
-  gchar       *device_id;
-  gchar       *device_name;
-  FpScanType   scan_type;
+  gchar     *device_id;
+  gchar     *device_name;
+  FpScanType scan_type;
 
-  guint64      driver_data;
+  guint64    driver_data;
 
-  gint         nr_enroll_stages;
-  GSList      *sources;
+  gint       nr_enroll_stages;
+  GSList    *sources;
 
   /* We always make sure that only one task is run at a time. */
   FpiDeviceAction     current_action;
diff --git a/libfprint/fp-device.c b/libfprint/fp-device.c
index 42b1322..251ed83 100644
--- a/libfprint/fp-device.c
+++ b/libfprint/fp-device.c
@@ -50,6 +50,8 @@ enum {
   PROP_FINGER_STATUS,
   PROP_FPI_ENVIRON,
   PROP_FPI_USB_DEVICE,
+  PROP_FPI_UDEV_DATA_SPIDEV,
+  PROP_FPI_UDEV_DATA_HIDRAW,
   PROP_FPI_DRIVER_DATA,
   N_PROPS
 };
@@ -169,6 +171,8 @@ fp_device_finalize (GObject *object)
 
   g_clear_object (&priv->usb_device);
   g_clear_pointer (&priv->virtual_env, g_free);
+  g_clear_pointer (&priv->udev_data.spidev_path, g_free);
+  g_clear_pointer (&priv->udev_data.hidraw_path, g_free);
 
   G_OBJECT_CLASS (fp_device_parent_class)->finalize (object);
 }
@@ -248,6 +252,20 @@ fp_device_set_property (GObject      *object,
         g_assert (g_value_get_object (value) == NULL);
       break;
 
+    case PROP_FPI_UDEV_DATA_SPIDEV:
+      if (cls->type == FP_DEVICE_TYPE_UDEV)
+        priv->udev_data.spidev_path = g_value_dup_string (value);
+      else
+        g_assert (g_value_get_string (value) == NULL);
+      break;
+
+    case PROP_FPI_UDEV_DATA_HIDRAW:
+      if (cls->type == FP_DEVICE_TYPE_UDEV)
+        priv->udev_data.hidraw_path = g_value_dup_string (value);
+      else
+        g_assert (g_value_get_string (value) == NULL);
+      break;
+
     case PROP_FPI_DRIVER_DATA:
       priv->driver_data = g_value_get_uint64 (value);
       break;
@@ -425,6 +443,32 @@ fp_device_class_init (FpDeviceClass *klass)
                          "Private: The USB device for the device",
                          G_USB_TYPE_DEVICE,
                          G_PARAM_STATIC_STRINGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
+  /**
+   * FpDevice::fpi-udev-data-spidev: (skip)
+   *
+   * This property is only for internal purposes.
+   *
+   * Stability: private
+   */
+  properties[PROP_FPI_UDEV_DATA_SPIDEV] =
+    g_param_spec_string ("fpi-udev-data-spidev",
+                         "Udev data: spidev path",
+                         "Private: The path to /dev/spidevN.M",
+                         NULL,
+                         G_PARAM_STATIC_STRINGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
+  /**
+   * FpDevice::fpi-udev-data-hidraw: (skip)
+   *
+   * This property is only for internal purposes.
+   *
+   * Stability: private
+   */
+  properties[PROP_FPI_UDEV_DATA_HIDRAW] =
+    g_param_spec_string ("fpi-udev-data-hidraw",
+                         "Udev data: hidraw path",
+                         "Private: The path to /dev/hidrawN",
+                         NULL,
+                         G_PARAM_STATIC_STRINGS | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
 
   /**
    * FpDevice::fpi-driver-data: (skip)
@@ -673,6 +717,7 @@ fp_device_open (FpDevice           *device,
       break;
 
     case FP_DEVICE_TYPE_VIRTUAL:
+    case FP_DEVICE_TYPE_UDEV:
       break;
 
     default:
diff --git a/libfprint/fp-device.h b/libfprint/fp-device.h
index c2ecd5e..806ad19 100644
--- a/libfprint/fp-device.h
+++ b/libfprint/fp-device.h
@@ -38,10 +38,12 @@ G_DECLARE_DERIVABLE_TYPE (FpDevice, fp_device, FP, DEVICE, GObject)
 /**
  * FpDeviceType:
  * @FP_DEVICE_TYPE_VIRTUAL: The device is a virtual device
+ * @FP_DEVICE_TYPE_UDEV: The device is a udev device
  * @FP_DEVICE_TYPE_USB: The device is a USB device
  */
 typedef enum {
   FP_DEVICE_TYPE_VIRTUAL,
+  FP_DEVICE_TYPE_UDEV,
   FP_DEVICE_TYPE_USB,
 } FpDeviceType;
 
diff --git a/libfprint/fpi-device.c b/libfprint/fpi-device.c
index 231dde9..9f334e2 100644
--- a/libfprint/fpi-device.c
+++ b/libfprint/fpi-device.c
@@ -335,6 +335,38 @@ fpi_device_get_usb_device (FpDevice *device)
   return priv->usb_device;
 }
 
+/**
+ * fpi_device_get_udev_data:
+ * @device: The #FpDevice
+ * @subtype: Which subtype to get information about
+ *
+ * Get a subtype-specific hardware resource for this #FpDevice. Only permissible to call if the
+ * #FpDevice is of type %FP_DEVICE_TYPE_UDEV.
+ *
+ * Returns: Depends on @subtype; for SPIDEV/HIDRAW returns a path to the relevant device.
+ */
+gpointer
+fpi_device_get_udev_data (FpDevice *device, FpiDeviceUdevSubtypeFlags subtype)
+{
+  FpDevicePrivate *priv = fp_device_get_instance_private (device);
+
+  g_return_val_if_fail (FP_IS_DEVICE (device), NULL);
+  g_return_val_if_fail (priv->type == FP_DEVICE_TYPE_UDEV, NULL);
+
+  switch (subtype)
+    {
+    case FPI_DEVICE_UDEV_SUBTYPE_HIDRAW:
+      return priv->udev_data.hidraw_path;
+
+    case FPI_DEVICE_UDEV_SUBTYPE_SPIDEV:
+      return priv->udev_data.spidev_path;
+
+    default:
+      g_return_val_if_reached (NULL);
+      return NULL;
+    }
+}
+
 /**
  * fpi_device_get_virtual_env:
  * @device: The #FpDevice
@@ -977,6 +1009,7 @@ fpi_device_close_complete (FpDevice *device, GError *error)
       break;
 
     case FP_DEVICE_TYPE_VIRTUAL:
+    case FP_DEVICE_TYPE_UDEV:
       break;
 
     default:
diff --git a/libfprint/fpi-device.h b/libfprint/fpi-device.h
index e0938ae..669ce15 100644
--- a/libfprint/fpi-device.h
+++ b/libfprint/fpi-device.h
@@ -24,6 +24,18 @@
 #include "fp-image.h"
 #include "fpi-print.h"
 
+#include <config.h>
+
+/**
+ * FpiDeviceUdevSubtype:
+ * @FPI_DEVICE_UDEV_SUBTYPE_SPIDEV: The device requires an spidev node
+ * @FPI_DEVICE_UDEV_SUBTYPE_HIDRAW: The device requires a hidraw node
+ */
+typedef enum {
+  FPI_DEVICE_UDEV_SUBTYPE_SPIDEV = 1 << 0,
+  FPI_DEVICE_UDEV_SUBTYPE_HIDRAW = 1 << 1,
+} FpiDeviceUdevSubtypeFlags;
+
 /**
  * FpIdEntry:
  *
@@ -43,6 +55,16 @@ struct _FpIdEntry
       guint vid;
     };
     const gchar *virtual_envvar;
+    struct
+    {
+      FpiDeviceUdevSubtypeFlags udev_types;
+      const gchar              *spi_acpi_id;
+      struct
+      {
+        guint pid;
+        guint vid;
+      } hid_id;
+    };
   };
   guint64 driver_data;
 };
@@ -171,6 +193,8 @@ typedef enum {
 
 GUsbDevice  *fpi_device_get_usb_device (FpDevice *device);
 const gchar *fpi_device_get_virtual_env (FpDevice *device);
+gpointer     fpi_device_get_udev_data (FpDevice                 *device,
+                                       FpiDeviceUdevSubtypeFlags subtype);
 //const gchar *fpi_device_get_spi_dev (FpDevice *device);
 
 
diff --git a/libfprint/meson.build b/libfprint/meson.build
index ee86033..0a24ffb 100644
--- a/libfprint/meson.build
+++ b/libfprint/meson.build
@@ -224,6 +224,7 @@ deps = [
     glib_dep,
     gobject_dep,
     gusb_dep,
+    gudev_dep,
     imaging_dep,
     mathlib_dep,
     nss_dep,
diff --git a/meson.build b/meson.build
index 1d63530..e110b33 100644
--- a/meson.build
+++ b/meson.build
@@ -94,6 +94,9 @@ virtual_drivers = [
     'virtual_device_storage',
 ]
 
+udev_drivers = [
+]
+
 default_drivers = [
     'upektc_img',
     'vfs5011',
@@ -126,7 +129,7 @@ endian_independent_drivers = virtual_drivers + [
     'synaptics',
 ]
 
-all_drivers = default_drivers + virtual_drivers
+all_drivers = default_drivers + virtual_drivers + udev_drivers
 
 if drivers == [ 'all' ]
     drivers = all_drivers
@@ -154,6 +157,7 @@ endif
 
 nss_dep = dependency('', required: false)
 imaging_dep = dependency('', required: false)
+gudev_dep = dependency('', required: false)
 libfprint_conf.set10('HAVE_PIXMAN', false)
 foreach driver: drivers
     if driver == 'uru4000'
@@ -170,6 +174,14 @@ foreach driver: drivers
 
         libfprint_conf.set10('HAVE_PIXMAN', true)
     endif
+	if udev_drivers.contains(driver)
+		gudev_dep = dependency('gudev-1.0', required: false)
+		if not gudev_dep.found()
+			error('udev is required for SPI support')
+		endif
+
+		libfprint_conf.set10('HAVE_UDEV', true)
+	endif
     if not all_drivers.contains(driver)
         error('Invalid driver \'' + driver + '\'')
     endif