Branch data Line data Source code
1 : : /*
2 : : * Copyright © 2024 GNOME Foundation Inc.
3 : : *
4 : : * SPDX-License-Identifier: LGPL-2.1-or-later
5 : : *
6 : : * This library is free software; you can redistribute it and/or
7 : : * modify it under the terms of the GNU Lesser General Public
8 : : * License as published by the Free Software Foundation; either
9 : : * version 2.1 of the License, or (at your option) any later version.
10 : : *
11 : : * This library is distributed in the hope that it will be useful,
12 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 : : * Lesser General Public License for more details.
15 : : *
16 : : * You should have received a copy of the GNU Lesser General
17 : : * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
18 : : *
19 : : * Authors: Julian Sparber <jsparber@gnome.org>
20 : : * Philip Withnall <philip@tecnocode.co.uk>
21 : : */
22 : :
23 : : /* A stub implementation of xdg-desktop-portal */
24 : :
25 : : #ifdef __FreeBSD__
26 : : #include <fcntl.h>
27 : : #include <sys/types.h>
28 : : #include <sys/user.h>
29 : : #endif /* __FreeBSD__ */
30 : :
31 : : #include <glib.h>
32 : : #include <gio/gio.h>
33 : : #include <gio/gunixfdlist.h>
34 : :
35 : :
36 : : #include "fake-desktop-portal.h"
37 : : #include "fake-openuri-portal-generated.h"
38 : : #include "fake-request-portal-generated.h"
39 : :
40 : : struct _GFakeDesktopPortalThread
41 : : {
42 : : GObject parent_instance;
43 : :
44 : : char *address; /* (not nullable) */
45 : : GCancellable *cancellable; /* (not nullable) (owned) */
46 : : GThread *thread; /* (not nullable) (owned) */
47 : : GCond cond; /* (mutex mutex) */
48 : : GMutex mutex;
49 : : gboolean ready; /* (mutex mutex) */
50 : :
51 : : char *request_activation_token; /* (mutex mutex) */
52 : : char *request_uri; /* (mutex mutex) */
53 : : } FakeDesktopPortalThread;
54 : :
55 : 38 : G_DEFINE_FINAL_TYPE (GFakeDesktopPortalThread, g_fake_desktop_portal_thread, G_TYPE_OBJECT)
56 : :
57 : : static void g_fake_desktop_portal_thread_finalize (GObject *object);
58 : :
59 : : static void
60 : 1 : g_fake_desktop_portal_thread_class_init (GFakeDesktopPortalThreadClass *klass)
61 : : {
62 : 1 : GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
63 : :
64 : 1 : gobject_class->finalize = g_fake_desktop_portal_thread_finalize;
65 : 1 : }
66 : :
67 : : static void
68 : 4 : g_fake_desktop_portal_thread_init (GFakeDesktopPortalThread *self)
69 : : {
70 : 4 : self->cancellable = g_cancellable_new ();
71 : 4 : g_cond_init (&self->cond);
72 : 4 : g_mutex_init (&self->mutex);
73 : 4 : }
74 : :
75 : : static void
76 : 4 : g_fake_desktop_portal_thread_finalize (GObject *object)
77 : : {
78 : 4 : GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (object);
79 : :
80 : 4 : g_assert (self->thread == NULL); /* should already have been joined */
81 : :
82 : 4 : g_mutex_clear (&self->mutex);
83 : 4 : g_cond_clear (&self->cond);
84 : 4 : g_clear_object (&self->cancellable);
85 : 4 : g_clear_pointer (&self->address, g_free);
86 : :
87 : 4 : g_clear_pointer (&self->request_activation_token, g_free);
88 : 4 : g_clear_pointer (&self->request_uri, g_free);
89 : :
90 : 4 : G_OBJECT_CLASS (g_fake_desktop_portal_thread_parent_class)->finalize (object);
91 : 4 : }
92 : :
93 : : static gboolean
94 : 0 : on_handle_close (FakeRequest *object,
95 : : GDBusMethodInvocation *invocation,
96 : : gpointer user_data)
97 : : {
98 : 0 : g_test_message ("Got close request");
99 : 0 : fake_request_complete_close (object, invocation);
100 : :
101 : 0 : return G_DBUS_METHOD_INVOCATION_HANDLED;
102 : : }
103 : :
104 : : static char*
105 : 4 : get_request_path (GDBusMethodInvocation *invocation,
106 : : const char *token)
107 : : {
108 : : char *request_obj_path;
109 : : char *sender;
110 : :
111 : 4 : sender = g_strdup (g_dbus_method_invocation_get_sender (invocation) + 1);
112 : :
113 : : /* The object path needs to be the specific format.
114 : : * See: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request */
115 : 17 : for (size_t i = 0; sender[i]; i++)
116 : 13 : if (sender[i] == '.')
117 : 4 : sender[i] = '_';
118 : :
119 : 4 : request_obj_path = g_strdup_printf ("/org/freedesktop/portal/desktop/request/%s/%s", sender, token);
120 : 4 : g_free (sender);
121 : :
122 : 4 : return request_obj_path;
123 : : }
124 : :
125 : : static gboolean
126 : 4 : handle_request (GFakeDesktopPortalThread *self,
127 : : FakeOpenURI *object,
128 : : GDBusMethodInvocation *invocation,
129 : : const gchar *arg_parent_window,
130 : : const gchar *arg_uri,
131 : : gboolean open_file,
132 : : GVariant *arg_options)
133 : : {
134 : 4 : const char *activation_token = NULL;
135 : 4 : GError *error = NULL;
136 : : FakeRequest *interface_request;
137 : : GVariantBuilder opt_builder;
138 : : char *request_obj_path;
139 : 4 : const char *token = NULL;
140 : :
141 : 4 : if (arg_options)
142 : : {
143 : 4 : g_variant_lookup (arg_options, "activation_token", "&s", &activation_token);
144 : 4 : g_variant_lookup (arg_options, "handle_token", "&s", &token);
145 : : }
146 : :
147 : 4 : g_set_str (&self->request_activation_token, activation_token);
148 : 4 : g_set_str (&self->request_uri, arg_uri);
149 : :
150 : 4 : request_obj_path = get_request_path (invocation, token ? token : "t");
151 : :
152 : 4 : if (open_file)
153 : : {
154 : 4 : g_test_message ("Got open file request for %s", arg_uri);
155 : :
156 : 4 : fake_open_uri_complete_open_file (object,
157 : : invocation,
158 : : NULL,
159 : : request_obj_path);
160 : :
161 : : }
162 : : else
163 : : {
164 : 0 : g_test_message ("Got open URI request for %s", arg_uri);
165 : :
166 : 0 : fake_open_uri_complete_open_uri (object,
167 : : invocation,
168 : : request_obj_path);
169 : :
170 : : }
171 : :
172 : 4 : interface_request = fake_request_skeleton_new ();
173 : 4 : g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT);
174 : :
175 : 4 : g_signal_connect (interface_request,
176 : : "handle-close",
177 : : G_CALLBACK (on_handle_close),
178 : : NULL);
179 : :
180 : 4 : g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (interface_request),
181 : : g_dbus_method_invocation_get_connection (invocation),
182 : : request_obj_path,
183 : : &error);
184 : 4 : g_assert_no_error (error);
185 : 4 : g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (interface_request),
186 : : G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD);
187 : 4 : g_test_message ("Request skeleton exported at %s", request_obj_path);
188 : :
189 : : /* We can't use `fake_request_emit_response()` because it doesn't set the sender */
190 : 4 : g_dbus_connection_emit_signal (g_dbus_method_invocation_get_connection (invocation),
191 : : g_dbus_method_invocation_get_sender (invocation),
192 : : request_obj_path,
193 : : "org.freedesktop.portal.Request",
194 : : "Response",
195 : : g_variant_new ("(u@a{sv})",
196 : : 0, /* Success */
197 : : g_variant_builder_end (&opt_builder)),
198 : : NULL);
199 : :
200 : 4 : g_test_message ("Response emitted");
201 : :
202 : 4 : g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (interface_request));
203 : 4 : g_free (request_obj_path);
204 : 4 : g_object_unref (interface_request);
205 : :
206 : 4 : return G_DBUS_METHOD_INVOCATION_HANDLED;
207 : : }
208 : :
209 : : /* This is currently private as there’s only one user of it, but it could become
210 : : * a public API in future. */
211 : : static char *
212 : 2 : _g_fd_query_path (int fd,
213 : : GError **error)
214 : : {
215 : : #if defined(__FreeBSD__)
216 : : struct kinfo_file kf;
217 : : kf.kf_structsize = sizeof (kf);
218 : : if (fcntl (fd, F_KINFO, &kf) < 0)
219 : : {
220 : : int saved_errno = errno;
221 : : g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
222 : : "Error querying file information for FD %d: %s",
223 : : fd, g_strerror (saved_errno));
224 : : return NULL;
225 : : }
226 : :
227 : : return g_strdup (kf.kf_path);
228 : : #elif defined(G_OS_UNIX)
229 : 2 : char *path = NULL;
230 : 2 : char *proc_path = g_strdup_printf ("/proc/self/fd/%d", fd);
231 : 2 : path = g_file_read_link (proc_path, error);
232 : 2 : g_free (proc_path);
233 : 2 : return g_steal_pointer (&path);
234 : : #else
235 : : /* - A NetBSD implementation would probably use `fcntl()` with `F_GETPATH`:
236 : : * https://man.netbsd.org/fcntl.2
237 : : * - A Windows implementation would probably use `GetFinalPathNameByHandleW()`:
238 : : * https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlea
239 : : * - A Hurd implementation could open("/dev/fd/%u"):
240 : : * https://gitlab.gnome.org/GNOME/glib/-/merge_requests/4396#note_2279923
241 : : */
242 : : #error "_g_fd_query_path() not supported on this platform"
243 : : #endif
244 : : }
245 : :
246 : : static char *
247 : 2 : handle_to_uri (GVariant *handle,
248 : : GUnixFDList *fd_list)
249 : : {
250 : 2 : int fd = -1;
251 : : int fd_id;
252 : : char *path;
253 : : char *uri;
254 : 2 : GError *local_error = NULL;
255 : :
256 : 2 : fd_id = g_variant_get_handle (handle);
257 : 2 : fd = g_unix_fd_list_get (fd_list, fd_id, NULL);
258 : :
259 : 2 : if (fd == -1)
260 : 0 : return NULL;
261 : :
262 : 2 : path = _g_fd_query_path (fd, &local_error);
263 : 2 : g_assert_no_error (local_error);
264 : :
265 : 2 : uri = g_filename_to_uri (path, NULL, NULL);
266 : 2 : g_free (path);
267 : 2 : close (fd);
268 : :
269 : 2 : return uri;
270 : : }
271 : :
272 : : static gboolean
273 : 2 : on_handle_open_file (FakeOpenURI *object,
274 : : GDBusMethodInvocation *invocation,
275 : : GUnixFDList *fd_list,
276 : : const gchar *arg_parent_window,
277 : : GVariant *arg_fd,
278 : : GVariant *arg_options,
279 : : gpointer user_data)
280 : : {
281 : 2 : GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data);
282 : 2 : char *uri = NULL;
283 : :
284 : 2 : uri = handle_to_uri (arg_fd, fd_list);
285 : 2 : handle_request (self,
286 : : object,
287 : : invocation,
288 : : arg_parent_window,
289 : : uri,
290 : : TRUE,
291 : : arg_options);
292 : 2 : g_free (uri);
293 : :
294 : 2 : return G_DBUS_METHOD_INVOCATION_HANDLED;
295 : : }
296 : :
297 : : static gboolean
298 : 2 : on_handle_open_uri (FakeOpenURI *object,
299 : : GDBusMethodInvocation *invocation,
300 : : const gchar *arg_parent_window,
301 : : const gchar *arg_uri,
302 : : GVariant *arg_options,
303 : : gpointer user_data)
304 : : {
305 : 2 : GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data);
306 : :
307 : 2 : handle_request (self,
308 : : object,
309 : : invocation,
310 : : arg_parent_window,
311 : : arg_uri,
312 : : TRUE,
313 : : arg_options);
314 : :
315 : 2 : return G_DBUS_METHOD_INVOCATION_HANDLED;
316 : : }
317 : :
318 : : static void
319 : 4 : on_name_acquired (GDBusConnection *connection,
320 : : const gchar *name,
321 : : gpointer user_data)
322 : : {
323 : 4 : GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data);
324 : :
325 : 4 : g_test_message ("Acquired the name %s", name);
326 : :
327 : 4 : g_mutex_lock (&self->mutex);
328 : 4 : self->ready = TRUE;
329 : 4 : g_cond_signal (&self->cond);
330 : 4 : g_mutex_unlock (&self->mutex);
331 : 4 : }
332 : :
333 : : static void
334 : 0 : on_name_lost (GDBusConnection *connection,
335 : : const gchar *name,
336 : : gpointer user_data)
337 : : {
338 : 0 : g_test_message ("Lost the name %s", name);
339 : 0 : }
340 : :
341 : : static gboolean
342 : 4 : cancelled_cb (GCancellable *cancellable,
343 : : gpointer user_data)
344 : : {
345 : 4 : g_test_message ("fake-desktop-portal cancelled");
346 : 4 : return G_SOURCE_CONTINUE;
347 : : }
348 : :
349 : : static gpointer
350 : 4 : fake_desktop_portal_thread_cb (gpointer user_data)
351 : : {
352 : 4 : GFakeDesktopPortalThread *self = G_FAKE_DESKTOP_PORTAL_THREAD (user_data);
353 : 4 : GMainContext *context = NULL;
354 : 4 : GDBusConnection *connection = NULL;
355 : 4 : GSource *source = NULL;
356 : : guint id;
357 : : FakeOpenURI *interface_open_uri;
358 : 4 : GError *local_error = NULL;
359 : :
360 : 4 : context = g_main_context_new ();
361 : 4 : g_main_context_push_thread_default (context);
362 : :
363 : 4 : connection = g_dbus_connection_new_for_address_sync (self->address,
364 : : G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
365 : : G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
366 : : NULL,
367 : : self->cancellable,
368 : : &local_error);
369 : 4 : g_assert_no_error (local_error);
370 : :
371 : : /* Listen for cancellation. The source will wake up the context iteration
372 : : * which can then re-check its exit condition below. */
373 : 4 : source = g_cancellable_source_new (self->cancellable);
374 : 4 : g_source_set_callback (source, G_SOURCE_FUNC (cancelled_cb), NULL, NULL);
375 : 4 : g_source_attach (source, context);
376 : 4 : g_source_unref (source);
377 : :
378 : : /* Set up the interface */
379 : 4 : g_test_message ("Acquired a message bus connection");
380 : :
381 : 4 : interface_open_uri = fake_open_uri_skeleton_new ();
382 : :
383 : 4 : g_signal_connect (interface_open_uri,
384 : : "handle-open-file",
385 : : G_CALLBACK (on_handle_open_file),
386 : : self);
387 : 4 : g_signal_connect (interface_open_uri,
388 : : "handle-open-uri",
389 : : G_CALLBACK (on_handle_open_uri),
390 : : self);
391 : :
392 : 4 : g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (interface_open_uri),
393 : : connection,
394 : : "/org/freedesktop/portal/desktop",
395 : : &local_error);
396 : 4 : g_assert_no_error (local_error);
397 : :
398 : : /* Own the portal name */
399 : 4 : id = g_bus_own_name_on_connection (connection,
400 : : "org.freedesktop.portal.Desktop",
401 : : G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT |
402 : : G_BUS_NAME_OWNER_FLAGS_REPLACE,
403 : : on_name_acquired,
404 : : on_name_lost,
405 : : self,
406 : : NULL);
407 : :
408 : 20 : while (!g_cancellable_is_cancelled (self->cancellable))
409 : 16 : g_main_context_iteration (context, TRUE);
410 : :
411 : 4 : g_bus_unown_name (id);
412 : 4 : g_clear_object (&connection);
413 : 4 : g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (interface_open_uri));
414 : 4 : g_object_unref (interface_open_uri);
415 : 4 : g_main_context_pop_thread_default (context);
416 : 4 : g_clear_pointer (&context, g_main_context_unref);
417 : :
418 : 4 : return NULL;
419 : : }
420 : :
421 : : /* Get the activation token given to the most recent OpenURI request
422 : : *
423 : : * Returns: (transfer none) (nullable: an activation token
424 : : */
425 : : const gchar *
426 : 4 : g_fake_desktop_portal_thread_get_last_request_activation_token (GFakeDesktopPortalThread *self)
427 : : {
428 : 4 : g_return_val_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self), NULL);
429 : :
430 : 4 : return self->request_activation_token;
431 : : }
432 : :
433 : : /* Get the file or URI given to the most recent OpenURI request
434 : : *
435 : : * Returns: (transfer none) (nullable): an URI
436 : : */
437 : : const gchar *
438 : 4 : g_fake_desktop_portal_thread_get_last_request_uri (GFakeDesktopPortalThread *self)
439 : : {
440 : 4 : g_return_val_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self), NULL);
441 : :
442 : 4 : return self->request_uri;
443 : : }
444 : :
445 : : /*
446 : : * Create a new #GFakeDesktopPortalThread. The thread isn’t started until
447 : : * g_fake_desktop_portal_thread_run() is called on it.
448 : : *
449 : : * Returns: (transfer full): the new fake desktop portal wrapper
450 : : */
451 : : GFakeDesktopPortalThread *
452 : 4 : g_fake_desktop_portal_thread_new (const char *address)
453 : : {
454 : 4 : GFakeDesktopPortalThread *self = g_object_new (G_TYPE_FAKE_DESKTOP_PORTAL_THREAD, NULL);
455 : 4 : self->address = g_strdup (address);
456 : 4 : return g_steal_pointer (&self);
457 : : }
458 : :
459 : : /*
460 : : * Start a worker thread which will run a fake
461 : : * `org.freedesktop.portal.Desktops` portal on the bus at @address. This is
462 : : * intended to be used with #GTestDBus to mock up a portal from within a unit
463 : : * test process, rather than relying on D-Bus activation of a mock portal
464 : : * subprocess.
465 : : *
466 : : * It blocks until the thread has owned its D-Bus name and is ready to handle
467 : : * requests.
468 : : */
469 : : void
470 : 4 : g_fake_desktop_portal_thread_run (GFakeDesktopPortalThread *self)
471 : : {
472 : 4 : g_return_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self));
473 : 4 : g_return_if_fail (self->thread == NULL);
474 : :
475 : 4 : self->thread = g_thread_new ("fake-desktop-portal", fake_desktop_portal_thread_cb, self);
476 : :
477 : : /* Block until the thread is ready. */
478 : 4 : g_mutex_lock (&self->mutex);
479 : 8 : while (!self->ready)
480 : 4 : g_cond_wait (&self->cond, &self->mutex);
481 : 4 : g_mutex_unlock (&self->mutex);
482 : : }
483 : :
484 : : /* Stop and join a worker thread started with fake_desktop_portal_thread_run().
485 : : * Blocks until the thread has stopped and joined.
486 : : *
487 : : * Once this has been called, it’s safe to drop the final reference on @self. */
488 : : void
489 : 4 : g_fake_desktop_portal_thread_stop (GFakeDesktopPortalThread *self)
490 : : {
491 : 4 : g_return_if_fail (G_IS_FAKE_DESKTOP_PORTAL_THREAD (self));
492 : 4 : g_return_if_fail (self->thread != NULL);
493 : :
494 : 4 : g_cancellable_cancel (self->cancellable);
495 : 4 : g_thread_join (g_steal_pointer (&self->thread));
496 : : }
497 : :
498 : : /* Whether fake-desktop-portal is supported on this platform. This basically
499 : : * means whether _g_fd_query_path() will work at runtime. */
500 : : gboolean
501 : 4 : g_fake_desktop_portal_is_supported (void)
502 : : {
503 : : #ifdef __GNU__
504 : : return FALSE;
505 : : #else
506 : 4 : return TRUE;
507 : : #endif
508 : : }
|