From a172f22c953dcea2386ec9cb9b63ff4e7ace162e Mon Sep 17 00:00:00 2001 From: Julian Sparber Date: Mon, 18 Nov 2024 16:25:17 +0100 Subject: [PATCH] GPortalNotificationBackend: Parse GBytesIcon and GLoadableIcon for v2 The notification portal v2 accepts a sealable `memfd`. We can load `GBytesIcon` and `GLoadableIcon` into a `memfd` automatically. For version 1 of the portal we can load `GLoadableIcon` into a `GBytesIcon` to increase support for more `GIcon` formats. This doesn't include `GEmblemedIcon` it's likely that it will be deprecated soon https://gitlab.gnome.org/GNOME/glib/-/issues/3544 Closes: https://gitlab.gnome.org/GNOME/glib/-/issues/3108 --- gio/gportalnotificationbackend.c | 541 ++++++++++++++++++++++++++++--- 1 file changed, 501 insertions(+), 40 deletions(-) diff --git a/gio/gportalnotificationbackend.c b/gio/gportalnotificationbackend.c index 5a1107baa..ff60513a5 100644 --- a/gio/gportalnotificationbackend.c +++ b/gio/gportalnotificationbackend.c @@ -17,11 +17,18 @@ * Public License along with this library; if not, see . * * Author: Matthias Clasen +* Julian Sparber */ #include "config.h" #include "gnotificationbackend.h" +#include +#include + +#include +#include + #include "giomodule-priv.h" #include "gioenumtypes.h" #include "gicon.h" @@ -30,6 +37,11 @@ #include "gnotification-private.h" #include "gportalsupport.h" #include "gtask.h" +#include "gunixfdlist.h" +#include + +/* This is the max size the xdg portal allows for icons, so load icons with this size when needed */ +#define ICON_SIZE 512 #define G_TYPE_PORTAL_NOTIFICATION_BACKEND (g_portal_notification_backend_get_type ()) #define G_PORTAL_NOTIFICATION_BACKEND(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), G_TYPE_PORTAL_NOTIFICATION_BACKEND, GPortalNotificationBackend)) @@ -127,30 +139,382 @@ get_supported_features_finish (GPortalNotificationBackend *backend, return g_task_propagate_boolean (G_TASK (result), error); } -static void -get_supported_features_cb (GObject *source_object, - GAsyncResult *result, - gpointer user_data) +typedef struct { + char *id; + GNotification *notification; +} CallData; + +static CallData* +call_data_new (const char *id, + GNotification *notification) { - GPortalNotificationBackend *backend = G_PORTAL_NOTIFICATION_BACKEND (source_object); - GVariant *notification_serialized = user_data; + CallData *data; + + data = g_new0 (CallData, 1); + data->id = g_strdup (id); + data->notification = g_object_ref (notification); + + return data; +} + +static void +call_data_free (gpointer user_data) +{ + CallData *data = user_data; + + g_clear_pointer (&data->id, g_free); + g_clear_object (&data->notification); + + g_free (data); +} + +typedef struct { + GUnixFDList *fd_list; + GVariantBuilder *builder; + gsize parse_ref; +} ParserData; + +static ParserData* +parser_data_new (const char *id) +{ + ParserData *data; + + data = g_new0 (ParserData, 1); + data->fd_list = g_unix_fd_list_new (); + data->builder = g_variant_builder_new (G_VARIANT_TYPE ("(sa{sv})")); + + g_variant_builder_add (data->builder, "s", id); + g_variant_builder_open (data->builder, G_VARIANT_TYPE ("a{sv}")); + + return data; +} + +static void +parser_data_hold (ParserData *data) +{ + data->parse_ref++; +} + +static gboolean +parser_data_release (ParserData *data) +{ + data->parse_ref--; + + if (data->parse_ref == 0) + g_variant_builder_close (data->builder); + + return data->parse_ref == 0; +} + +static void +parser_data_free (gpointer user_data) +{ + ParserData *data = user_data; + + g_clear_object (&data->fd_list); + g_clear_pointer (&data->builder, g_variant_builder_unref); + + g_free (data); +} + +static void +splice_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task = G_TASK (user_data); GError *error = NULL; - if (!get_supported_features_finish (backend, result, &error)) + if (g_output_stream_splice_finish (G_OUTPUT_STREAM (source_object), res, &error) == -1) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_boolean (task, TRUE); + + g_object_unref (task); +} + +static void +icon_load_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task = G_TASK (user_data); + GOutputStream *stream_out = G_OUTPUT_STREAM (g_task_get_task_data (task)); + GError *error = NULL; + GInputStream *stream_in = NULL; + + stream_in = g_loadable_icon_load_finish (G_LOADABLE_ICON (source_object), res, NULL, &error); + if (!stream_in) { - g_warning ("Failed to get notification portal version: %s", error->message); - g_clear_pointer (&error, g_error_free); + g_task_return_error (task, g_steal_pointer (&error)); + g_object_unref (task); return; } - g_dbus_connection_call (G_NOTIFICATION_BACKEND (backend)->dbus_connection, - "org.freedesktop.portal.Desktop", - "/org/freedesktop/portal/desktop", - "org.freedesktop.portal.Notification", - "AddNotification", - notification_serialized, - G_VARIANT_TYPE_UNIT, - G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + g_output_stream_splice_async (stream_out, + G_INPUT_STREAM (stream_in), + G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE, + g_task_get_priority (task), + g_task_get_cancellable (task), + splice_cb, + g_object_ref (task)); + + g_object_unref (stream_in); + g_object_unref (task); +} + +static int +bytes_to_memfd (const gchar *name, + GBytes *bytes, + GError **error) +{ + int fd = -1; + gconstpointer bytes_data; + gpointer shm; + gsize bytes_len; + + fd = memfd_create (name, MFD_ALLOW_SEALING); + if (fd == -1) + { + int saved_errno; + + saved_errno = errno; + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (saved_errno), + "memfd_create: %s", g_strerror (saved_errno)); + return -1; + } + + bytes_data = g_bytes_get_data (bytes, &bytes_len); + + if (ftruncate (fd, bytes_len) == -1) + { + int saved_errno; + + saved_errno = errno; + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (saved_errno), + "ftruncate: %s", g_strerror (saved_errno)); + close (fd); + return -1; + } + + shm = mmap (NULL, bytes_len, PROT_WRITE, MAP_SHARED, fd, 0); + if (shm == MAP_FAILED) + { + int saved_errno; + + saved_errno = errno; + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (saved_errno), + "mmap: %s", g_strerror (saved_errno)); + close (fd); + return -1; + } + + memcpy (shm, bytes_data, bytes_len); + + if (munmap (shm, bytes_len) == -1) + { + int saved_errno; + + saved_errno = errno; + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (saved_errno), + "munmap: %s", g_strerror (saved_errno)); + close (fd); + return -1; + } + + return g_steal_fd (&fd); +} + +static void +serialize_icon (GIcon *icon, + uint version, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task = NULL; + + task = g_task_new (NULL, NULL, callback, user_data); + g_task_set_source_tag (task, serialize_icon); + + if (G_IS_THEMED_ICON (icon)) + { + g_task_set_task_data (task, + g_icon_serialize (icon), + (GDestroyNotify) g_variant_unref); + g_task_return_boolean (task, TRUE); + } + else if (G_IS_BYTES_ICON (icon)) + { + if (version < 2) + { + g_task_set_task_data (task, + g_icon_serialize (icon), + (GDestroyNotify) g_variant_unref); + g_task_return_boolean (task, TRUE); + } + else + { + GOutputStream *stream_out = NULL; + GError *error = NULL; + int fd = -1; + + fd = bytes_to_memfd ("notification-icon", + g_bytes_icon_get_bytes (G_BYTES_ICON (icon)), + &error); + if (fd == -1) + { + g_task_return_error (task, g_steal_pointer (&error)); + g_object_unref (task); + return; + } + + stream_out = g_unix_output_stream_new (g_steal_fd (&fd), TRUE); + g_task_set_task_data (task, g_steal_pointer (&stream_out), g_object_unref); + g_task_return_boolean (task, TRUE); + } + } + else if (G_IS_LOADABLE_ICON (icon)) + { + GOutputStream *stream_out = NULL; + + if (version < 2) + { + stream_out = g_memory_output_stream_new_resizable (); + } + else + { + int fd = -1; + + fd = memfd_create ("notification-icon", MFD_ALLOW_SEALING); + if (fd == -1) + { + int saved_errno; + + saved_errno = errno; + g_task_return_new_error (task, + G_IO_ERROR, + g_io_error_from_errno (saved_errno), + "memfd_create: %s", g_strerror (saved_errno)); + + g_object_unref (task); + return; + } + + stream_out = g_unix_output_stream_new (g_steal_fd (&fd), TRUE); + } + + g_task_set_task_data (task, g_steal_pointer (&stream_out), g_object_unref); + + g_loadable_icon_load_async (G_LOADABLE_ICON (icon), + ICON_SIZE, + NULL, + icon_load_cb, + g_object_ref (task)); + } + else + { + g_assert_not_reached (); + } + + g_object_unref (task); +} + +static GVariant* +serialize_icon_finish (GAsyncResult *result, + GUnixFDList *fd_list, + GError **error) +{ + GTask *task = G_TASK (result); + gpointer data = g_task_get_task_data (task); + GVariant *res = NULL; + + g_return_val_if_fail (g_task_get_source_tag (task) == serialize_icon, NULL); + + if (!g_task_propagate_boolean (task, error)) + return NULL; + + g_assert (data != NULL); + + if (G_IS_MEMORY_OUTPUT_STREAM (data)) + { + GBytes *bytes = NULL; + GIcon *icon = NULL; + + bytes = g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (data)); + icon = g_bytes_icon_new (g_steal_pointer (&bytes)); + res = g_icon_serialize (icon), + + g_bytes_unref (bytes); + g_clear_object (&icon); + } + else if (G_IS_UNIX_OUTPUT_STREAM (data)) + { + int fd; + int fd_in; + + fd = g_unix_output_stream_get_fd (G_UNIX_OUTPUT_STREAM (data)); + g_assert (fd != -1); + + if (lseek (fd, 0, SEEK_SET) == -1) + { + int saved_errno = errno; + + g_task_return_new_error (task, + G_IO_ERROR, + g_io_error_from_errno (saved_errno), + "lseek: %s", g_strerror (saved_errno)); + return NULL; + } + + fd_in = g_unix_fd_list_append (fd_list, fd, error); + if (fd_in == -1) + return NULL; + + res = g_variant_ref_sink (g_variant_new ("(sv)", "file-descriptor", g_variant_new_handle (fd_in))); + } + else + { + res = g_variant_ref (g_task_get_task_data (task)); + } + + return res; +} + +static void +serialize_icon_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GTask *task = G_TASK (user_data); + ParserData *data = g_task_get_task_data (task); + GError *error = NULL; + GVariant *icon_out = NULL; + + icon_out = serialize_icon_finish (res, data->fd_list, &error); + + if (!icon_out) + { + g_prefix_error_literal (&error, "Failed to serialize icon: "); + g_task_return_error (task, g_steal_pointer (&error)); + g_object_unref (task); + return; + } + + g_variant_builder_add (data->builder, "{sv}", "icon", icon_out); + + if (parser_data_release (data)) + g_task_return_boolean (task, TRUE); + + g_clear_pointer (&icon_out, g_variant_unref); + g_object_unref (task); } static GVariant * @@ -208,53 +572,156 @@ serialize_priority (GNotification *notification) return nick; } -static GVariant * -serialize_notification (GNotification *notification) +static void +serialize_notification (const char *id, + GNotification *notification, + uint version, + GAsyncReadyCallback callback, + gpointer user_data) { - GVariantBuilder builder; + ParserData *data; + GTask *task; const gchar *body; GIcon *icon; gchar *default_action = NULL; GVariant *default_action_target = NULL; GVariant *buttons = NULL; - g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); + task = g_task_new (NULL, NULL, callback, user_data); + g_task_set_source_tag (task, serialize_notification); - g_variant_builder_add (&builder, "{sv}", "title", g_variant_new_string (g_notification_get_title (notification))); + data = parser_data_new (id); + g_task_set_task_data (task, + data, + parser_data_free); + parser_data_hold (data); + + g_variant_builder_add (data->builder, "{sv}", "title", g_variant_new_string (g_notification_get_title (notification))); if ((body = g_notification_get_body (notification))) - g_variant_builder_add (&builder, "{sv}", "body", g_variant_new_string (body)); + g_variant_builder_add (data->builder, "{sv}", "body", g_variant_new_string (body)); if ((icon = g_notification_get_icon (notification))) { - GVariant *serialized_icon = NULL; - - if ((serialized_icon = g_icon_serialize (icon))) + if (G_IS_THEMED_ICON (icon) || G_IS_BYTES_ICON (icon) || G_IS_LOADABLE_ICON (icon)) { - g_variant_builder_add (&builder, "{sv}", "icon", serialized_icon); - g_clear_object (&icon); + parser_data_hold (data); + serialize_icon (icon, + version, + serialize_icon_cb, + g_object_ref (task)); + } + else + { + g_warning ("Can’t add icon to portal notification: %s isn’t handled", g_type_name_from_instance ((GTypeInstance *)icon)); } } - g_variant_builder_add (&builder, "{sv}", "priority", serialize_priority (notification)); + g_variant_builder_add (data->builder, "{sv}", "priority", serialize_priority (notification)); if (g_notification_get_default_action (notification, &default_action, &default_action_target)) { - g_variant_builder_add (&builder, "{sv}", "default-action", + g_variant_builder_add (data->builder, "{sv}", "default-action", g_variant_new_take_string (g_steal_pointer (&default_action))); if (default_action_target) { - g_variant_builder_add (&builder, "{sv}", "default-action-target", + g_variant_builder_add (data->builder, "{sv}", "default-action-target", default_action_target); g_clear_pointer (&default_action_target, g_variant_unref); } } if ((buttons = serialize_buttons (notification))) - g_variant_builder_add (&builder, "{sv}", "buttons", buttons); + g_variant_builder_add (data->builder, "{sv}", "buttons", buttons); - return g_variant_builder_end (&builder); + if (parser_data_release (data)) + g_task_return_boolean (task, TRUE); + + g_clear_object (&task); +} + +static GVariant * +serialize_notification_finish (GAsyncResult *result, + GUnixFDList **fd_list, + GError **error) +{ + ParserData *data; + g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == serialize_notification, NULL); + + if (!g_task_propagate_boolean (G_TASK (result), error)) + return NULL; + + data = g_task_get_task_data (G_TASK (result)); + + if (fd_list) + *fd_list = g_steal_pointer (&data->fd_list); + + return g_variant_ref_sink (g_variant_builder_end (g_steal_pointer (&data->builder))); +} + +static void +serialize_notification_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GNotificationBackend *backend = G_NOTIFICATION_BACKEND (user_data); + GError *error = NULL; + GUnixFDList *fd_list = NULL; + GVariant *parameters = NULL; + + parameters = serialize_notification_finish (result, &fd_list, &error); + + if (!parameters) + { + g_warning ("Failed to send notification: %s", error->message); + g_error_free (error); + return; + } + + g_dbus_connection_call_with_unix_fd_list (backend->dbus_connection, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.Notification", + "AddNotification", + parameters, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + fd_list, + NULL, + NULL, + NULL); + + g_object_unref (backend); + g_object_unref (fd_list); + g_variant_unref (parameters); +} + +static void +get_supported_features_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + GPortalNotificationBackend *backend = G_PORTAL_NOTIFICATION_BACKEND (source_object); + CallData *data = user_data; + GError *error = NULL; + + if (!get_supported_features_finish (backend, result, &error)) + { + g_warning ("Failed to get notification portal version: %s", error->message); + g_clear_pointer (&error, g_error_free); + call_data_free (data); + return; + } + + serialize_notification (data->id, + data->notification, + backend->version, + serialize_notification_cb, + g_object_ref (backend)); + + call_data_free (data); } static gboolean @@ -268,15 +735,9 @@ g_portal_notification_backend_send_notification (GNotificationBackend *backend, const gchar *id, GNotification *notification) { - GVariant *notification_serialized; - - notification_serialized = g_variant_new ("(s@a{sv})", - id, - serialize_notification (notification)); - get_supported_features (G_PORTAL_NOTIFICATION_BACKEND (backend), get_supported_features_cb, - notification_serialized); + call_data_new (id, notification)); } static void