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