diff --git a/.github/workflows/Containerfile b/.github/workflows/Containerfile
index 5d34ebb80..699f48b42 100644
--- a/.github/workflows/Containerfile
+++ b/.github/workflows/Containerfile
@@ -29,6 +29,7 @@ RUN apt install -y --no-install-recommends \
libgstreamer-plugins-good1.0-dev \
libgeoclue-2-dev \
libglib2.0-dev \
+ libgudev-1.0-dev \
libjson-glib-dev \
libpipewire-0.3-dev \
libsystemd-dev \
@@ -46,7 +47,11 @@ RUN apt install -y --no-install-recommends \
python3-pytest \
python3-pytest-xdist \
python3-dbusmock \
- python3-dbus
+ python3-dbus \
+ libumockdev0 \
+ libumockdev-dev \
+ umockdev \
+ gir1.2-umockdev-1.0
# Install pip
RUN apt install -y --no-install-recommends python3-pip
diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml
index b1f66e95e..206801c1f 100644
--- a/.github/workflows/container.yml
+++ b/.github/workflows/container.yml
@@ -1,5 +1,5 @@
env:
- IMAGE_TAG: 20241024-1
+ IMAGE_TAG: 20241024-3
on:
workflow_call:
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..d72b6c3d8
--- /dev/null
+++ b/data/org.freedesktop.impl.portal.Usb.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/org.freedesktop.portal.Usb.xml b/data/org.freedesktop.portal.Usb.xml
new file mode 100644
index 000000000..74cb3005e
--- /dev/null
+++ b/data/org.freedesktop.portal.Usb.xml
@@ -0,0 +1,243 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 6a571978f..adac2c7a5 100644
--- a/meson.build
+++ b/meson.build
@@ -132,6 +132,8 @@ libportal_dep = dependency(
)
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'))
+umockdev_dep = dependency('umockdev-1.0')
bwrap = find_program('bwrap', required: get_option('sandboxed-image-validation').allowed() or get_option('sandboxed-sound-validation').allowed())
@@ -160,6 +162,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_72'], language: 'c')
build_documentation = false
@@ -218,6 +225,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 d64071f42..64d9bd3d3 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/po/POTFILES.in b/po/POTFILES.in
index 3b7adcc80..d514b2167 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -5,4 +5,5 @@ src/dynamic-launcher.c
src/location.c
src/screenshot.c
src/settings.c
+src/usb.c
src/wallpaper.c
diff --git a/src/meson.build b/src/meson.build
index 9be185c8e..0da816488 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -59,6 +59,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
@@ -123,6 +124,12 @@ if have_geoclue
)
endif
+if have_gudev
+ xdg_desktop_portal_sources += files(
+ 'usb.c',
+ )
+endif
+
common_deps = [
glib_dep,
gio_dep,
@@ -130,6 +137,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/request.c b/src/request.c
index 70353c636..4a6ffeb5e 100644
--- a/src/request.c
+++ b/src/request.c
@@ -254,6 +254,12 @@ request_from_invocation (GDBusMethodInvocation *invocation)
return g_object_get_data (G_OBJECT (invocation), "request");
}
+const char *
+request_get_object_path (Request *request)
+{
+ return request->id;
+}
+
void
request_export (Request *request,
GDBusConnection *connection)
diff --git a/src/request.h b/src/request.h
index 38d6cc25c..2c43fe5d6 100644
--- a/src/request.h
+++ b/src/request.h
@@ -73,6 +73,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (Request, g_object_unref)
void request_init_invocation (GDBusMethodInvocation *invocation, XdpAppInfo *app_info);
Request *request_from_invocation (GDBusMethodInvocation *invocation);
+const char *request_get_object_path (Request *request);
void request_export (Request *request,
GDBusConnection *connection);
void request_unexport (Request *request);
diff --git a/src/usb.c b/src/usb.c
new file mode 100644
index 000000000..a00749a7b
--- /dev/null
+++ b/src/usb.c
@@ -0,0 +1,1571 @@
+/*
+ * 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"
+#define MAX_DEVICES 8
+
+/* TODO:
+ *
+ * AccessDevices()
+ * - Check if backend is returning appropriate device ids
+ * - Check if backend is not increasing permissions
+ * - Save allowed devices in the permission store
+ */
+
+struct _XdpUsb
+{
+ XdpDbusUsbSkeleton parent_instance;
+
+ GHashTable *ids_to_devices;
+ GHashTable *syspaths_to_ids;
+
+ GHashTable *sessions;
+ GHashTable *sender_infos;
+
+ GUdevClient *gudev_client;
+};
+
+#define XDP_TYPE_USB (xdp_usb_get_type ())
+G_DECLARE_FINAL_TYPE (XdpUsb, xdp_usb, XDP, USB, XdpDbusUsbSkeleton)
+
+static void xdp_usb_iface_init (XdpDbusUsbIface *iface);
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (XdpUsb, xdp_usb, XDP_DBUS_TYPE_USB_SKELETON,
+ G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_USB, xdp_usb_iface_init));
+
+struct _XdpUsbSession
+{
+ Session parent;
+
+ GHashTable *available_devices;
+};
+
+#define XDP_TYPE_USB_SESSION (xdp_usb_session_get_type ())
+G_DECLARE_FINAL_TYPE (XdpUsbSession,
+ xdp_usb_session,
+ XDP, USB_SESSION,
+ Session)
+
+G_DEFINE_TYPE (XdpUsbSession, xdp_usb_session, session_get_type ())
+
+typedef struct
+{
+ char *device_id;
+ gboolean writable;
+} UsbDeviceAcquireData;
+
+typedef struct _UsbOwnedDevice
+{
+ gatomicrefcount ref_count;
+
+ char *sender_name;
+ char *device_id;
+ int fd;
+} UsbOwnedDevice;
+
+typedef struct _UsbSenderInfo
+{
+ gatomicrefcount ref_count;
+
+ char *sender_name;
+ XdpAppInfo *app_info;
+
+ GHashTable *pending_devices; /* object_path → GPtrArray */
+
+ GHashTable *owned_devices; /* device id → UsbOwnedDevices */
+} UsbSenderInfo;
+
+static XdpDbusImplUsb *usb_impl;
+static XdpUsb *usb;
+
+static void usb_device_acquire_data_free (UsbDeviceAcquireData *acquire_data);
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (UsbDeviceAcquireData, usb_device_acquire_data_free)
+
+static void usb_owned_device_unref (UsbOwnedDevice *owned_device);
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (UsbOwnedDevice, usb_owned_device_unref)
+
+static void usb_sender_info_unref (UsbSenderInfo *sender_info);
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (UsbSenderInfo, usb_sender_info_unref)
+
+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;
+
+ permission_id = g_string_new ("usb:");
+
+ property = g_udev_device_get_property (device, "ID_VENDOR_ID");
+ if (property)
+ g_string_append (permission_id, property);
+
+ property = g_udev_device_get_property (device, "ID_MODEL_ID");
+ g_string_append_printf (permission_id, "%s%s", "/", property ? property : "");
+
+ property = g_udev_device_get_property (device, "ID_SERIAL");
+ g_string_append_printf (permission_id, "%s%s", "/", property ? property : "");
+
+ return g_string_free (g_steal_pointer (&permission_id), FALSE);
+}
+
+static void
+usb_device_acquire_data_free (UsbDeviceAcquireData *acquire_data)
+{
+ g_return_if_fail (acquire_data != NULL);
+
+ g_clear_pointer (&acquire_data->device_id, g_free);
+ g_clear_pointer (&acquire_data, g_free);
+}
+
+static UsbOwnedDevice *
+usb_owned_device_ref (UsbOwnedDevice *owned_device)
+{
+ g_return_val_if_fail (owned_device != NULL, NULL);
+
+ g_atomic_ref_count_inc (&owned_device->ref_count);
+
+ return owned_device;
+}
+
+static void
+usb_owned_device_unref (UsbOwnedDevice *owned_device)
+{
+ g_return_if_fail (owned_device != NULL);
+
+ if (g_atomic_ref_count_dec (&owned_device->ref_count))
+ {
+ g_clear_fd (&owned_device->fd, NULL);
+ g_clear_pointer (&owned_device->device_id, g_free);
+ g_clear_pointer (&owned_device, g_free);
+ }
+}
+
+static void
+usb_sender_info_unref (UsbSenderInfo *sender_info)
+{
+ g_return_if_fail (sender_info != NULL);
+
+ 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->pending_devices, g_hash_table_destroy);
+ g_clear_pointer (&sender_info, g_free);
+ }
+}
+
+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->owned_devices =
+ g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, (GDestroyNotify) usb_owned_device_unref);
+ sender_info->pending_devices =
+ g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, (GDestroyNotify) g_ptr_array_unref);
+
+ return g_steal_pointer (&sender_info);
+}
+
+static UsbSenderInfo *
+usb_sender_info_from_sender (XdpUsb *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 (XdpUsb *self,
+ Call *call)
+{
+ g_return_val_if_fail (call != NULL, NULL);
+
+ return usb_sender_info_from_sender (self, call->sender, call->app_info);
+}
+
+static UsbSenderInfo *
+usb_sender_info_from_request (XdpUsb *self,
+ Request *request)
+{
+ g_return_val_if_fail (request != NULL, 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 = g_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 && validate_hex_uint16 (vendor_id_str, 4, &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 && validate_hex_uint16 (product_id_str, 4, &device_product_id))
+ device_has_product_id = TRUE;
+
+ device_class_str = g_udev_device_get_sysfs_attr (device, "bDeviceClass");
+ if (device_class_str != NULL && validate_hex_uint16 (device_class_str, 2, &device_class))
+ device_has_class = TRUE;
+
+ device_subclass_str = g_udev_device_get_sysfs_attr (device, "bDeviceSubclass");
+ if (device_subclass_str != NULL && validate_hex_uint16 (device_subclass_str, 2, &device_subclass))
+ device_has_subclass = TRUE;
+
+ queries = xdp_app_info_get_usb_queries (sender_info->app_info);
+ for (size_t i = 0; queries && 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;
+}
+
+static void
+xdp_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
+xdp_usb_session_dispose (GObject *object)
+{
+ XdpUsbSession *usb_session = XDP_USB_SESSION (object);
+
+ g_clear_pointer (&usb_session->available_devices, g_hash_table_destroy);
+}
+
+static void
+xdp_usb_session_class_init (XdpUsbSessionClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ SessionClass *session_class = (SessionClass*) klass;
+
+ object_class->dispose = xdp_usb_session_dispose;
+
+ session_class->close = xdp_usb_session_close;
+}
+
+static void
+xdp_usb_session_init (XdpUsbSession *session)
+{
+ session->available_devices = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+}
+
+static XdpUsbSession *
+xdp_usb_session_new (GDBusConnection *connection,
+ Call *call,
+ GVariant *options,
+ GError **error)
+{
+ Session *session = NULL;
+
+ session = g_initable_new (XDP_TYPE_USB_SESSION,
+ 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 XDP_USB_SESSION (session);
+}
+
+static GVariant *
+gudev_device_to_variant (XdpUsb *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;
+
+ 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,
+ };
+
+ if (!is_gudev_device_suitable (device))
+ return NULL;
+
+ 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_ref_sink (g_variant_dict_end (&device_variant_dict));
+}
+
+/* Register the device and create a unique ID for it */
+static char *
+register_with_unique_usb_id (XdpUsb *self,
+ GUdevDevice *device)
+{
+ 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));
+
+ return g_steal_pointer (&id);
+}
+
+static void
+handle_session_event (XdpUsb *self,
+ XdpUsbSession *usb_session,
+ GUdevDevice *device,
+ const char *id,
+ const char *action,
+ gboolean removing)
+{
+ 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)))
+ return;
+
+ 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, 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));
+}
+
+static void
+gudev_client_uevent_cb (GUdevClient *client,
+ const char *action,
+ GUdevDevice *device,
+ XdpUsb *self)
+{
+ static const char *supported_actions[] = {
+ "add",
+ "change",
+ "remove",
+ NULL,
+ };
+
+ g_autofree char *id = NULL;
+ GHashTableIter iter;
+ XdpUsbSession *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"))
+ {
+ id = register_with_unique_usb_id (self, device);
+ }
+ 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))
+ handle_session_event (self, usb_session, device, id, action, removing);
+
+ 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");
+ }
+}
+
+static void
+send_initial_device_list (XdpUsb *self,
+ XdpUsbSession *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;
+ g_autoptr(GVariant) events = NULL;
+ GHashTableIter iter;
+ GUdevDevice *device;
+ const char *id;
+ gboolean has_devices = FALSE;
+
+ g_debug ("[usb] Appending devices to CreateSession response");
+
+ g_variant_builder_init (&devices_builder, G_VARIANT_TYPE ("(oa(ssa{sv}))"));
+ g_variant_builder_add (&devices_builder, "o", session->id);
+ g_variant_builder_open (&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);
+ /* NULL mean the device isn't suitable */
+ if (device_variant == NULL)
+ continue;
+
+ g_variant_builder_add (&devices_builder, "(ss@a{sv})", "add", id, device_variant);
+
+ g_hash_table_add (usb_session->available_devices, g_strdup (id));
+
+ has_devices = TRUE;
+ }
+
+ g_variant_builder_close (&devices_builder);
+ events = g_variant_ref_sink (g_variant_builder_end (&devices_builder));
+
+ if (!has_devices)
+ return;
+
+ g_dbus_connection_emit_signal (session->connection,
+ session->sender,
+ "/org/freedesktop/portal/desktop",
+ "org.freedesktop.portal.Usb",
+ "DeviceEvents",
+ events,
+ 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;
+ XdpUsbSession *usb_session;
+ Permission permission;
+ Session *session;
+ Call *call;
+ XdpUsb *self;
+
+ static const XdpOptionKey usb_create_session_options[] = {
+ { "session_handle_token", G_VARIANT_TYPE_STRING, NULL },
+ };
+
+ self = XDP_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_ref_sink (g_variant_builder_end (&options_builder));
+
+ connection = g_dbus_method_invocation_get_connection (invocation);
+ usb_session = xdp_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;
+}
+
+static GVariant *
+list_permitted_devices (XdpUsb *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_autoptr(GVariant) device_variant = gudev_device_to_variant (self, sender_info, device);
+ if (device_variant == NULL)
+ continue;
+
+ g_variant_builder_add (&builder, "(s@a{sv})", id, device_variant);
+ }
+ }
+
+ 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;
+ XdpUsb *self;
+
+ static const XdpOptionKey usb_enumerate_devices_options[] = {
+ };
+
+ self = XDP_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_ref_sink (g_variant_builder_end (&options_builder));
+
+ devices = list_permitted_devices(self, call);
+
+ xdp_dbus_usb_complete_enumerate_devices (object, invocation, devices);
+
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+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);
+
+ g_print ("got request back %s\n", request_get_object_path (request));
+
+ response = XDG_DESKTOP_PORTAL_RESPONSE_OTHER;
+ sender_info = usb_sender_info_from_request (usb, request);
+
+ g_assert (sender_info != NULL);
+
+ 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_autoptr(GPtrArray) pending_devices =
+ g_ptr_array_new_full (g_variant_iter_n_children (devices_iter),
+ (GDestroyNotify) 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 (pending_devices, g_steal_pointer (&access_data));
+
+ usb_sender_info_set_device_permission (sender_info, device, PERMISSION_YES);
+
+ g_clear_pointer (&options, g_variant_unref);
+ }
+ g_hash_table_insert (sender_info->pending_devices,
+ g_strdup (request_get_object_path (request)),
+ g_steal_pointer (&pending_devices));
+ }
+ else if (response == XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED)
+ {
+ g_hash_table_remove (sender_info->pending_devices,
+ request_get_object_path (request));
+ }
+
+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 (XdpUsb *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;
+
+ static const XdpOptionKey usb_device_options[] = {
+ { "writable", G_VARIANT_TYPE_BOOLEAN, NULL },
+ };
+
+ 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,
+ device_variant,
+ g_variant_dict_end (&device_options_dict));
+ }
+
+ *out_filtered_devices =
+ g_variant_ref_sink (g_variant_builder_end (&filtered_devices_builder));
+ return TRUE;
+}
+
+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;
+ XdpUsb *self;
+
+ static const XdpOptionKey usb_acquire_devices_options[] = {
+ };
+
+ self = XDP_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_acquire_devices_options,
+ G_N_ELEMENTS (usb_acquire_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);
+
+ /* 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));
+
+ xdp_dbus_impl_usb_call_acquire_devices (usb_impl,
+ request->id,
+ arg_parent_window,
+ xdp_app_info_get_id (request->app_info),
+ filtered_devices,
+ 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;
+}
+
+static gboolean
+handle_finish_acquire_devices (XdpDbusUsb *object,
+ GDBusMethodInvocation *invocation,
+ const char *object_path,
+ 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;
+ GPtrArray *pending_devices = NULL;
+ Call *call;
+ XdpUsb *self;
+
+ g_print ("object path: %s\n", object_path);
+
+ self = XDP_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 */
+ g_hash_table_remove (sender_info->pending_devices, object_path);
+
+ 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;
+ }
+
+ pending_devices = g_hash_table_lookup (sender_info->pending_devices, object_path);
+ if (pending_devices == NULL)
+ {
+ /* If the request was cancelled in some way. This would happen by calling
+ * FinishAcquireDevices after the user denied the permission.
+ */
+ g_hash_table_remove (sender_info->pending_devices, object_path);
+
+ 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 (pending_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 &&
+ pending_devices->len > 0)
+ {
+ g_autoptr(UsbDeviceAcquireData) access_data = NULL;
+ g_autoptr(GError) error = NULL;
+ GVariantDict dict;
+ UsbOwnedDevice *owned_device = NULL;
+ g_autofd int fd = -1;
+ int fd_index;
+
+ g_variant_dict_init (&dict, NULL);
+
+ access_data = g_ptr_array_steal_index (pending_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,
+ g_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 = pending_devices->len == 0;
+
+ if (finished)
+ {
+ g_hash_table_remove (sender_info->pending_devices, object_path);
+ }
+
+ 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),
+ fds);
+
+ return G_DBUS_METHOD_INVOCATION_HANDLED;
+}
+
+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;
+ XdpUsb *self;
+
+ static const XdpOptionKey usb_release_devices_options[] = {
+ };
+
+ self = XDP_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_ref_sink (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
+xdp_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
+xdp_usb_dispose (GObject *object)
+{
+ XdpUsb *self = XDP_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
+xdp_usb_class_init (XdpUsbClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = xdp_usb_dispose;
+}
+
+static void
+xdp_usb_init (XdpUsb *self)
+{
+ g_autolist(GUdevDevice) devices = NULL;
+ static 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, (GDestroyNotify) 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)
+ {
+ g_autofree char *id = NULL;
+ GUdevDevice *device = l->data;
+
+ if (!is_gudev_device_suitable (device))
+ continue;
+
+ id = register_with_unique_usb_id (self, device);
+ }
+}
+
+static void
+peer_died_cb (const char *sender)
+{
+ if (usb && g_hash_table_remove (usb->sender_infos, sender))
+ g_debug ("Removed sender %s", sender);
+}
+
+GDBusInterfaceSkeleton *
+xdp_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;
+ }
+
+ xdp_connection_track_name_owners (connection, peer_died_cb);
+
+ 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 (xdp_usb_get_type (), NULL);
+
+ return G_DBUS_INTERFACE_SKELETON (usb);
+}
diff --git a/src/usb.h b/src/usb.h
new file mode 100644
index 000000000..636382895
--- /dev/null
+++ b/src/usb.h
@@ -0,0 +1,28 @@
+/*
+ * 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 * xdp_usb_create (GDBusConnection *connection,
+ const char *dbus_name);
diff --git a/src/xdg-desktop-portal.c b/src/xdg-desktop-portal.c
index eab3d6d5e..f71637fff 100644
--- a/src/xdg-desktop-portal.c
+++ b/src/xdg-desktop-portal.c
@@ -67,6 +67,7 @@
#include "secret.h"
#include "settings.h"
#include "trash.h"
+#include "usb.h"
#include "wallpaper.h"
static int global_exit_status = 0;
@@ -362,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,
+ xdp_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 41594524f..771887a4a 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.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
@@ -15,6 +16,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"
@@ -28,6 +32,7 @@
#include
#include "xdp-app-info-flatpak-private.h"
+#include "xdp-usb-query.h"
#define FLATPAK_ENGINE_ID "org.flatpak"
@@ -48,6 +53,7 @@ struct _XdpAppInfoFlatpak
XdpAppInfo parent;
GKeyFile *flatpak_info;
+ GPtrArray *queries;
};
G_DEFINE_FINAL_TYPE (XdpAppInfoFlatpak, xdp_app_info_flatpak, XDP_TYPE_APP_INFO)
@@ -364,6 +370,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,
@@ -430,6 +488,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);
}
@@ -444,6 +503,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-host.c b/src/xdp-app-info-host.c
index 78f688f40..9b7a1f80d 100644
--- a/src/xdp-app-info-host.c
+++ b/src/xdp-app-info-host.c
@@ -25,14 +25,25 @@
#endif
#include "xdp-app-info-host-private.h"
+#include "xdp-usb-query.h"
struct _XdpAppInfoHost
{
XdpAppInfo parent;
+
+ GPtrArray *usb_queries;
};
G_DEFINE_FINAL_TYPE (XdpAppInfoHost, xdp_app_info_host, XDP_TYPE_APP_INFO)
+static const GPtrArray *
+xdp_app_info_host_get_usb_queries (XdpAppInfo *app_info)
+{
+ XdpAppInfoHost *app_info_host = XDP_APP_INFO_HOST (app_info);
+
+ return app_info_host->usb_queries;
+}
+
gboolean
xdp_app_info_host_is_valid_sub_app_id (XdpAppInfo *app_info,
const char *sub_app_id)
@@ -58,12 +69,26 @@ xdp_app_info_host_validate_dynamic_launcher (XdpAppInfo *app_info,
return TRUE;
}
+static void
+xdp_app_info_host_dispose (GObject *object)
+{
+ XdpAppInfoHost *app_info = XDP_APP_INFO_HOST (object);
+
+ g_clear_pointer (&app_info->usb_queries, g_ptr_array_unref);
+
+ G_OBJECT_CLASS (xdp_app_info_host_parent_class)->dispose (object);
+}
static void
xdp_app_info_host_class_init (XdpAppInfoHostClass *klass)
{
XdpAppInfoClass *app_info_class = XDP_APP_INFO_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = xdp_app_info_host_dispose;
+
+ app_info_class->get_usb_queries =
+ xdp_app_info_host_get_usb_queries;
app_info_class->validate_autostart =
xdp_app_info_host_validate_autostart;
app_info_class->validate_dynamic_launcher =
@@ -73,6 +98,14 @@ xdp_app_info_host_class_init (XdpAppInfoHostClass *klass)
static void
xdp_app_info_host_init (XdpAppInfoHost *app_info_host)
{
+ g_autoptr(XdpUsbQuery) query = NULL;
+
+ app_info_host->usb_queries =
+ g_ptr_array_new_with_free_func ((GDestroyNotify) xdp_usb_query_free);
+
+ query = xdp_usb_query_from_string (XDP_USB_QUERY_TYPE_ENUMERABLE, "all");
+ if (query)
+ g_ptr_array_add (app_info_host->usb_queries, g_steal_pointer (&query));
}
#ifdef HAVE_LIBSYSTEMD
diff --git a/src/xdp-app-info-private.h b/src/xdp-app-info-private.h
index fb68dbd06..0b5b06081 100644
--- a/src/xdp-app-info-private.h
+++ b/src/xdp-app-info-private.h
@@ -33,6 +33,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-test-private.h b/src/xdp-app-info-test-private.h
index 1cc11e008..d323323ec 100644
--- a/src/xdp-app-info-test-private.h
+++ b/src/xdp-app-info-test-private.h
@@ -32,4 +32,5 @@ G_DECLARE_FINAL_TYPE (XdpAppInfoTest,
XDP, APP_INFO_TEST,
XdpAppInfo)
-XdpAppInfo * xdp_app_info_test_new (const char *app_id);
+XdpAppInfo * xdp_app_info_test_new (const char *app_id,
+ const char *usb_queries_str);
diff --git a/src/xdp-app-info-test.c b/src/xdp-app-info-test.c
index 1b27e3b52..f7e1242e1 100644
--- a/src/xdp-app-info-test.c
+++ b/src/xdp-app-info-test.c
@@ -21,9 +21,13 @@
#include "xdp-app-info-test-private.h"
+#include "xdp-usb-query.h"
+
struct _XdpAppInfoTest
{
XdpAppInfo parent;
+
+ GPtrArray *usb_queries;
};
G_DEFINE_FINAL_TYPE (XdpAppInfoTest, xdp_app_info_test, XDP_TYPE_APP_INFO)
@@ -46,15 +50,38 @@ xdp_app_info_test_validate_dynamic_launcher (XdpAppInfo *app_info,
return TRUE;
}
+static const GPtrArray *
+xdp_app_info_test_get_usb_queries (XdpAppInfo *app_info)
+{
+ XdpAppInfoTest *app_info_test = XDP_APP_INFO_TEST (app_info);
+
+ return app_info_test->usb_queries;
+}
+
+static void
+xdp_app_info_test_dispose (GObject *object)
+{
+ XdpAppInfoTest *app_info = XDP_APP_INFO_TEST (object);
+
+ g_clear_pointer (&app_info->usb_queries, g_ptr_array_unref);
+
+ G_OBJECT_CLASS (xdp_app_info_test_parent_class)->dispose (object);
+}
+
static void
xdp_app_info_test_class_init (XdpAppInfoTestClass *klass)
{
XdpAppInfoClass *app_info_class = XDP_APP_INFO_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = xdp_app_info_test_dispose;
app_info_class->validate_autostart =
xdp_app_info_test_validate_autostart;
app_info_class->validate_dynamic_launcher =
xdp_app_info_test_validate_dynamic_launcher;
+ app_info_class->get_usb_queries =
+ xdp_app_info_test_get_usb_queries;
}
static void
@@ -62,8 +89,38 @@ xdp_app_info_test_init (XdpAppInfoTest *app_info_test)
{
}
+static GPtrArray *
+parse_usb_queries_string (const char *usb_queries_str)
+{
+ g_autoptr(GPtrArray) usb_queries = NULL;
+ g_auto(GStrv) queries_strs = NULL;
+
+ if (!usb_queries_str)
+ return NULL;
+
+ usb_queries =
+ g_ptr_array_new_with_free_func ((GDestroyNotify) xdp_usb_query_free);
+
+ queries_strs = g_strsplit (usb_queries_str, ";", 0);
+ for (size_t i = 0; queries_strs[i] != NULL; i++)
+ {
+ g_autoptr(XdpUsbQuery) query =
+ xdp_usb_query_from_string (XDP_USB_QUERY_TYPE_ENUMERABLE,
+ queries_strs[i]);
+
+ if (query)
+ g_ptr_array_add (usb_queries, g_steal_pointer (&query));
+ }
+
+ if (usb_queries->len == 0)
+ return NULL;
+
+ return g_steal_pointer (&usb_queries);
+}
+
XdpAppInfo *
-xdp_app_info_test_new (const char *app_id)
+xdp_app_info_test_new (const char *app_id,
+ const char *usb_queries_str)
{
g_autoptr (XdpAppInfoTest) app_info_test = NULL;
@@ -73,5 +130,7 @@ xdp_app_info_test_new (const char *app_id)
-1, NULL,
TRUE, TRUE, TRUE);
+ app_info_test->usb_queries = parse_usb_queries_string (usb_queries_str);
+
return XDP_APP_INFO (g_steal_pointer (&app_info_test));
}
diff --git a/src/xdp-app-info.c b/src/xdp-app-info.c
index e6799ec48..76b803b8c 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.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
@@ -15,6 +16,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"
@@ -43,6 +47,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
@@ -547,6 +552,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,
@@ -732,6 +751,7 @@ xdp_connection_lookup_app_info_sync (GDBusConnection *connection,
g_autofd int pidfd = -1;
uint32_t pid;
const char *test_override_app_id;
+ const char *test_override_usb_queries;
g_autoptr(GError) local_error = NULL;
app_info = cache_lookup_app_info_by_sender (sender);
@@ -742,8 +762,12 @@ xdp_connection_lookup_app_info_sync (GDBusConnection *connection,
return NULL;
test_override_app_id = g_getenv ("XDG_DESKTOP_PORTAL_TEST_APP_ID");
+ test_override_usb_queries = g_getenv ("XDG_DESKTOP_PORTAL_TEST_USB_QUERIES");
if (test_override_app_id)
- app_info = xdp_app_info_test_new (test_override_app_id);
+ {
+ app_info = xdp_app_info_test_new (test_override_app_id,
+ test_override_usb_queries);
+ }
if (app_info == NULL)
app_info = xdp_app_info_flatpak_new (pid, pidfd, &local_error);
diff --git a/src/xdp-app-info.h b/src/xdp-app-info.h
index 1b0172b19..35f44e733 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.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
@@ -15,6 +16,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
@@ -75,6 +79,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..7483a1294
--- /dev/null
+++ b/src/xdp-usb-query.c
@@ -0,0 +1,213 @@
+/*
+ * 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);
+
+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)
+{
+ g_return_if_fail (query != NULL);
+
+ 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..250cb7724
--- /dev/null
+++ b/src/xdp-usb-query.h
@@ -0,0 +1,89 @@
+/*
+ * 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);
+
+gboolean
+validate_hex_uint16 (const char *value,
+ size_t expected_length,
+ uint16_t *out_value);
diff --git a/src/xdp-utils.c b/src/xdp-utils.c
index ba09fbeb5..bd183e09f 100644
--- a/src/xdp-utils.c
+++ b/src/xdp-utils.c
@@ -162,7 +162,7 @@ xdp_connection_track_name_owners (GDBusConnection *connection,
gboolean
xdp_filter_options (GVariant *options,
GVariantBuilder *filtered,
- XdpOptionKey *supported_options,
+ const XdpOptionKey *supported_options,
int n_supported_options,
GError **error)
{
diff --git a/src/xdp-utils.h b/src/xdp-utils.h
index b0ef28200..59a18d67c 100644
--- a/src/xdp-utils.h
+++ b/src/xdp-utils.h
@@ -81,7 +81,7 @@ typedef struct {
gboolean xdp_filter_options (GVariant *options_in,
GVariantBuilder *options_out,
- XdpOptionKey *supported_options,
+ const XdpOptionKey *supported_options,
int n_supported_options,
GError **error);
diff --git a/tests/__init__.py b/tests/__init__.py
index cb306a886..fc8d0c047 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -338,6 +338,7 @@ def __init__(
dbus_test_case,
portal_name: str,
app_id: str = "org.example.App",
+ usb_queries: Optional[str] = None,
umockdev=None,
):
self.dbus_test_case = dbus_test_case
@@ -347,6 +348,7 @@ def __init__(
self.dbus_monitor = None
self.portal_interfaces: Dict[str, dbus.Interface] = {}
self.app_id = app_id
+ self.usb_queries = usb_queries
self.busses = {dbusmock.BusType.SYSTEM: {}, dbusmock.BusType.SESSION: {}}
self.umockdev = umockdev
@@ -458,6 +460,9 @@ def start_xdp(self):
env["XDG_CURRENT_DESKTOP"] = "test"
env["XDG_DESKTOP_PORTAL_TEST_APP_ID"] = self.app_id
+ if self.usb_queries:
+ env["XDG_DESKTOP_PORTAL_TEST_USB_QUERIES"] = self.usb_queries
+
if self.umockdev:
env["UMOCKDEV_DIR"] = self.umockdev.get_root_dir()
diff --git a/tests/conftest.py b/tests/conftest.py
index 76af3ce70..283f44426 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -133,6 +133,15 @@ def app_id():
return "org.example.App"
+@pytest.fixture
+def usb_queries():
+ """
+ Default fixture providing the usb queries the connecting process can
+ enumerate
+ """
+ return None
+
+
@pytest.fixture
def umockdev():
"""
@@ -143,12 +152,18 @@ def umockdev():
@pytest.fixture
def portal_mock(
- dbus_test_case, portal_name, required_templates, template_params, app_id, umockdev
+ dbus_test_case,
+ portal_name,
+ required_templates,
+ template_params,
+ app_id,
+ usb_queries,
+ umockdev,
) -> PortalMock:
"""
Fixture yielding a PortalMock object with the impl started, if applicable.
"""
- pmock = PortalMock(dbus_test_case, portal_name, app_id, umockdev)
+ pmock = PortalMock(dbus_test_case, portal_name, app_id, usb_queries, umockdev)
for template, params in required_templates.items():
params = template_params.get(template, params)
diff --git a/tests/meson.build b/tests/meson.build
index ed214a124..8c6888a5e 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -277,7 +277,11 @@ python = pymod.find_installation(
required: get_option('pytest'),
)
-enable_pytest = pytest.found() and python.found() and python.language_version().version_compare('>=3.9')
+enable_pytest = \
+ pytest.found() and \
+ python.found() and \
+ python.language_version().version_compare('>=3.9') and \
+ umockdev_dep.found()
if enable_pytest
subdir('templates')
@@ -304,6 +308,7 @@ if enable_pytest
'test_location.py',
'test_remotedesktop.py',
'test_trash.py',
+ 'test_usb.py',
]
foreach pytest_file : pytest_files
configure_file(
diff --git a/tests/portals/meson.build b/tests/portals/meson.build
index 96b932680..7c31aa8e3 100644
--- a/tests/portals/meson.build
+++ b/tests/portals/meson.build
@@ -15,6 +15,7 @@ test_portals = [
'org.freedesktop.impl.portal.RemoteDesktop',
'org.freedesktop.impl.portal.Screenshot',
'org.freedesktop.impl.portal.Settings',
+ 'org.freedesktop.impl.portal.Usb',
'org.freedesktop.impl.portal.Wallpaper',
]
diff --git a/tests/templates/meson.build b/tests/templates/meson.build
index 315832011..24e74dba7 100644
--- a/tests/templates/meson.build
+++ b/tests/templates/meson.build
@@ -6,6 +6,7 @@ template_files = [
'globalshortcuts.py',
'inputcapture.py',
'remotedesktop.py',
+ 'usb.py',
]
foreach template_file : template_files
configure_file(
diff --git a/tests/templates/usb.py b/tests/templates/usb.py
new file mode 100644
index 000000000..3b8cb889b
--- /dev/null
+++ b/tests/templates/usb.py
@@ -0,0 +1,129 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is formatted with Python Black
+
+from tests.templates import Response, init_template_logger, ImplRequest
+import dbus
+import dbus.service
+from dbusmock import MOCK_IFACE
+
+from gi.repository import GLib
+
+
+BUS_NAME = "org.freedesktop.impl.portal.Test"
+MAIN_OBJ = "/org/freedesktop/portal/desktop"
+SYSTEM_BUS = False
+MAIN_IFACE = "org.freedesktop.impl.portal.Usb"
+VERSION = 1
+
+
+logger = init_template_logger(__name__)
+
+
+def load(mock, parameters={}):
+ logger.debug(f"Loading parameters: {parameters}")
+
+ mock.delay: int = parameters.get("delay", 200)
+ mock.response: int = parameters.get("response", 0)
+ mock.filters = parameters.get("filters", {})
+ mock.AddProperties(
+ MAIN_IFACE,
+ dbus.Dictionary(
+ {
+ "version": dbus.UInt32(parameters.get("version", VERSION)),
+ }
+ ),
+ )
+
+
+@dbus.service.method(
+ MAIN_IFACE,
+ in_signature="ossa(sa{sv}a{sv})a{sv}",
+ out_signature="ua{sv}",
+ async_callbacks=("cb_success", "cb_error"),
+)
+def AcquireDevices(
+ self,
+ handle,
+ parent_window,
+ app_id,
+ devices,
+ options,
+ cb_success,
+ cb_error,
+):
+ try:
+ logger.debug(
+ f"AcquireDevices({handle}, {parent_window}, {app_id}, {devices}, {options})"
+ )
+
+ # no options supported
+ assert not options
+ devices_out = []
+
+ for device in devices:
+ (id, info, access_options) = device
+ props = info["properties"]
+
+ allows_writable = self.filters.get("writable", True)
+ needs_writable = access_options.get("writable", False)
+ if needs_writable and not allows_writable:
+ logger.debug(f"Skipping device {id} because it requires writable")
+ continue
+
+ needs_vendor = self.filters.get("vendor", None)
+ needs_vendor = int(needs_vendor, 16) if needs_vendor else None
+
+ vendor = props.get("ID_VENDOR_ID", None)
+ vendor = int(vendor, 16) if vendor else None
+
+ if needs_vendor is not None and needs_vendor != vendor:
+ logger.debug(
+ f"Skipping device {id} because it does not belong to vendor {needs_vendor:02x}"
+ )
+ continue
+
+ needs_model = self.filters.get("model", None)
+ needs_model = int(needs_model, 16) if needs_model else None
+
+ model = props.get("ID_MODEL_ID", None)
+ model = int(model, 16) if model else None
+
+ if needs_model is not None and needs_model != model:
+ logger.debug(
+ f"Skipping device {id} because it is not a model {needs_model:02x}"
+ )
+ continue
+
+ devices_out.append(
+ dbus.Struct([id, access_options], signature="sa{sv}", variant_level=1)
+ )
+
+ response = Response(
+ self.response,
+ {"devices": dbus.Array(devices_out, signature="(sa{sv})", variant_level=1)},
+ )
+ request = ImplRequest(self, BUS_NAME, handle)
+ request.export()
+
+ def reply():
+ logger.debug(f"AcquireDevices with response {response}")
+ cb_success(response.response, response.results)
+
+ logger.debug(f"scheduling delay of {self.delay}")
+ GLib.timeout_add(self.delay, reply)
+
+ except Exception as e:
+ logger.critical(e)
+ cb_error(e)
+
+
+@dbus.service.method(
+ MOCK_IFACE,
+ in_signature="a{sv}",
+ out_signature="",
+)
+def SetSelectionFilters(self, filters):
+ logger.debug(f"SetSelectionFilters({filters})")
+
+ self.filters = filters
diff --git a/tests/test_usb.py b/tests/test_usb.py
new file mode 100644
index 000000000..6bfa67deb
--- /dev/null
+++ b/tests/test_usb.py
@@ -0,0 +1,395 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is formatted with Python Black
+
+from tests import Session
+from gi.repository import GLib
+
+import pytest
+import os
+import gi
+
+gi.require_version("UMockdev", "1.0")
+from gi.repository import UMockdev # noqa E402
+
+
+@pytest.fixture
+def portal_name():
+ return "Usb"
+
+
+@pytest.fixture
+def umockdev():
+ return UMockdev.Testbed.new()
+
+
+class TestUsb:
+ _num_devices = 0
+
+ def generate_device(
+ self, testbed, vendor, vendor_name, product, product_name, serial
+ ):
+ n = self._num_devices
+ self._num_devices += 1
+
+ testbed.add_from_string(f"""P: /devices/usb{n}
+N: bus/usb/001/{n:03d}
+E: BUSNUM=001
+E: DEVNUM={n:03d}
+E: DEVNAME=/dev/bus/usb/001/{n:03d}
+E: DEVTYPE=usb_device
+E: DRIVER=usb
+E: ID_BUS=usb
+E: ID_MODEL={product_name}
+E: ID_MODEL_ID={product}
+E: ID_REVISION=0002
+E: ID_SERIAL={vendor_name}_{product_name}_{serial}
+E: ID_SERIAL_SHORT={serial}
+E: ID_VENDOR={vendor_name}
+E: ID_VENDOR_ID={vendor}
+E: SUBSYSTEM=usb
+A: idProduct={product}
+A: idVendor={vendor}
+""")
+
+ return f"/sys/devices/usb{n}"
+
+ def test_version(self, portal_mock):
+ portal_mock.check_version(1)
+
+ def test_create_close_session(self, portal_mock, app_id):
+ usb_intf = portal_mock.get_dbus_interface()
+
+ session = Session(
+ portal_mock.dbus_con,
+ usb_intf.CreateSession({"session_handle_token": "session_token0"}),
+ )
+
+ session.close()
+
+ def test_empty_initial_devices(self, portal_mock, app_id):
+ device_events_signal_received = False
+
+ usb_intf = portal_mock.get_dbus_interface()
+
+ Session(
+ portal_mock.dbus_con,
+ usb_intf.CreateSession({"session_handle_token": "session_token0"}),
+ )
+
+ def cb_device_events(session_handle, events):
+ nonlocal device_events_signal_received
+ device_events_signal_received = True
+
+ usb_intf.connect_to_signal("DeviceEvents", cb_device_events)
+
+ mainloop = GLib.MainLoop()
+ GLib.timeout_add(300, mainloop.quit)
+ mainloop.run()
+
+ assert not device_events_signal_received
+
+ @pytest.mark.parametrize("usb_queries", ["vnd:04a9", None])
+ def test_initial_devices(self, portal_mock, app_id, usb_queries):
+ device_events_signal_received = False
+ devices_received = 0
+
+ self.generate_device(
+ portal_mock.umockdev,
+ "04a9",
+ "Canon_Inc.",
+ "31c0",
+ "Canon_Digital_Camera",
+ "C767F1C714174C309255F70E4A7B2EE2",
+ )
+
+ mainloop = GLib.MainLoop()
+ GLib.timeout_add(300, mainloop.quit)
+ mainloop.run()
+
+ usb_intf = portal_mock.get_dbus_interface()
+
+ session = Session(
+ portal_mock.dbus_con,
+ usb_intf.CreateSession({"session_handle_token": "session_token0"}),
+ )
+
+ def cb_device_events(session_handle, events):
+ nonlocal device_events_signal_received
+ nonlocal devices_received
+ assert session.handle == session_handle
+
+ for action, id, device in events:
+ assert action == "add"
+ devices_received += 1
+
+ device_events_signal_received = True
+
+ usb_intf.connect_to_signal("DeviceEvents", cb_device_events)
+
+ mainloop = GLib.MainLoop()
+ GLib.timeout_add(300, mainloop.quit)
+ mainloop.run()
+
+ if usb_queries is None:
+ assert not device_events_signal_received
+ assert devices_received == 0
+ else:
+ assert device_events_signal_received
+ assert devices_received == 1
+
+ @pytest.mark.parametrize("usb_queries", ["vnd:04a9", None])
+ def test_device_add(self, portal_mock, app_id, usb_queries):
+ device_events_signal_received = False
+ devices_received = 0
+ device = None
+
+ usb_intf = portal_mock.get_dbus_interface()
+
+ session = Session(
+ portal_mock.dbus_con,
+ usb_intf.CreateSession({"session_handle_token": "session_token0"}),
+ )
+
+ def cb_device_events(session_handle, events):
+ nonlocal device_events_signal_received
+ nonlocal devices_received
+ nonlocal device
+ assert session.handle == session_handle
+
+ for action, _, dev in events:
+ assert action == "add"
+ device = dev
+ devices_received += 1
+
+ device_events_signal_received = True
+
+ usb_intf.connect_to_signal("DeviceEvents", cb_device_events)
+
+ mainloop = GLib.MainLoop()
+ GLib.timeout_add(300, mainloop.quit)
+ mainloop.run()
+
+ assert not device_events_signal_received
+
+ self.generate_device(
+ portal_mock.umockdev,
+ "04a9",
+ "Canon_Inc.",
+ "31c0",
+ "Canon_Digital_Camera",
+ "C767F1C714174C309255F70E4A7B2EE2",
+ )
+
+ mainloop = GLib.MainLoop()
+ GLib.timeout_add(300, mainloop.quit)
+ mainloop.run()
+
+ if usb_queries is None:
+ assert not device_events_signal_received
+ assert devices_received == 0
+ else:
+ assert device_events_signal_received
+ assert devices_received == 1
+
+ assert device
+ assert device["readable"]
+ assert device["writable"]
+ assert device["device-file"] == "/dev/bus/usb/001/000"
+ assert device["properties"]["ID_VENDOR_ID"] == "04a9"
+ assert device["properties"]["ID_MODEL_ID"] == "31c0"
+ assert (
+ device["properties"]["ID_SERIAL"]
+ == "Canon_Inc._Canon_Digital_Camera_C767F1C714174C309255F70E4A7B2EE2"
+ )
+
+ @pytest.mark.parametrize("usb_queries", ["vnd:04a9;vnd:04aa", None])
+ def test_device_remove(self, portal_mock, app_id, usb_queries):
+ device_events_signal_count = 0
+ devices_received = 0
+ devices_removed = 0
+
+ dev_path = self.generate_device(
+ portal_mock.umockdev,
+ "04aa",
+ "Someone Else.",
+ "31c0",
+ "SomeProduct",
+ "00001",
+ )
+
+ dev_path = self.generate_device(
+ portal_mock.umockdev,
+ "04a9",
+ "Canon_Inc.",
+ "31c0",
+ "Canon_Digital_Camera",
+ "C767F1C714174C309255F70E4A7B2EE2",
+ )
+
+ usb_intf = portal_mock.get_dbus_interface()
+
+ session = Session(
+ portal_mock.dbus_con,
+ usb_intf.CreateSession({"session_handle_token": "session_token0"}),
+ )
+
+ def cb_device_events(session_handle, events):
+ nonlocal device_events_signal_count
+ nonlocal devices_received
+ nonlocal devices_removed
+ assert session.handle == session_handle
+
+ print("events ", events)
+ for action, id, device in events:
+ print("device event ", action)
+ if action == "add":
+ devices_received += 1
+ elif action == "remove":
+ devices_removed += 1
+ else:
+ assert False
+ device_events_signal_count += 1
+ print("count ", device_events_signal_count)
+ print("count recv", devices_received)
+
+ usb_intf.connect_to_signal("DeviceEvents", cb_device_events)
+
+ mainloop = GLib.MainLoop()
+ GLib.timeout_add(1000, mainloop.quit)
+ mainloop.run()
+
+ if usb_queries is None:
+ assert device_events_signal_count == 0
+ assert devices_received == 0
+ assert devices_removed == 0
+ else:
+ assert device_events_signal_count == 1
+ assert devices_received == 2
+ assert devices_removed == 0
+
+ print("Removong device ", dev_path)
+ portal_mock.umockdev.remove_device(dev_path)
+ print("Removed")
+
+ mainloop = GLib.MainLoop()
+ GLib.timeout_add(1000, mainloop.quit)
+ mainloop.run()
+
+ print("End of loop")
+
+ if usb_queries is None:
+ assert device_events_signal_count == 0
+ assert devices_received == 0
+ assert devices_removed == 0
+ else:
+ assert device_events_signal_count == 2
+ assert devices_received == 2
+ assert devices_removed == 1
+
+ @pytest.mark.parametrize("usb_queries", ["vnd:04a9;vnd:04aa"])
+ @pytest.mark.parametrize("params", [{"filters": {"vendor": "04a9"}}])
+ def test_acquire(self, portal_mock, app_id):
+ self.generate_device(
+ portal_mock.umockdev,
+ "04a9",
+ "Canon_Inc.",
+ "31c0",
+ "Canon_Digital_Camera",
+ "C767F1C714174C309255F70E4A7B2EE2",
+ )
+
+ self.generate_device(
+ portal_mock.umockdev,
+ "04aa",
+ "Someone Else.",
+ "31c0",
+ "SomeProduct",
+ "00001",
+ )
+
+ possible_vendors = ["04a9", "04aa"]
+
+ usb_intf = portal_mock.get_dbus_interface()
+ devices = usb_intf.EnumerateDevices({})
+ assert len(devices) == 2
+ (id1, dev_info1) = devices[0]
+ assert id1
+ assert dev_info1
+ vendor_id = dev_info1["properties"]["ID_VENDOR_ID"]
+ assert vendor_id in possible_vendors
+ possible_vendors.remove(vendor_id)
+ (id2, dev_info2) = devices[1]
+ assert id2
+ assert dev_info2
+ vendor_id = dev_info2["properties"]["ID_VENDOR_ID"]
+ assert vendor_id in possible_vendors
+ possible_vendors.remove(vendor_id)
+
+ request = portal_mock.create_request()
+ response = request.call(
+ "AcquireDevices",
+ parent_window="",
+ devices=[
+ (id1, {"writable": True}),
+ (id2, {"writable": True}),
+ ],
+ options={},
+ )
+ assert response.response == 0
+
+ (results, finished) = usb_intf.FinishAcquireDevices(request.handle, {})
+ assert finished
+ assert len(results) == 1
+ (res_id, device) = results[0]
+ assert res_id == id1 or res_id == id2
+ assert device["success"]
+ fd = device["fd"].take()
+ assert fd > 0
+ with os.fdopen(fd, "r") as f:
+ assert f
+ assert "error" not in device
+
+ usb_intf.ReleaseDevices([res_id], {})
+
+ @pytest.mark.parametrize("usb_queries", ["vnd:0001"])
+ @pytest.mark.parametrize(
+ "expected,params",
+ [
+ (1, {"filters": {"model": "0000"}}),
+ (1, {"filters": {"model": "0001"}}),
+ (0, {"filters": {"model": "0002"}}),
+ (2, {"filters": {"vendor": "0001"}}),
+ (0, {"filters": {"vendor": "0002"}}),
+ (1, {"filters": {"vendor": "0001", "model": "0000"}}),
+ (0, {"filters": {"vendor": "0002", "model": "0000"}}),
+ ],
+ )
+ def test_queries(self, expected, portal_mock, app_id, usb_queries):
+ for i in range(2):
+ self.generate_device(
+ portal_mock.umockdev,
+ "0001",
+ "example_org",
+ f"000{i}",
+ f"model{i}",
+ "0001",
+ )
+
+ usb_intf = portal_mock.get_dbus_interface()
+ devices = usb_intf.EnumerateDevices({})
+ assert len(devices) == 2
+ acquire_devices = [(id, {"writable": True}) for (id, _) in devices]
+
+ request = portal_mock.create_request()
+ response = request.call(
+ "AcquireDevices",
+ parent_window="",
+ devices=acquire_devices,
+ options={},
+ )
+ assert response.response == 0
+
+ (results, finished) = usb_intf.FinishAcquireDevices(request.handle, {})
+ assert finished
+ assert len(results) == expected