From 2661e962df2f3cf618db21ff79b01bcf6a602616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hubert=20Figui=C3=A8re?= Date: Fri, 13 Sep 2024 19:03:33 -0400 Subject: [PATCH] Introduce USB portal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The USB portal is the middleman between sandboxed apps, and the devices connected and available to the host system. This is the first version of the portal. Device filtering ================ Sandboxed apps must declare which USB devices they support ahead of time. This information is read by the XDG Desktop Portal and used to determine which USB devices will be exposed to requesting apps. On Flatpak, these enumerable and hidden devices are set by the "--usb" and "--nousb" arguments against "flatpak build-finish" and "flatpak run". Neither "--devices=all" nor "--device=usb" do influence the portal. Hidding a device always take precedence over making them enumerable, even when a blanket permission ("--usb=all") is set. Individual devices are assigned a unique identifier by the portal, which is used for all further interactions. This unique identifier is completely random and independent of the device. Permission checks are in place to not allow apps to try and guess device ids without having permission to access then. Permissions =========== There are 2 dynamic permissions managed by the USB portal in the permission store: 1. Blanket USB permission: per-app permission to use any methods of the USB portal. Without this permission, apps must not be able to do anything - enumerate, monitor, or acquire - with the USB portal. [1] 2. Specific device permission: per-app permission to acquire a specific USB device, down to the serial number. Enumerating devices =================== There are 2 ways for apps to learn about devices: - Apps can call the EnumerateDevices() method, which gives a snapshot of the current devices to the app. - Apps can create a device monitoring session with CreateSession() which sends the list of available devices on creation, and also notifies the app about connected and disconnected devices. Only devices that the app is allowed to see are reported in both cases. The udev properties exposed by device enumeration is limited to a well known subset of properties. [2] Device acquisition & release ============================ Once an app has determined which devices it wants to access, the app can call the AcquireDevices() method. This method may prompt a dialog for the user to allow or deny the app from accessing specific devices. If permission is granted, XDG Desktop Portal tries to open the device file on the behalf of the requesting app, and pass down the file descriptor to that file. [3] --- [1] Exceptionally, apps can release previously acquired devices, even when this permission is disabled. This is so because we don't yet have kernel-sided USB revoking. With USB revoking in place, it would be possible to hard-cut app access right when the app permission changes. [2] This patch uses a hardcoded list. There is no mechanism for apps to influence which other udev properties are fetched. This approach is open to suggestions - it may be necessary to expose more information more liberally through the portal. [3] This is clearly not ideal. The ideal approach is to go through logind's TakeDevice() method. However, that will add significant complexity to the portal, since this logind method can only be called by the session controller (i.e. the only executable capable of calling TakeControl() in the session - usually the compositor). This can and probably should be implemented in a subsequent round of improvements to the USB portal. Co-Authored By: Georges Basile Stavracas Neto Co-Authored-By: Ryan Gonzalez Signed-off-by: Hubert Figuière --- .github/workflows/check.yml | 1 + .github/workflows/pages.yml | 1 + data/meson.build | 2 + data/org.freedesktop.impl.portal.Usb.xml | 75 + data/org.freedesktop.portal.Usb.xml | 222 +++ doc/api-reference.rst | 1 + doc/impl-dbus-interfaces.rst | 1 + meson.build | 7 + meson_options.txt | 4 + src/meson.build | 11 + src/usb.c | 1596 ++++++++++++++++++++++ src/usb.h | 30 + src/xdg-desktop-portal.c | 11 + src/xdp-app-info-flatpak.c | 61 + src/xdp-app-info-private.h | 2 + src/xdp-app-info.c | 19 + src/xdp-app-info.h | 6 + src/xdp-usb-query.c | 214 +++ src/xdp-usb-query.h | 84 ++ 19 files changed, 2348 insertions(+) create mode 100644 data/org.freedesktop.impl.portal.Usb.xml create mode 100644 data/org.freedesktop.portal.Usb.xml create mode 100644 src/usb.c create mode 100644 src/usb.h create mode 100644 src/xdp-usb-query.c create mode 100644 src/xdp-usb-query.h diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 13946dbfd..85550c7ba 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -67,6 +67,7 @@ jobs: librsvg2-common \ libgeoclue-2-dev \ libglib2.0-dev \ + libgudev-1.0-dev \ libjson-glib-dev \ libpipewire-0.3-dev \ libsystemd-dev \ diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 31c1c2140..90c29eac4 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -35,6 +35,7 @@ jobs: json-glib-devel \ libcap \ libcap-devel \ + libgudev-devel \ libportal-devel \ llvm \ meson \ diff --git a/data/meson.build b/data/meson.build index 4e601077e..66ad68d16 100644 --- a/data/meson.build +++ b/data/meson.build @@ -35,6 +35,7 @@ portal_sources = files( 'org.freedesktop.portal.Session.xml', 'org.freedesktop.portal.Settings.xml', 'org.freedesktop.portal.Trash.xml', + 'org.freedesktop.portal.Usb.xml', 'org.freedesktop.portal.Wallpaper.xml', ) @@ -61,6 +62,7 @@ portal_impl_sources = files( 'org.freedesktop.impl.portal.Secret.xml', 'org.freedesktop.impl.portal.Session.xml', 'org.freedesktop.impl.portal.Settings.xml', + 'org.freedesktop.impl.portal.Usb.xml', 'org.freedesktop.impl.portal.Wallpaper.xml', ) diff --git a/data/org.freedesktop.impl.portal.Usb.xml b/data/org.freedesktop.impl.portal.Usb.xml new file mode 100644 index 000000000..6268f8b2f --- /dev/null +++ b/data/org.freedesktop.impl.portal.Usb.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/org.freedesktop.portal.Usb.xml b/data/org.freedesktop.portal.Usb.xml new file mode 100644 index 000000000..4d4d06426 --- /dev/null +++ b/data/org.freedesktop.portal.Usb.xml @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/api-reference.rst b/doc/api-reference.rst index 7057851b5..7a0c85f2e 100644 --- a/doc/api-reference.rst +++ b/doc/api-reference.rst @@ -41,4 +41,5 @@ and the object path ``/org/freedesktop/portal/desktop`` on the session bus. doc-org.freedesktop.portal.Session.rst doc-org.freedesktop.portal.Settings.rst doc-org.freedesktop.portal.Trash.rst + doc-org.freedesktop.portal.Usb.rst doc-org.freedesktop.portal.Wallpaper.rst diff --git a/doc/impl-dbus-interfaces.rst b/doc/impl-dbus-interfaces.rst index a7e9425f3..937d2d196 100644 --- a/doc/impl-dbus-interfaces.rst +++ b/doc/impl-dbus-interfaces.rst @@ -30,4 +30,5 @@ accessible to sandboxed applications. doc-org.freedesktop.impl.portal.Secret.rst doc-org.freedesktop.impl.portal.Session.rst doc-org.freedesktop.impl.portal.Settings.rst + doc-org.freedesktop.impl.portal.Usb.rst doc-org.freedesktop.impl.portal.Wallpaper.rst diff --git a/meson.build b/meson.build index 5c19cb87a..0b9cc6ab4 100644 --- a/meson.build +++ b/meson.build @@ -115,6 +115,7 @@ libportal_dep = dependency('libportal', required: get_option('libportal')) pipewire_dep = dependency('libpipewire-0.3', version: '>= 0.2.90') libsystemd_dep = dependency('libsystemd', required: get_option('systemd')) +gudev_dep = dependency('gudev-1.0', required: get_option('gudev')) bwrap = find_program('bwrap', required: get_option('sandboxed-image-validation')) @@ -144,6 +145,11 @@ if have_libsystemd config_h.set('HAVE_LIBSYSTEMD', 1) endif +have_gudev = gudev_dep.found() +if have_gudev + config_h.set('HAVE_GUDEV', 1) +endif + add_project_arguments(['-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_66'], language: 'c') build_documentation = false @@ -202,6 +208,7 @@ summary({ 'Enable libsystemd support': have_libsystemd, 'Enable geoclue support': have_geoclue, 'Enable libportal support': have_libportal, + 'Enable gudev support': have_gudev, 'Enable installed tests:': enable_installed_tests, 'Enable python test suite': enable_pytest, 'Build man pages': rst2man.found(), diff --git a/meson_options.txt b/meson_options.txt index 19667f542..ace4bd983 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -22,6 +22,10 @@ option('geoclue', type: 'feature', value: 'auto', description: 'Enable Geoclue support. Needed for location portal') +option('gudev', + type: 'feature', + value: 'auto', + description: 'Enable udev support. Needed for the USB portal.') option('systemd', type: 'feature', value: 'auto', diff --git a/src/meson.build b/src/meson.build index b33cbfd08..78ed8611b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -62,6 +62,7 @@ xdp_utils_sources = files( 'xdp-app-info-flatpak.c', 'xdp-app-info-snap.c', 'xdp-app-info-test.c', + 'xdp-usb-query.c', ) if have_libsystemd @@ -126,6 +127,12 @@ if have_geoclue ) endif +if have_gudev + xdg_desktop_portal_sources += files( + 'usb.c', + ) +endif + common_deps = [ glib_dep, gio_dep, @@ -133,6 +140,10 @@ common_deps = [ json_glib_dep, ] +if gudev_dep.found() + common_deps += gudev_dep +endif + xdg_desktop_portal_deps = common_deps + [ geoclue_dep, pipewire_dep, diff --git a/src/usb.c b/src/usb.c new file mode 100644 index 000000000..f4108f14e --- /dev/null +++ b/src/usb.c @@ -0,0 +1,1596 @@ +/* + * Copyright © 2023-2024 GNOME Foundation Inc. + * 2020 Endless OS Foundation LLC + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Georges Basile Stavracas Neto + * Hubert Figuière + * Ryan Gonzalez + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "usb.h" +#include "request.h" +#include "permissions.h" +#include "session.h" +#include "xdp-dbus.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" +#include "xdp-usb-query.h" + +#define PERMISSION_TABLE "usb" +#define PERMISSION_ID "usb" + +/* TODO: + * + * AccessDevices() + * - Check if backend is returning appropriate device ids + * - Check if backend is not increasing permissions + * - Save allowed devices in the permission store + */ + +typedef struct _Usb +{ + XdpDbusUsbSkeleton parent_instance; + + GHashTable *ids_to_devices; + GHashTable *syspaths_to_ids; + + GHashTable *sessions; + GHashTable *sender_infos; + + GUdevClient *gudev_client; +} Usb; + +typedef struct _UsbClass +{ + XdpDbusUsbSkeletonClass parent_class; +} UsbClass; + +typedef struct +{ + char *device_id; + gboolean writable; +} UsbDeviceAcquireData; + +typedef struct _UsbOwnedDevice +{ + gatomicrefcount ref_count; + + char *sender_name; + char *device_id; + int fd; +} UsbOwnedDevice; + +typedef enum +{ + USB_SENDER_STATE_DEFAULT, + USB_SENDER_STATE_ACQUIRING_DEVICES, +} UsbSenderState; + +typedef struct _UsbSenderInfo +{ + gatomicrefcount ref_count; + + char *sender_name; + XdpAppInfo *app_info; + + UsbSenderState sender_state; + GPtrArray *acquiring_devices; + + GHashTable *owned_devices; /* device id → UsbOwnedDevices */ +} UsbSenderInfo; + +static XdpDbusImplUsb *usb_impl; +static Usb *usb; + +static const char * const allowed_udev_properties[] = { + "ID_INPUT_JOYSTICK", + "ID_MODEL_ID", + "ID_MODEL_ENC", + "ID_MODEL_FROM_DATABASE", + "ID_REVISION", + "ID_SERIAL", + "ID_SERIAL_SHORT", + "ID_TYPE", + "ID_VENDOR_ENC", + "ID_VENDOR_ID", + "ID_VENDOR_FROM_DATABASE", + NULL, +}; + +GType usb_get_type (void) G_GNUC_CONST; +static void usb_iface_init (XdpDbusUsbIface *iface); + +G_DEFINE_TYPE_WITH_CODE (Usb, usb, XDP_DBUS_TYPE_USB_SKELETON, + G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_USB, usb_iface_init)); + +static gboolean +hex_to_uint16 (const char *property, + uint16_t *out_n) +{ + long n; + + g_assert (property != NULL); + g_assert (out_n != NULL); + + n = strtol (property, NULL, 16); + + if (n < 0 || n > UINT16_MAX) + return FALSE; + + *out_n = (uint16_t) n; + return TRUE; +} + +static gboolean +is_gudev_device_suitable (GUdevDevice *device) +{ + const char *device_file = NULL; + const char *devtype = NULL; + + g_assert (G_UDEV_IS_DEVICE (device)); + g_return_val_if_fail (g_strcmp0 (g_udev_device_get_subsystem (device), "usb") == 0, + FALSE); + + device_file = g_udev_device_get_device_file (device); + if (!device_file) + return FALSE; + + devtype = g_udev_device_get_property (device, "DEVTYPE"); + if (!devtype || g_strcmp0 (devtype, "usb_device") != 0) + return FALSE; + + return TRUE; +} + +static char * +unique_permission_id_for_device (GUdevDevice *device) +{ + g_autoptr(GString) permission_id = NULL; + const char *property; + int count = 0; + + permission_id = g_string_new ("device:"); + + property = g_udev_device_get_property (device, "ID_VENDOR_ID"); + if (property) + { + g_string_append (permission_id, property); + count++; + } + + property = g_udev_device_get_property (device, "ID_MODEL_ID"); + if (property) + { + g_string_append_printf (permission_id, "%s%s", count > 0 ? "/" : "", property); + count++; + } + + property = g_udev_device_get_property (device, "ID_SERIAL"); + if (property) + { + g_string_append_printf (permission_id, "%s%s", count > 0 ? "/" : "", property); + count++; + } + + return g_string_free (g_steal_pointer (&permission_id), FALSE); +} + +static void +usb_device_acquire_data_free (gpointer data) +{ + UsbDeviceAcquireData *access_data = (UsbDeviceAcquireData *) data; + + g_return_if_fail (access_data); + + g_clear_pointer (&access_data->device_id, g_free); + g_clear_pointer (&access_data, g_free); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (UsbDeviceAcquireData, usb_device_acquire_data_free) + +static UsbOwnedDevice * +usb_owned_device_ref (UsbOwnedDevice *owned_device) +{ + g_assert (owned_device); + + g_atomic_ref_count_inc (&owned_device->ref_count); + + return owned_device; +} + +static void +usb_owned_device_unref (gpointer data) +{ + UsbOwnedDevice *owned_device = (UsbOwnedDevice *) data; + + if (!owned_device) + return; + + if (g_atomic_ref_count_dec (&owned_device->ref_count)) + { + xdp_close_fd (&owned_device->fd); + g_clear_pointer (&owned_device->device_id, g_free); + g_clear_pointer (&owned_device, g_free); + } +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (UsbOwnedDevice, usb_owned_device_unref) + +static void +usb_sender_info_unref (gpointer data) +{ + UsbSenderInfo *sender_info = (UsbSenderInfo *) data; + + if (g_atomic_ref_count_dec (&sender_info->ref_count)) + { + g_clear_object (&sender_info->app_info); + g_clear_pointer (&sender_info->sender_name, g_free); + g_clear_pointer (&sender_info->owned_devices, g_hash_table_destroy); + g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + g_clear_pointer (&sender_info, g_free); + } +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (UsbSenderInfo, usb_sender_info_unref) + +static UsbSenderInfo * +usb_sender_info_new (const char *sender_name, + XdpAppInfo *app_info) +{ + g_autoptr(UsbSenderInfo) sender_info = NULL; + + sender_info = g_new0 (UsbSenderInfo, 1); + g_atomic_ref_count_init (&sender_info->ref_count); + sender_info->sender_name = g_strdup (sender_name); + sender_info->app_info = g_object_ref (app_info); + sender_info->sender_state = USB_SENDER_STATE_DEFAULT; + sender_info->owned_devices = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, usb_owned_device_unref); + + return g_steal_pointer (&sender_info); +} + +static UsbSenderInfo * +_usb_sender_info_from_sender (Usb *self, const char *sender, XdpAppInfo *app_info) +{ + g_autoptr(UsbSenderInfo) sender_info = NULL; + + + sender_info = g_hash_table_lookup (self->sender_infos, sender); + + if (!sender_info) + { + sender_info = usb_sender_info_new (sender, app_info); + g_hash_table_insert (self->sender_infos, g_strdup (sender), sender_info); + } + + g_assert (sender_info != NULL); + g_atomic_ref_count_inc (&sender_info->ref_count); + + return g_steal_pointer (&sender_info); +} + +static UsbSenderInfo * +usb_sender_info_from_call (Usb *self, + Call *call) +{ + g_assert (call != NULL); + + return _usb_sender_info_from_sender (self, call->sender, call->app_info); +} + +static UsbSenderInfo * +usb_sender_info_from_request (Usb *self, + Request *request) +{ + g_assert (request != NULL); + + return _usb_sender_info_from_sender (self, request->sender, request->app_info); +} + +static void +usb_sender_info_acquire_device (UsbSenderInfo *sender_info, + const char *device_id, + int fd) +{ + g_autoptr(UsbOwnedDevice) owned_device = NULL; + + g_assert (sender_info != NULL); + g_assert (!g_hash_table_contains (sender_info->owned_devices, device_id)); + + owned_device = g_new0 (UsbOwnedDevice, 1); + g_atomic_ref_count_init (&owned_device->ref_count); + owned_device->device_id = g_strdup (device_id); + owned_device->fd = xdp_steal_fd (&fd); + + g_hash_table_insert (sender_info->owned_devices, + g_strdup (device_id), + g_steal_pointer (&owned_device)); +} + +static void +usb_sender_info_release_device (UsbSenderInfo *sender_info, + const char *device_id) +{ + g_assert (sender_info != NULL); + + if (!g_hash_table_remove (sender_info->owned_devices, device_id)) + g_warning ("Device %s not owned by %s", device_id, sender_info->sender_name); + +} + +static Permission +usb_sender_info_get_device_permission (UsbSenderInfo *sender_info, + GUdevDevice *device) +{ + g_autofree char *permission_id = NULL; + + g_assert (G_UDEV_IS_DEVICE (device)); + + permission_id = unique_permission_id_for_device (device); + return get_permission_sync (xdp_app_info_get_id (sender_info->app_info), + PERMISSION_TABLE, permission_id); +} + +static void +usb_sender_info_set_device_permission (UsbSenderInfo *sender_info, + GUdevDevice *device, + Permission permission) +{ + g_autofree char *permission_id = NULL; + + g_assert (G_UDEV_IS_DEVICE (device)); + + permission_id = unique_permission_id_for_device (device); + set_permission_sync (xdp_app_info_get_id (sender_info->app_info), + PERMISSION_TABLE, permission_id, permission); +} + +static gboolean +usb_sender_info_match_device (UsbSenderInfo *sender_info, + GUdevDevice *device) +{ + const char *device_subclass_str = NULL; + const char *device_class_str = NULL; + const char *product_id_str = NULL; + const char *vendor_id_str = NULL; + gboolean device_has_product_id = FALSE; + gboolean device_has_vendor_id = FALSE; + gboolean device_has_subclass = FALSE; + gboolean device_has_class = FALSE; + Permission permission; + uint16_t device_product_id; + uint16_t device_vendor_id; + uint16_t device_subclass; + uint16_t device_class; + gboolean match = FALSE; + const GPtrArray *queries = NULL; + + permission = usb_sender_info_get_device_permission (sender_info, device); + if (permission == PERMISSION_NO) + return FALSE; + + vendor_id_str = g_udev_device_get_property (device, "ID_VENDOR_ID"); + if (vendor_id_str != NULL && hex_to_uint16 (vendor_id_str, &device_vendor_id)) + device_has_vendor_id = TRUE; + + product_id_str = g_udev_device_get_property (device, "ID_MODEL_ID"); + if (product_id_str != NULL && hex_to_uint16 (product_id_str, &device_product_id)) + device_has_product_id = TRUE; + + device_class_str = g_udev_device_get_sysfs_attr (device, "bDeviceClass"); + if (device_class_str != NULL && hex_to_uint16 (device_class_str, &device_class)) + device_has_class = TRUE; + + device_subclass_str = g_udev_device_get_sysfs_attr (device, "bDeviceSubclass"); + if (device_subclass_str != NULL && hex_to_uint16 (device_subclass_str, &device_subclass)) + device_has_subclass = TRUE; + + queries = xdp_app_info_get_usb_queries (sender_info->app_info); + for (size_t i = 0; i < queries->len; i++) + { + XdpUsbQuery *query = g_ptr_array_index (queries, i); + gboolean query_matches = TRUE; + + if (!query) + { + g_debug ("query %ld is null", i); + continue; + } + + for (size_t j = 0; j < query->rules->len; j++) + { + XdpUsbRule *rule = g_ptr_array_index (query->rules, j); + + switch (rule->rule_type) + { + case XDP_USB_RULE_TYPE_ALL: + query_matches = TRUE; + break; + + case XDP_USB_RULE_TYPE_CLASS: + query_matches &= device_has_class && + device_class == rule->d.device_class.class; + + if (rule->d.device_class.type == XDP_USB_RULE_CLASS_TYPE_CLASS_SUBCLASS) + query_matches &= device_has_subclass && + device_subclass == rule->d.device_class.subclass; + + break; + + case XDP_USB_RULE_TYPE_DEVICE: + query_matches &= device_has_product_id && + device_product_id == rule->d.product.id; + break; + + case XDP_USB_RULE_TYPE_VENDOR: + query_matches &= device_has_vendor_id && + device_vendor_id == rule->d.vendor.id; + break; + + default: + g_assert_not_reached (); + } + } + + switch (query->query_type) + { + case XDP_USB_QUERY_TYPE_ENUMERABLE: + if (query_matches) + match = TRUE; + break; + + case XDP_USB_QUERY_TYPE_HIDDEN: + if (query_matches) + return FALSE; + break; + } + } + + return match; +} + +/* UsbSession */ + +struct _UsbSessionClass +{ + SessionClass parent_class; +}; + +#define USB_TYPE_SESSION (usb_session_get_type ()) +G_DECLARE_FINAL_TYPE (UsbSession, + usb_session, + USB, SESSION, + Session) + +struct _UsbSession +{ + Session parent; + + GHashTable *available_devices; +}; + +G_DEFINE_TYPE (UsbSession, usb_session, USB_TYPE_SESSION) + +static void +usb_session_close (Session *session) +{ + g_debug ("USB session '%s' closed", session->id); + + g_assert (g_hash_table_contains (usb->sessions, session)); + g_hash_table_remove (usb->sessions, session); +} + +static void +usb_session_dispose (GObject *object) +{ + UsbSession *usb_session = USB_SESSION (object); + + g_clear_pointer (&usb_session->available_devices, g_hash_table_destroy); +} + +static void +usb_session_class_init (UsbSessionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + SessionClass *session_class = (SessionClass*) klass; + + object_class->dispose = usb_session_dispose; + + session_class->close = usb_session_close; +} + +static void +usb_session_init (UsbSession *session) +{ + session->available_devices = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); +} + +static UsbSession * +usb_session_new (GDBusConnection *connection, + Call *call, + GVariant *options, + GError **error) +{ + Session *session = NULL; + + session = g_initable_new (usb_session_get_type (), + NULL, error, + "connection", connection, + "sender", call->sender, + "app-id", xdp_app_info_get_id (call->app_info), + "token", lookup_session_token (options), + NULL); + if (!session) + return NULL; + + g_debug ("[usb] USB session '%s' created", session->id); + + return USB_SESSION (session); +} + +static GVariant * +gudev_device_to_variant (Usb *self, + UsbSenderInfo *sender_info, + GUdevDevice *device) +{ + g_auto(GVariantDict) udev_properties_dict = G_VARIANT_DICT_INIT (NULL); + g_auto(GVariantDict) device_variant_dict = G_VARIANT_DICT_INIT (NULL); + g_autoptr(GUdevDevice) parent = NULL; + const char *device_file = NULL; + size_t n_added_properties = 0; + + g_assert (is_gudev_device_suitable (device)); + + parent = g_udev_device_get_parent (device); + if (parent != NULL && usb_sender_info_match_device (sender_info, parent)) + { + const char *parent_syspath = NULL; + const char *parent_id = NULL; + + parent_syspath = g_udev_device_get_sysfs_path (parent); + if (parent_syspath != NULL) + { + parent_id = g_hash_table_lookup (self->syspaths_to_ids, parent_syspath); + if (parent_id != NULL) + g_variant_dict_insert (&device_variant_dict, "parent", "s", parent_id); + } + } + + device_file = g_udev_device_get_device_file (device); + g_variant_dict_insert (&device_variant_dict, "device-file", "s", device_file); + + if (access (device_file, R_OK) != -1) + g_variant_dict_insert (&device_variant_dict, "readable", "b", TRUE); + if (access (device_file, W_OK) != -1) + g_variant_dict_insert (&device_variant_dict, "writable", "b", TRUE); + + for (size_t i = 0; allowed_udev_properties[i] != NULL; i++) + { + const char *property = g_udev_device_get_property (device, allowed_udev_properties[i]); + + if (!property) + continue; + + g_variant_dict_insert (&udev_properties_dict, allowed_udev_properties[i], "s", property); + n_added_properties++; + } + + if (n_added_properties > 0) + { + g_variant_dict_insert (&device_variant_dict, + "properties", + "@a{sv}", + g_variant_dict_end (&udev_properties_dict)); + } + + return g_variant_dict_end (&device_variant_dict); +} + +static gboolean +create_unique_usb_id (Usb *self, + GUdevDevice *device, + char **out_new_id) +{ + g_autofree char *id = NULL; + const char *syspath; + + g_assert (is_gudev_device_suitable (device)); + + syspath = g_udev_device_get_sysfs_path (device); + g_assert (syspath != NULL); + + do + { + g_clear_pointer (&id, g_free); + id = g_uuid_string_random (); + } + while (g_hash_table_contains (self->ids_to_devices, id)); + + g_debug ("Assigned unique ID %s to USB device %s", id, syspath); + + g_hash_table_insert (self->ids_to_devices, g_strdup (id), g_object_ref (device)); + g_hash_table_insert (self->syspaths_to_ids, g_strdup (syspath), g_strdup (id)); + + if (out_new_id) + *out_new_id = g_steal_pointer (&id); + + return TRUE; +} + +/* Callbacks */ + +static void +gudev_client_uevent_cb (GUdevClient *client, + const char *action, + GUdevDevice *device, + Usb *self) +{ + static const char *supported_actions[] = { + "add", + "change", + "remove", + NULL, + }; + + g_autofree char *id = NULL; + GHashTableIter iter; + UsbSession *usb_session; + const char *syspath = NULL; + gboolean removing; + + if (!g_strv_contains (supported_actions, action)) + return; + + if (!is_gudev_device_suitable (device)) + return; + + removing = g_str_equal (action, "remove"); + + if (g_str_equal (action, "add")) + { + create_unique_usb_id (self, device, &id); + } + else + { + syspath = g_udev_device_get_sysfs_path (device); + + g_assert (syspath != NULL); + id = g_strdup (g_hash_table_lookup (self->syspaths_to_ids, syspath)); + } + + g_assert (id != NULL); + + /* Send event to all sessions that are allowed to handle it */ + g_hash_table_iter_init (&iter, self->sessions); + while (g_hash_table_iter_next (&iter, (gpointer *) &usb_session, NULL)) + { + g_autoptr(GVariant) device_variant = NULL; + GVariantBuilder devices_builder; + UsbSenderInfo *sender_info; + Session *session; + + g_assert (G_UDEV_IS_DEVICE (device)); + g_assert (g_strcmp0 (g_udev_device_get_subsystem (device), "usb") == 0); + + session = (Session *) usb_session; + sender_info = g_hash_table_lookup (self->sender_infos, session->sender); + g_assert (sender_info != NULL); + + /* We can't use usb_sender_info_match_device() when a device is being removed because, + * on removal, the only property the GUdevDevice has is its sysfs path. + * Check if this device was previously available to the USB session + * instead. */ + if ((removing && !g_hash_table_contains (usb_session->available_devices, id)) || + (!removing && !usb_sender_info_match_device (sender_info, device))) + continue; + + g_variant_builder_init (&devices_builder, G_VARIANT_TYPE ("a(ssa{sv})")); + + device_variant = gudev_device_to_variant (self, sender_info, device); + g_variant_builder_add (&devices_builder, "(ss@a{sv})", action, id, g_steal_pointer (&device_variant)); + + g_dbus_connection_emit_signal (session->connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Usb", + "DeviceEvents", + g_variant_new ("(o@a(ssa{sv}))", + session->id, + g_variant_builder_end (&devices_builder)), + NULL); + + if (removing) + g_hash_table_remove (usb_session->available_devices, id); + else + g_hash_table_add (usb_session->available_devices, g_strdup (id)); + } + + if (removing) + { + g_assert (syspath != NULL); + + g_debug ("Removing %s -> %s", id, syspath); + + /* The value of id is owned by syspaths_to_ids, so that must be removed *after* + the id is used for removal from ids_to_devices. */ + if (!g_hash_table_remove (self->ids_to_devices, id)) + g_critical ("Error removing USB device from ids_to_devices table"); + + if (!g_hash_table_remove (self->syspaths_to_ids, syspath)) + g_critical ("Error removing USB device from syspaths_to_ids table"); + } +} + +/* CreateSession */ + +static XdpOptionKey usb_create_session_options[] = { + { "session_handle_token", G_VARIANT_TYPE_STRING, NULL }, +}; + +static void +send_initial_device_list (Usb *self, UsbSession *usb_session, Call *call) +{ + /* Send initial list of devices the app has permission to see */ + g_autoptr(UsbSenderInfo) sender_info = NULL; + Session *session = (Session *) usb_session; + GVariantBuilder devices_builder; + GHashTableIter iter; + GUdevDevice *device; + const char *id; + + g_debug ("[usb] Appending devices to CreateSession response"); + + g_variant_builder_init (&devices_builder, G_VARIANT_TYPE ("a(ssa{sv})")); + + g_assert (self != NULL); + + sender_info = usb_sender_info_from_call (self, call); + + g_hash_table_iter_init (&iter, self->ids_to_devices); + while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &device)) + { + g_autoptr(GVariant) device_variant = NULL; + + g_assert (G_UDEV_IS_DEVICE (device)); + g_assert (g_strcmp0 (g_udev_device_get_subsystem (device), "usb") == 0); + + if (!usb_sender_info_match_device (sender_info, device)) + continue; + + device_variant = gudev_device_to_variant (self, sender_info, device); + g_variant_builder_add (&devices_builder, "(ss@a{sv})", "add", id, g_steal_pointer (&device_variant)); + + g_hash_table_add (usb_session->available_devices, g_strdup (id)); + } + + g_dbus_connection_emit_signal (session->connection, + session->sender, + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Usb", + "DeviceEvents", + g_variant_new ("(o@a(ssa{sv}))", + session->id, + g_variant_builder_end (&devices_builder)), + NULL); +} + +static gboolean +handle_create_session (XdpDbusUsb *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + GDBusConnection *connection; + GVariantBuilder options_builder; + UsbSession *usb_session; + Permission permission; + Session *session; + Call *call; + Usb *self; + + self = (Usb *) object; + call = call_from_invocation (invocation); + + g_debug ("[usb] Handling CreateSession"); + + permission = get_permission_sync (xdp_app_info_get_id (call->app_info), + PERMISSION_TABLE, + PERMISSION_ID); + if (permission == PERMISSION_NO) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not allowed to create USB sessions"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, + &options_builder, + usb_create_session_options, + G_N_ELEMENTS (usb_create_session_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + connection = g_dbus_method_invocation_get_connection (invocation); + usb_session = usb_session_new (connection, call, options, &error); + if (!usb_session) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + session = (Session *) usb_session; + if (!session_export (session, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + session_close (session, FALSE); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + session_register (session); + + g_debug ("New USB session registered: %s", session->id); + g_hash_table_add (self->sessions, usb_session); + + xdp_dbus_usb_complete_create_session (object, invocation, session->id); + + send_initial_device_list (self, usb_session, call); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +/* EnumerateDevices */ + +static XdpOptionKey usb_enumerate_devices_options[] = { +}; + +static GVariant * +list_permitted_devices (Usb *self, Call *call) +{ + g_autoptr(UsbSenderInfo) sender_info = NULL; + GVariantBuilder builder; + GHashTableIter iter; + GUdevDevice *device; + const char *id; + + sender_info = usb_sender_info_from_call (self, call); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(sa{sv})")); + + g_hash_table_iter_init (&iter, self->ids_to_devices); + while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &device)) + { + g_assert (G_UDEV_IS_DEVICE (device)); + g_assert (g_strcmp0 (g_udev_device_get_subsystem (device), "usb") == 0); + + if (usb_sender_info_match_device (sender_info, device)) + g_variant_builder_add (&builder, "(s@a{sv})", id, gudev_device_to_variant (self, sender_info, device)); + } + + return g_variant_ref_sink (g_variant_builder_end (&builder)); +} + +/* List devices the app has permission */ +static gboolean +handle_enumerate_devices (XdpDbusUsb *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + g_autoptr(GVariant) options = NULL; + g_autoptr(GVariant) devices = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + Permission permission; + Call *call; + Usb *self; + + self = (Usb *) object; + call = call_from_invocation (invocation); + + permission = get_permission_sync (xdp_app_info_get_id (call->app_info), + PERMISSION_TABLE, + PERMISSION_ID); + + if (permission == PERMISSION_NO) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not allowed to enumerate devices"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, &options_builder, + usb_enumerate_devices_options, + G_N_ELEMENTS (usb_enumerate_devices_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + devices = list_permitted_devices(self, call); + + xdp_dbus_usb_complete_enumerate_devices (object, invocation, g_steal_pointer (&devices)); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +/* AccessDevice */ + +static XdpOptionKey usb_device_options[] = { + { "writable", G_VARIANT_TYPE_BOOLEAN, NULL }, +}; + +static void +usb_acquire_devices_cb (GObject *source_object, + GAsyncResult *result, + gpointer data) +{ + XdgDesktopPortalResponseEnum response; + g_autoptr(UsbSenderInfo) sender_info = NULL; + g_autoptr(GVariantIter) devices_iter = NULL; + g_auto(GVariantBuilder) results_builder; + g_autoptr (GVariant) results = NULL; + g_autoptr(Request) request = data; + g_autoptr(GError) error = NULL; + GVariant *options; + const char *device_id; + + REQUEST_AUTOLOCK (request); + + response = XDG_DESKTOP_PORTAL_RESPONSE_OTHER; + sender_info = usb_sender_info_from_request (usb, request); + + g_assert (sender_info != NULL); + g_assert (sender_info->sender_state == USB_SENDER_STATE_ACQUIRING_DEVICES); + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT); + + if (!xdp_dbus_impl_usb_call_acquire_devices_finish (usb_impl, &response, &results, result, &error)) + { + response = XDG_DESKTOP_PORTAL_RESPONSE_OTHER; + g_dbus_error_strip_remote_error (error); + goto out; + } + + /* TODO: check if the list of devices that the backend reported is strictly + * equal or a subset of the devices the app requested. */ + + /* TODO: check if we're strictly equal or downgrading the "writable" option */ + + if (!g_variant_lookup (results, "devices", "a(sa{sv})", &devices_iter)) + goto out; + + if (response == XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS) + { + g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + sender_info->acquiring_devices = g_ptr_array_new_full (g_variant_iter_n_children (devices_iter), + usb_device_acquire_data_free); + while (g_variant_iter_next (devices_iter, "(&s@a{sv})", &device_id, &options)) + { + g_autoptr(UsbDeviceAcquireData) access_data = NULL; + GUdevDevice *device; + gboolean writable; + + device = g_hash_table_lookup (usb->ids_to_devices, device_id); + if (!device) + continue; + + if (!g_variant_lookup (options, "writable", "b", &writable)) + writable = FALSE; + + access_data = g_new0 (UsbDeviceAcquireData, 1); + access_data->device_id = g_strdup (device_id); + access_data->writable = writable; + + g_ptr_array_add (sender_info->acquiring_devices, g_steal_pointer (&access_data)); + + usb_sender_info_set_device_permission (sender_info, device, PERMISSION_YES); + + g_clear_pointer (&options, g_variant_unref); + } + } + else if (response == XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED) + { + sender_info->sender_state = USB_SENDER_STATE_DEFAULT; + } + +out: + if (request->exported) + { + xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), + response, + g_variant_builder_end (&results_builder)); + request_unexport (request); + } +} + +static gboolean +filter_access_devices (Usb *self, + UsbSenderInfo *sender_info, + GVariant *devices, + GVariant **out_filtered_devices, + GError **out_error) +{ + GVariantBuilder filtered_devices_builder; + GVariantIter *device_options_iter; + GVariantIter devices_iter; + const char *device_id; + size_t n_devices; + + g_assert (self != NULL); + g_assert (sender_info != NULL); + g_assert (devices != NULL); + g_assert (out_filtered_devices != NULL && *out_filtered_devices == NULL); + g_assert (out_error != NULL && *out_error == NULL); + + n_devices = g_variant_iter_init (&devices_iter, devices); + + if (n_devices == 0) + { + g_set_error (out_error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "No devices in the devices array"); + return FALSE; + } + + g_variant_builder_init (&filtered_devices_builder, G_VARIANT_TYPE ("a(sa{sv}a{sv})")); + + while (g_variant_iter_next (&devices_iter, + "(&sa{sv})", + &device_id, + &device_options_iter)) + { + g_autoptr(GVariantIter) owned_deviced_options_iter = device_options_iter; + g_autoptr(GVariant) device_variant = NULL; + GVariantDict device_options_dict; + GUdevDevice *device; + GVariant *device_option_value; + const char *device_option; + + device = g_hash_table_lookup (self->ids_to_devices, device_id); + + if (!device) + { + g_set_error (out_error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, + "Device %s not available", + device_id); + return FALSE; + } + + g_assert (G_UDEV_IS_DEVICE (device)); + g_assert (g_strcmp0 (g_udev_device_get_subsystem (device), "usb") == 0); + + /* Can the app even request this device? */ + if (!usb_sender_info_match_device (sender_info, device)) + { + g_set_error (out_error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Access to device %s is not allowed", + device_id); + return FALSE; + } + + g_variant_dict_init (&device_options_dict, NULL); + + while (g_variant_iter_next (device_options_iter, + "{&sv}", + &device_option, + &device_option_value)) + { + for (size_t i = 0; i < G_N_ELEMENTS (usb_device_options); i++) + { + if (g_strcmp0 (device_option, usb_device_options[i].key) != 0) + continue; + + if (!g_variant_is_of_type (device_option_value, usb_device_options[i].type)) + { + g_set_error (out_error, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Invalid type for option '%s'", + device_option); + g_variant_builder_clear (&filtered_devices_builder); + g_variant_dict_clear (&device_options_dict); + g_clear_pointer (&device_option_value, g_variant_unref); + return FALSE; + } + + g_variant_dict_insert_value (&device_options_dict, device_option, device_option_value); + + g_clear_pointer (&device_option_value, g_variant_unref); + } + } + + device_variant = gudev_device_to_variant (self, sender_info, device); + + g_variant_builder_add (&filtered_devices_builder, + "(s@a{sv}@a{sv})", + device_id, + g_steal_pointer (&device_variant), + g_variant_dict_end (&device_options_dict)); + } + + *out_filtered_devices = g_variant_builder_end (&filtered_devices_builder); + return TRUE; +} + +static XdpOptionKey usb_access_devices_options[] = { +}; + +static gboolean +handle_acquire_devices (XdpDbusUsb *object, + GDBusMethodInvocation *invocation, + const char *arg_parent_window, + GVariant *arg_devices, + GVariant *arg_options) +{ + g_autoptr(XdpDbusImplRequest) impl_request = NULL; + g_autoptr(UsbSenderInfo) sender_info = NULL; + g_autoptr(GVariant) filtered_devices = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + Permission permission; + Request *request; + Usb *self; + + self = (Usb *) object; + request = request_from_invocation (invocation); + + g_debug ("[usb] Handling AccessDevices"); + + REQUEST_AUTOLOCK (request); + + permission = get_permission_sync (xdp_app_info_get_id (request->app_info), + PERMISSION_TABLE, + PERMISSION_ID); + if (permission == PERMISSION_NO) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not allowed to create USB sessions"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (usb_impl)), + G_DBUS_PROXY_FLAGS_NONE, + g_dbus_proxy_get_name (G_DBUS_PROXY (usb_impl)), + request->id, + NULL, + &error); + if (!impl_request) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, + &options_builder, + usb_access_devices_options, + G_N_ELEMENTS (usb_access_devices_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_ref_sink (g_variant_builder_end (&options_builder)); + + sender_info = usb_sender_info_from_request (self, request); + g_assert (sender_info != NULL); + + if (sender_info->sender_state != USB_SENDER_STATE_DEFAULT) + { + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_FAILED, + "Cannot call AcquireDevices() with an unfinished " + "call to AcquireDevices()"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + /* Validate devices */ + if (!filter_access_devices (self, sender_info, arg_devices, &filtered_devices, &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + request_set_impl_request (request, impl_request); + request_export (request, g_dbus_method_invocation_get_connection (invocation)); + + sender_info->sender_state = USB_SENDER_STATE_ACQUIRING_DEVICES; + + xdp_dbus_impl_usb_call_acquire_devices (usb_impl, + request->id, + arg_parent_window, + xdp_app_info_get_id (request->app_info), + g_steal_pointer (&filtered_devices), + g_steal_pointer (&options), + NULL, + usb_acquire_devices_cb, + g_object_ref (request)); + + xdp_dbus_usb_complete_acquire_devices (object, invocation, request->id); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +/* FinishAccessDevices */ + +#define MAX_DEVICES 8 + +static gboolean +handle_finish_acquire_devices (XdpDbusUsb *object, + GDBusMethodInvocation *invocation, + GVariant *arg_options) +{ + g_autoptr(UsbSenderInfo) sender_info = NULL; + g_autoptr(GUnixFDList) fds = NULL; + GVariantBuilder results_builder; + Permission permission; + uint32_t accessed_devices; + gboolean finished; + Call *call; + Usb *self; + + self = (Usb *) object; + call = call_from_invocation (invocation); + + g_debug ("[usb] Handling FinishAccessDevices"); + + sender_info = usb_sender_info_from_call (self, call); + + permission = get_permission_sync (xdp_app_info_get_id (call->app_info), + PERMISSION_TABLE, + PERMISSION_ID); + if (permission == PERMISSION_NO) + { + /* If permission was revoked in between D-Bus calls, reset state */ + sender_info->sender_state = USB_SENDER_STATE_DEFAULT; + g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "Not allowed to access USB devices"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + if (sender_info->sender_state != USB_SENDER_STATE_ACQUIRING_DEVICES || sender_info->acquiring_devices == NULL) + { + /* If the request was cancelled in some way. This would happen by calling + * FinishAcquireDevices after the user denied the permission. + */ + sender_info->sender_state = USB_SENDER_STATE_DEFAULT; + g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + + g_dbus_method_invocation_return_error (invocation, + XDG_DESKTOP_PORTAL_ERROR, + XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, + "There is no device being acquired"); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + + /* We should never trigger these asserts. */ + g_assert (sender_info->sender_state == USB_SENDER_STATE_ACQUIRING_DEVICES); + g_assert (sender_info->acquiring_devices != NULL); + + fds = g_unix_fd_list_new (); + + g_variant_builder_init (&results_builder, G_VARIANT_TYPE ("a(sa{sv})")); + + accessed_devices = 0; + while (accessed_devices < MAX_DEVICES && + sender_info->acquiring_devices->len > 0) + { + g_autoptr(UsbDeviceAcquireData) access_data = NULL; + g_autoptr(GError) error = NULL; + GVariantDict dict; + UsbOwnedDevice *owned_device = NULL; + xdp_autofd int fd = -1; + int fd_index; + + g_variant_dict_init (&dict, NULL); + + access_data = g_ptr_array_steal_index (sender_info->acquiring_devices, 0); + + /* Check we haven't already acquired the device */ + owned_device = g_hash_table_lookup (sender_info->owned_devices, access_data->device_id); + if (!owned_device) + { + const char *device_file; + GUdevDevice *device; + + device = g_hash_table_lookup (self->ids_to_devices, access_data->device_id); + + if (!device) + { + g_variant_dict_insert (&dict, "success", "b", FALSE); + g_variant_dict_insert (&dict, "error", "s", _("Device not available")); + g_variant_builder_add (&results_builder, "(s@a{sv})", + access_data->device_id, + g_variant_dict_end (&dict)); + continue; + } + + device_file = g_udev_device_get_device_file (device); + g_assert (device_file != NULL); + + /* Can the app even request this device? */ + if (!usb_sender_info_match_device (sender_info, device)) + { + g_variant_dict_insert (&dict, "success", "b", FALSE); + g_variant_dict_insert (&dict, "error", "s", _("Not allowed")); + g_variant_builder_add (&results_builder, "(s@a{sv})", + access_data->device_id, + g_variant_dict_end (&dict)); + continue; + } + + fd = open (device_file, access_data->writable ? O_RDWR : O_RDONLY); + if (fd == -1) + { + g_variant_dict_insert (&dict, "success", "b", FALSE); + g_variant_dict_insert (&dict, "error", "s", g_strerror (errno)); + g_variant_builder_add (&results_builder, "(s@a{sv})", + access_data->device_id, + g_variant_dict_end (&dict)); + continue; + } + fd_index = g_unix_fd_list_append (fds, fd, &error); + } + else + { + /* If we have already acquired the device, just return the fd again */ + fd_index = g_unix_fd_list_append (fds, owned_device->fd, &error); + } + + if (error) + { + g_variant_dict_insert (&dict, "success", "b", FALSE); + g_variant_dict_insert (&dict, "error", "s", error->message); + g_variant_builder_add (&results_builder, "(s@a{sv})", + access_data->device_id, + g_variant_dict_end (&dict)); + continue; + } + + /* This sender now owns this device. Either create a new one + * or ref the existing one. + */ + if (!owned_device) + { + usb_sender_info_acquire_device (sender_info, + access_data->device_id, + xdp_steal_fd (&fd)); + } + else + { + usb_owned_device_ref (owned_device); + } + + g_variant_dict_insert (&dict, "success", "b", TRUE); + g_variant_dict_insert (&dict, "fd", "h", fd_index); + g_variant_builder_add (&results_builder, "(s@a{sv})", + access_data->device_id, + g_variant_dict_end (&dict)); + + accessed_devices++; + } + + finished = sender_info->acquiring_devices->len == 0; + + if (finished) + { + sender_info->sender_state = USB_SENDER_STATE_DEFAULT; + g_clear_pointer (&sender_info->acquiring_devices, g_ptr_array_unref); + } + + g_dbus_method_invocation_return_value_with_unix_fd_list (invocation, + g_variant_new ("(@a(sa{sv})b)", + g_variant_builder_end (&results_builder), + finished), + g_steal_pointer (&fds)); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +/* ReleaseDevice */ + +static XdpOptionKey usb_release_devices_options[] = { +}; + +static gboolean +handle_release_devices (XdpDbusUsb *object, + GDBusMethodInvocation *invocation, + const char * const *arg_devices, + GVariant *arg_options) +{ + g_autoptr(UsbSenderInfo) sender_info = NULL; + g_autoptr(GVariant) options = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder options_builder; + Call *call; + Usb *self; + + self = (Usb *) object; + call = call_from_invocation (invocation); + + g_debug ("[usb] Handling ReleaseDevices"); + + g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); + if (!xdp_filter_options (arg_options, + &options_builder, + usb_release_devices_options, + G_N_ELEMENTS (usb_release_devices_options), + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + return G_DBUS_METHOD_INVOCATION_HANDLED; + } + options = g_variant_builder_end (&options_builder); + + sender_info = usb_sender_info_from_call (self, call); + g_assert (sender_info != NULL); + + for (size_t i = 0; arg_devices && arg_devices[i]; i++) + usb_sender_info_release_device (sender_info, arg_devices[i]); + + xdp_dbus_usb_complete_release_devices (object, invocation); + + return G_DBUS_METHOD_INVOCATION_HANDLED; +} + +static void +usb_iface_init (XdpDbusUsbIface *iface) +{ + iface->handle_create_session = handle_create_session; + iface->handle_enumerate_devices = handle_enumerate_devices; + iface->handle_acquire_devices = handle_acquire_devices; + iface->handle_finish_acquire_devices = handle_finish_acquire_devices; + iface->handle_release_devices = handle_release_devices; +} + +static void +usb_dispose (GObject *object) +{ + Usb *self = (Usb *) object; + + g_clear_pointer (&self->ids_to_devices, g_hash_table_unref); + g_clear_pointer (&self->syspaths_to_ids, g_hash_table_unref); + g_clear_pointer (&self->sessions, g_hash_table_unref); + + g_clear_object (&self->gudev_client); +} + +static void +usb_class_init (UsbClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = usb_dispose; +} + +static void +usb_init (Usb *self) +{ + g_autolist(GUdevDevice) devices = NULL; + const char * const subsystems[] = { + "usb", + NULL, + }; + + g_debug ("[usb] Initializing USB portal"); + + xdp_dbus_usb_set_version (XDP_DBUS_USB (self), 1); + + self->ids_to_devices = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); + self->syspaths_to_ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + self->sessions = g_hash_table_new (g_direct_hash, g_direct_equal); + self->sender_infos = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, usb_sender_info_unref); + + self->gudev_client = g_udev_client_new (subsystems); + g_signal_connect (self->gudev_client, + "uevent", + G_CALLBACK (gudev_client_uevent_cb), + self); + + /* Initialize devices */ + devices = g_udev_client_query_by_subsystem (self->gudev_client, "usb"); + for (GList *l = devices; l; l = l->next) + { + GUdevDevice *device = l->data; + + if (!is_gudev_device_suitable (device)) + continue; + + if (!create_unique_usb_id (self, device, NULL)) + g_assert_not_reached (); + } +} + +GDBusInterfaceSkeleton * +usb_create (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + usb_impl = xdp_dbus_impl_usb_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, + &error); + if (usb_impl == NULL) + { + g_warning ("Failed to create USB proxy: %s", error->message); + return NULL; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (usb_impl), G_MAXINT); + + g_assert (usb_impl != NULL); + g_assert (usb == NULL); + + usb = g_object_new (usb_get_type (), NULL); + + return G_DBUS_INTERFACE_SKELETON (usb); +} + +void +usb_revoke_devices_from_sender (const char *sender) +{ + if (usb && g_hash_table_remove (usb->sender_infos, sender)) + g_debug ("Revoked acquired USB devices from sender %s", sender); +} diff --git a/src/usb.h b/src/usb.h new file mode 100644 index 000000000..2c913a836 --- /dev/null +++ b/src/usb.h @@ -0,0 +1,30 @@ +/* + * Copyright © 2023 GNOME Foundation Inc. + * 2020 Endless OS Foundation LLC + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Georges Basile Stavracas Neto + * Ryan Gonzalez + */ + +#pragma once + +#include + +GDBusInterfaceSkeleton * usb_create (GDBusConnection *connection, + const char *dbus_name); + +void usb_revoke_devices_from_sender (const char *sender); diff --git a/src/xdg-desktop-portal.c b/src/xdg-desktop-portal.c index 519d8df87..1cf713e4d 100644 --- a/src/xdg-desktop-portal.c +++ b/src/xdg-desktop-portal.c @@ -64,6 +64,7 @@ #include "secret.h" #include "settings.h" #include "trash.h" +#include "usb.h" #include "wallpaper.h" static int global_exit_status = 0; @@ -184,6 +185,9 @@ export_portal_implementation (GDBusConnection *connection, static void peer_died_cb (const char *name) { +#ifdef HAVE_GUDEV + usb_revoke_devices_from_sender (name); +#endif close_requests_for_sender (name); close_sessions_for_sender (name); xdp_session_persistence_delete_transient_permissions_for_sender (name); @@ -359,6 +363,13 @@ on_bus_acquired (GDBusConnection *connection, if (implementation != NULL) export_portal_implementation (connection, input_capture_create (connection, implementation->dbus_name)); + +#ifdef HAVE_GUDEV + implementation = find_portal_implementation ("org.freedesktop.impl.portal.Usb"); + if (implementation != NULL) + export_portal_implementation (connection, + usb_create (connection, implementation->dbus_name)); +#endif } static void diff --git a/src/xdp-app-info-flatpak.c b/src/xdp-app-info-flatpak.c index b891f19a2..b592f26ed 100644 --- a/src/xdp-app-info-flatpak.c +++ b/src/xdp-app-info-flatpak.c @@ -1,5 +1,6 @@ /* * Copyright © 2024 Red Hat, Inc + * Copyright © 2024 GNOME Foundation Inc. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -13,6 +14,9 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . + * + * Authors: + * Hubert Figuière */ #include "config.h" @@ -26,6 +30,7 @@ #include #include "xdp-app-info-flatpak-private.h" +#include "xdp-usb-query.h" #define FLATPAK_ENGINE_ID "org.flatpak" @@ -46,6 +51,7 @@ struct _XdpAppInfoFlatpak XdpAppInfo parent; GKeyFile *flatpak_info; + GPtrArray *queries; }; G_DEFINE_FINAL_TYPE (XdpAppInfoFlatpak, xdp_app_info_flatpak, XDP_TYPE_APP_INFO) @@ -234,6 +240,58 @@ xdp_app_info_flatpak_validate_autostart (XdpAppInfo *app_info, return TRUE; } +static const GPtrArray * +xdp_app_info_flaptak_get_usb_queries (XdpAppInfo *app_info) +{ + XdpAppInfoFlatpak *app_info_flatpak = XDP_APP_INFO_FLATPAK (app_info); + + if (!app_info_flatpak->queries) + { + g_autoptr(GPtrArray) usb_queries = NULL; + + usb_queries = g_ptr_array_new_with_free_func ((GDestroyNotify) xdp_usb_query_free); + + g_auto(GStrv) enumerable_devices = NULL; + g_auto(GStrv) hidden_devices = NULL; + + enumerable_devices = g_key_file_get_string_list (app_info_flatpak->flatpak_info, + "USB Devices", + "enumerable-devices", + NULL, NULL); + + for (size_t i = 0; enumerable_devices && enumerable_devices[i] != NULL; i++) + { + g_autoptr(XdpUsbQuery) query = + xdp_usb_query_from_string (XDP_USB_QUERY_TYPE_ENUMERABLE, enumerable_devices[i]); + + if (query) + g_ptr_array_add (usb_queries, g_steal_pointer (&query)); + } + + hidden_devices = g_key_file_get_string_list (app_info_flatpak->flatpak_info, + "USB Devices", + "hidden-devices", + NULL, NULL); + + for (size_t i = 0; hidden_devices && hidden_devices[i] != NULL; i++) + { + g_autoptr(XdpUsbQuery) query = + xdp_usb_query_from_string (XDP_USB_QUERY_TYPE_HIDDEN, hidden_devices[i]); + + if (query) + g_ptr_array_add (usb_queries, g_steal_pointer (&query)); + } + + g_message ("Found %d enumerable and %d hidden for app %s", + enumerable_devices ? g_strv_length (enumerable_devices) : 0, + hidden_devices ? g_strv_length (hidden_devices) : 0, + xdp_app_info_get_id (app_info)); + app_info_flatpak->queries = g_steal_pointer (&usb_queries); + } + + return app_info_flatpak->queries; +} + static gboolean xdp_app_info_flatpak_validate_dynamic_launcher (XdpAppInfo *app_info, GKeyFile *key_file, @@ -300,6 +358,7 @@ xdp_app_info_flatpak_dispose (GObject *object) XdpAppInfoFlatpak *app_info = XDP_APP_INFO_FLATPAK (object); g_clear_pointer (&app_info->flatpak_info, g_key_file_free); + g_clear_pointer (&app_info->queries, g_ptr_array_unref); G_OBJECT_CLASS (xdp_app_info_flatpak_parent_class)->dispose (object); } @@ -314,6 +373,8 @@ xdp_app_info_flatpak_class_init (XdpAppInfoFlatpakClass *klass) app_info_class->remap_path = xdp_app_info_flatpak_remap_path; + app_info_class->get_usb_queries = + xdp_app_info_flaptak_get_usb_queries; app_info_class->validate_autostart = xdp_app_info_flatpak_validate_autostart; app_info_class->validate_dynamic_launcher = diff --git a/src/xdp-app-info-private.h b/src/xdp-app-info-private.h index 3a44218d7..89739b223 100644 --- a/src/xdp-app-info-private.h +++ b/src/xdp-app-info-private.h @@ -28,6 +28,8 @@ struct _XdpAppInfoClass char * (*remap_path) (XdpAppInfo *app_info, const char *path); + const GPtrArray * (*get_usb_queries) (XdpAppInfo *app_info); + gboolean (*validate_autostart) (XdpAppInfo *app_info, GKeyFile *keyfile, const char * const *autostart_exec, diff --git a/src/xdp-app-info.c b/src/xdp-app-info.c index 16f4cf7aa..0af382503 100644 --- a/src/xdp-app-info.c +++ b/src/xdp-app-info.c @@ -1,5 +1,6 @@ /* * Copyright © 2024 Red Hat, Inc + * Copyright © 2024 GNOME Foundation Inc. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -13,6 +14,9 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . + * + * Authors: + * Hubert Figuière */ #include "config.h" @@ -41,6 +45,7 @@ #include "xdp-app-info-snap-private.h" #include "xdp-app-info-host-private.h" #include "xdp-app-info-test-private.h" +#include "xdp-utils.h" #define DBUS_NAME_DBUS "org.freedesktop.DBus" #define DBUS_INTERFACE_DBUS DBUS_NAME_DBUS @@ -531,6 +536,20 @@ xdp_app_info_validate_dynamic_launcher (XdpAppInfo *app_info, error); } +const GPtrArray * +xdp_app_info_get_usb_queries (XdpAppInfo *app_info) +{ + XdpAppInfoPrivate *priv = xdp_app_info_get_instance_private (app_info); + + if (!priv->id || + !XDP_APP_INFO_GET_CLASS (app_info)->get_usb_queries) + { + return NULL; + } + + return XDP_APP_INFO_GET_CLASS (app_info)->get_usb_queries (app_info); +} + static gboolean xdp_connection_get_pid_legacy (GDBusConnection *connection, const char *sender, diff --git a/src/xdp-app-info.h b/src/xdp-app-info.h index 0c6aea7a4..a11add774 100644 --- a/src/xdp-app-info.h +++ b/src/xdp-app-info.h @@ -1,5 +1,6 @@ /* * Copyright © 2024 Red Hat, Inc + * Copyright © 2024 GNOME Foundation Inc. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -13,6 +14,9 @@ * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . + * + * Authors: + * Hubert Figuière */ #pragma once @@ -70,6 +74,8 @@ gboolean xdp_app_info_validate_dynamic_launcher (XdpAppInfo *app_info, GKeyFile *key_file, GError **error); +const GPtrArray * xdp_app_info_get_usb_queries (XdpAppInfo *app_info); + XdpAppInfo * xdp_invocation_lookup_app_info_sync (GDBusMethodInvocation *invocation, GCancellable *cancellable, GError **error); diff --git a/src/xdp-usb-query.c b/src/xdp-usb-query.c new file mode 100644 index 000000000..d5af554a3 --- /dev/null +++ b/src/xdp-usb-query.c @@ -0,0 +1,214 @@ +/* + * Copyright © 2023-2024 GNOME Foundation Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Georges Basile Stavracas Neto + * Hubert Figuière + */ + +#include +#include + +#include "xdp-usb-query.h" + +static void +xdp_usb_rule_free (XdpUsbRule *rule) +{ + g_free (rule); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpUsbRule, xdp_usb_rule_free); + +static gboolean +validate_hex_uint16 (const char *value, + size_t expected_length, + uint16_t *out_value) +{ + size_t len; + char *end; + long n; + + g_assert (value != NULL); + g_assert (expected_length > 0 && expected_length <= 4); + + len = strlen (value); + if (len != expected_length) + return FALSE; + + n = strtol (value, &end, 16); + + if (end - value != len) + return FALSE; + + if (n <= 0 || n > UINT16_MAX) + return FALSE; + + if (out_value) + *out_value = n; + + return TRUE; +} + +static gboolean +parse_all_usb_rule (XdpUsbRule *dest, + GStrv data) +{ + if (g_strv_length (data) != 1) + return FALSE; + + dest->rule_type = XDP_USB_RULE_TYPE_ALL; + return TRUE; +} + +static gboolean +parse_cls_usb_rule (XdpUsbRule *dest, + GStrv data) +{ + const char *subclass; + const char *class; + + if (g_strv_length (data) < 3) + return FALSE; + + class = data[1]; + subclass = data[2]; + + if (!validate_hex_uint16 (class, 2, &dest->d.device_class.class)) + return FALSE; + + if (g_strcmp0 (subclass, "*") == 0) + dest->d.device_class.type = XDP_USB_RULE_CLASS_TYPE_CLASS_ONLY; + else if (validate_hex_uint16 (subclass, 2, &dest->d.device_class.subclass)) + dest->d.device_class.type = XDP_USB_RULE_CLASS_TYPE_CLASS_SUBCLASS; + else + return FALSE; + + dest->rule_type = XDP_USB_RULE_TYPE_CLASS; + return TRUE; +} + +static gboolean +parse_dev_usb_rule (XdpUsbRule *dest, + GStrv data) +{ + if (g_strv_length (data) != 2) + return FALSE; + + if (!validate_hex_uint16 (data[1], 4, &dest->d.product.id)) + return FALSE; + + dest->rule_type = XDP_USB_RULE_TYPE_DEVICE; + return TRUE; +} + +static gboolean +parse_vnd_usb_rule (XdpUsbRule *dest, + GStrv data) +{ + if (g_strv_length (data) != 2) + return FALSE; + + if (!validate_hex_uint16 (data[1], 4, &dest->d.product.id)) + return FALSE; + + dest->rule_type = XDP_USB_RULE_TYPE_VENDOR; + return TRUE; +} + +static const struct { + const char *name; + gboolean (*parse) (XdpUsbRule *dest, + GStrv data); +} rule_parsers[] = { + { "all", parse_all_usb_rule }, + { "cls", parse_cls_usb_rule }, + { "dev", parse_dev_usb_rule }, + { "vnd", parse_vnd_usb_rule }, +}; + +static XdpUsbRule * +xdp_usb_rule_from_string (const char *string) +{ + g_autoptr(XdpUsbRule) usb_rule = NULL; + g_auto(GStrv) split = NULL; + gboolean parsed = FALSE; + + split = g_strsplit (string, ":", 0); + + if (!split || g_strv_length (split) > 3) + return NULL; + + usb_rule = g_new0 (XdpUsbRule, 1); + + for (size_t i = 0; i < G_N_ELEMENTS (rule_parsers); i++) + { + if (g_strcmp0 (rule_parsers[i].name, split[0]) == 0) + { + if (!rule_parsers[i].parse (usb_rule, split)) + return FALSE; + + parsed = TRUE; + break; + } + } + + if (!parsed) + return NULL; + + return g_steal_pointer (&usb_rule); +} + +void +xdp_usb_query_free (XdpUsbQuery *query) +{ + if (!query) + return; + + g_clear_pointer (&query->rules, g_ptr_array_unref); + g_free (query); +} + +XdpUsbQuery * +xdp_usb_query_from_string (XdpUsbQueryType query_type, + const char *string) +{ + g_autoptr(XdpUsbQuery) usb_query = NULL; + g_auto(GStrv) split = NULL; + + split = g_strsplit (string, "+", 0); + if (!split) + return NULL; + + usb_query = g_new0 (XdpUsbQuery, 1); + usb_query->query_type = query_type; + usb_query->rules = g_ptr_array_new_with_free_func ((GDestroyNotify) xdp_usb_rule_free); + + for (size_t i = 0; split[i] != NULL; i++) + { + g_autoptr(XdpUsbRule) usb_rule = NULL; + const char *rule = split[i]; + + usb_rule = xdp_usb_rule_from_string (rule); + if (!usb_rule) + return NULL; + + g_ptr_array_add (usb_query->rules, g_steal_pointer (&usb_rule)); + } + + g_return_val_if_fail (usb_query->rules->len > 0, NULL); + + return g_steal_pointer (&usb_query); +} diff --git a/src/xdp-usb-query.h b/src/xdp-usb-query.h new file mode 100644 index 000000000..62d005029 --- /dev/null +++ b/src/xdp-usb-query.h @@ -0,0 +1,84 @@ +/* + * Copyright © 2023-2024 GNOME Foundation Inc. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: + * Georges Basile Stavracas Neto + * Hubert Figuière + */ + +#include + +#pragma once + +typedef enum +{ + XDP_USB_RULE_TYPE_ALL, + XDP_USB_RULE_TYPE_CLASS, + XDP_USB_RULE_TYPE_DEVICE, + XDP_USB_RULE_TYPE_VENDOR, +} UsbRuleType; + +typedef enum +{ + XDP_USB_RULE_CLASS_TYPE_CLASS_ONLY, + XDP_USB_RULE_CLASS_TYPE_CLASS_SUBCLASS, +} UsbDeviceClassType; + +typedef struct +{ + UsbDeviceClassType type; + uint16_t class; + uint16_t subclass; +} UsbDeviceClass; + +typedef struct +{ + uint16_t id; +} UsbProduct; + +typedef struct +{ + uint16_t id; +} UsbVendor; + +typedef struct +{ + UsbRuleType rule_type; + + union { + UsbDeviceClass device_class; + UsbProduct product; + UsbVendor vendor; + } d; +} XdpUsbRule; + +typedef enum +{ + XDP_USB_QUERY_TYPE_HIDDEN, + XDP_USB_QUERY_TYPE_ENUMERABLE, +} XdpUsbQueryType; + +typedef struct +{ + XdpUsbQueryType query_type; + GPtrArray *rules; +} XdpUsbQuery; + +void xdp_usb_query_free (XdpUsbQuery *query); +XdpUsbQuery *xdp_usb_query_from_string (XdpUsbQueryType query_type, + const char *string); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (XdpUsbQuery, xdp_usb_query_free);