1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22#include "../SDL_dialog_utils.h"
23
24#include "../../core/linux/SDL_dbus.h"
25
26#ifdef SDL_USE_LIBDBUS
27
28#include <errno.h>
29#include <sys/types.h>
30#include <sys/wait.h>
31#include <unistd.h>
32
33#define PORTAL_DESTINATION "org.freedesktop.portal.Desktop"
34#define PORTAL_PATH "/org/freedesktop/portal/desktop"
35#define PORTAL_INTERFACE "org.freedesktop.portal.FileChooser"
36
37#define SIGNAL_SENDER "org.freedesktop.portal.Desktop"
38#define SIGNAL_INTERFACE "org.freedesktop.portal.Request"
39#define SIGNAL_NAME "Response"
40#define SIGNAL_FILTER "type='signal', sender='"SIGNAL_SENDER"', interface='"SIGNAL_INTERFACE"', member='"SIGNAL_NAME"', path='"
41
42#define HANDLE_LEN 10
43
44#define WAYLAND_HANDLE_PREFIX "wayland:"
45#define X11_HANDLE_PREFIX "x11:"
46
47typedef struct {
48 SDL_DialogFileCallback callback;
49 void *userdata;
50 const char *path;
51} SignalCallback;
52
53static void DBus_AppendStringOption(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key, const char *value)
54{
55 DBusMessageIter options_pair, options_value;
56
57 dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair);
58 dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key);
59 dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "s", &options_value);
60 dbus->message_iter_append_basic(&options_value, DBUS_TYPE_STRING, &value);
61 dbus->message_iter_close_container(&options_pair, &options_value);
62 dbus->message_iter_close_container(options, &options_pair);
63}
64
65static void DBus_AppendBoolOption(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key, int value)
66{
67 DBusMessageIter options_pair, options_value;
68
69 dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair);
70 dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key);
71 dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "b", &options_value);
72 dbus->message_iter_append_basic(&options_value, DBUS_TYPE_BOOLEAN, &value);
73 dbus->message_iter_close_container(&options_pair, &options_value);
74 dbus->message_iter_close_container(options, &options_pair);
75}
76
77static void DBus_AppendFilter(SDL_DBusContext *dbus, DBusMessageIter *parent, const SDL_DialogFileFilter filter)
78{
79 DBusMessageIter filter_entry, filter_array, filter_array_entry;
80 char *state = NULL, *patterns, *pattern, *glob_pattern;
81 int zero = 0;
82
83 dbus->message_iter_open_container(parent, DBUS_TYPE_STRUCT, NULL, &filter_entry);
84 dbus->message_iter_append_basic(&filter_entry, DBUS_TYPE_STRING, &filter.name);
85 dbus->message_iter_open_container(&filter_entry, DBUS_TYPE_ARRAY, "(us)", &filter_array);
86
87 patterns = SDL_strdup(filter.pattern);
88 if (!patterns) {
89 goto cleanup;
90 }
91
92 pattern = SDL_strtok_r(patterns, ";", &state);
93 while (pattern) {
94 size_t max_len = SDL_strlen(pattern) + 3;
95
96 dbus->message_iter_open_container(&filter_array, DBUS_TYPE_STRUCT, NULL, &filter_array_entry);
97 dbus->message_iter_append_basic(&filter_array_entry, DBUS_TYPE_UINT32, &zero);
98
99 glob_pattern = SDL_calloc(max_len, sizeof(char));
100 if (!glob_pattern) {
101 goto cleanup;
102 }
103 glob_pattern[0] = '*';
104 /* Special case: The '*' filter doesn't need to be rewritten */
105 if (pattern[0] != '*' || pattern[1]) {
106 glob_pattern[1] = '.';
107 SDL_strlcat(glob_pattern + 2, pattern, max_len);
108 }
109 dbus->message_iter_append_basic(&filter_array_entry, DBUS_TYPE_STRING, &glob_pattern);
110 SDL_free(glob_pattern);
111
112 dbus->message_iter_close_container(&filter_array, &filter_array_entry);
113 pattern = SDL_strtok_r(NULL, ";", &state);
114 }
115
116cleanup:
117 SDL_free(patterns);
118
119 dbus->message_iter_close_container(&filter_entry, &filter_array);
120 dbus->message_iter_close_container(parent, &filter_entry);
121}
122
123static void DBus_AppendFilters(SDL_DBusContext *dbus, DBusMessageIter *options, const SDL_DialogFileFilter *filters, int nfilters)
124{
125 DBusMessageIter options_pair, options_value, options_value_array;
126 static const char *filters_name = "filters";
127
128 dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair);
129 dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &filters_name);
130 dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "a(sa(us))", &options_value);
131 dbus->message_iter_open_container(&options_value, DBUS_TYPE_ARRAY, "(sa(us))", &options_value_array);
132 for (int i = 0; i < nfilters; i++) {
133 DBus_AppendFilter(dbus, &options_value_array, filters[i]);
134 }
135 dbus->message_iter_close_container(&options_value, &options_value_array);
136 dbus->message_iter_close_container(&options_pair, &options_value);
137 dbus->message_iter_close_container(options, &options_pair);
138}
139
140static void DBus_AppendByteArray(SDL_DBusContext *dbus, DBusMessageIter *options, const char *key, const char *value)
141{
142 DBusMessageIter options_pair, options_value, options_array;
143
144 dbus->message_iter_open_container(options, DBUS_TYPE_DICT_ENTRY, NULL, &options_pair);
145 dbus->message_iter_append_basic(&options_pair, DBUS_TYPE_STRING, &key);
146 dbus->message_iter_open_container(&options_pair, DBUS_TYPE_VARIANT, "ay", &options_value);
147 dbus->message_iter_open_container(&options_value, DBUS_TYPE_ARRAY, "y", &options_array);
148 do {
149 dbus->message_iter_append_basic(&options_array, DBUS_TYPE_BYTE, value);
150 } while (*value++);
151 dbus->message_iter_close_container(&options_value, &options_array);
152 dbus->message_iter_close_container(&options_pair, &options_value);
153 dbus->message_iter_close_container(options, &options_pair);
154}
155
156static DBusHandlerResult DBus_MessageFilter(DBusConnection *conn, DBusMessage *msg, void *data)
157{
158 SDL_DBusContext *dbus = SDL_DBus_GetContext();
159 SignalCallback *signal_data = (SignalCallback *)data;
160
161 if (dbus->message_is_signal(msg, SIGNAL_INTERFACE, SIGNAL_NAME) &&
162 dbus->message_has_path(msg, signal_data->path)) {
163 DBusMessageIter signal_iter, result_array, array_entry, value_entry, uri_entry;
164 uint32_t result;
165 size_t length = 2, current = 0;
166 const char **path = NULL;
167
168 dbus->message_iter_init(msg, &signal_iter);
169 // Check if the parameters are what we expect
170 if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_UINT32) {
171 goto not_our_signal;
172 }
173 dbus->message_iter_get_basic(&signal_iter, &result);
174
175 if (result == 1 || result == 2) {
176 // cancelled
177 const char *result_data[] = { NULL };
178 signal_data->callback(signal_data->userdata, result_data, -1); // TODO: Set this to the last selected filter
179 goto done;
180
181 } else if (result) {
182 // some error occurred
183 signal_data->callback(signal_data->userdata, NULL, -1);
184 goto done;
185 }
186
187 if (!dbus->message_iter_next(&signal_iter)) {
188 goto not_our_signal;
189 }
190
191 if (dbus->message_iter_get_arg_type(&signal_iter) != DBUS_TYPE_ARRAY) {
192 goto not_our_signal;
193 }
194
195 dbus->message_iter_recurse(&signal_iter, &result_array);
196
197 while (dbus->message_iter_get_arg_type(&result_array) == DBUS_TYPE_DICT_ENTRY) {
198 const char *method;
199
200 dbus->message_iter_recurse(&result_array, &array_entry);
201 if (dbus->message_iter_get_arg_type(&array_entry) != DBUS_TYPE_STRING) {
202 goto not_our_signal;
203 }
204
205 dbus->message_iter_get_basic(&array_entry, &method);
206 if (!SDL_strcmp(method, "uris")) {
207 // we only care about the selected file paths
208 break;
209 }
210
211 if (!dbus->message_iter_next(&result_array)) {
212 goto not_our_signal;
213 }
214 }
215
216 if (!dbus->message_iter_next(&array_entry)) {
217 goto not_our_signal;
218 }
219
220 if (dbus->message_iter_get_arg_type(&array_entry) != DBUS_TYPE_VARIANT) {
221 goto not_our_signal;
222 }
223 dbus->message_iter_recurse(&array_entry, &value_entry);
224
225 if (dbus->message_iter_get_arg_type(&value_entry) != DBUS_TYPE_ARRAY) {
226 goto not_our_signal;
227 }
228 dbus->message_iter_recurse(&value_entry, &uri_entry);
229
230 path = SDL_malloc(length * sizeof(const char *));
231 if (!path) {
232 signal_data->callback(signal_data->userdata, NULL, -1);
233 goto done;
234 }
235
236 while (dbus->message_iter_get_arg_type(&uri_entry) == DBUS_TYPE_STRING) {
237 const char *uri = NULL;
238
239 if (current >= length - 1) {
240 ++length;
241 const char **newpath = SDL_realloc(path, length * sizeof(const char *));
242 if (!newpath) {
243 signal_data->callback(signal_data->userdata, NULL, -1);
244 goto done;
245 }
246 path = newpath;
247 }
248
249 dbus->message_iter_get_basic(&uri_entry, &uri);
250
251 // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileChooser.html
252 // Returned paths will always start with 'file://'; SDL_URIToLocal() truncates it.
253 char *decoded_uri = SDL_malloc(SDL_strlen(uri) + 1);
254 if (SDL_URIToLocal(uri, decoded_uri)) {
255 path[current] = decoded_uri;
256 } else {
257 SDL_free(decoded_uri);
258 SDL_SetError("Portal dialogs: Unsupported protocol: %s", uri);
259 signal_data->callback(signal_data->userdata, NULL, -1);
260 goto done;
261 }
262
263 dbus->message_iter_next(&uri_entry);
264 ++current;
265 }
266 path[current] = NULL;
267 signal_data->callback(signal_data->userdata, path, -1); // TODO: Fetch the index of the filter that was used
268done:
269 dbus->connection_remove_filter(conn, &DBus_MessageFilter, signal_data);
270
271 if (path) {
272 for (size_t i = 0; i < current; ++i) {
273 SDL_free((char *)path[i]);
274 }
275 SDL_free(path);
276 }
277 SDL_free((void *)signal_data->path);
278 SDL_free(signal_data);
279 return DBUS_HANDLER_RESULT_HANDLED;
280 }
281
282not_our_signal:
283 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
284}
285
286void SDL_Portal_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
287{
288 const char *method;
289 const char *method_title;
290
291 SDL_Window* window = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, NULL);
292 SDL_DialogFileFilter *filters = SDL_GetPointerProperty(props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, NULL);
293 int nfilters = (int) SDL_GetNumberProperty(props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, 0);
294 bool allow_many = SDL_GetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false);
295 const char* default_location = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, NULL);
296 const char* accept = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, NULL);
297 bool open_folders = false;
298
299 switch (type) {
300 case SDL_FILEDIALOG_OPENFILE:
301 method = "OpenFile";
302 method_title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, "Open File");
303 break;
304
305 case SDL_FILEDIALOG_SAVEFILE:
306 method = "SaveFile";
307 method_title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, "Save File");
308 break;
309
310 case SDL_FILEDIALOG_OPENFOLDER:
311 method = "OpenFile";
312 method_title = SDL_GetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, "Open Folder");
313 open_folders = true;
314 break;
315
316 default:
317 /* This is already checked in ../SDL_dialog.c; this silences compiler warnings */
318 SDL_SetError("Invalid file dialog type: %d", type);
319 callback(userdata, NULL, -1);
320 return;
321 }
322
323 SDL_DBusContext *dbus = SDL_DBus_GetContext();
324 DBusMessage *msg;
325 DBusMessageIter params, options;
326 const char *signal_id = NULL;
327 char *handle_str, *filter;
328 int filter_len;
329 static uint32_t handle_id = 0;
330 static char *default_parent_window = "";
331 SDL_PropertiesID window_props = SDL_GetWindowProperties(window);
332
333 const char *err_msg = validate_filters(filters, nfilters);
334
335 if (err_msg) {
336 SDL_SetError("%s", err_msg);
337 callback(userdata, NULL, -1);
338 return;
339 }
340
341 if (dbus == NULL) {
342 SDL_SetError("Failed to connect to DBus");
343 callback(userdata, NULL, -1);
344 return;
345 }
346
347 msg = dbus->message_new_method_call(PORTAL_DESTINATION, PORTAL_PATH, PORTAL_INTERFACE, method);
348 if (msg == NULL) {
349 SDL_SetError("Failed to send message to portal");
350 callback(userdata, NULL, -1);
351 return;
352 }
353
354 dbus->message_iter_init_append(msg, &params);
355
356 handle_str = default_parent_window;
357 if (window_props) {
358 const char *parent_handle = SDL_GetStringProperty(window_props, SDL_PROP_WINDOW_WAYLAND_XDG_TOPLEVEL_EXPORT_HANDLE_STRING, NULL);
359 if (parent_handle) {
360 size_t len = SDL_strlen(parent_handle);
361 len += sizeof(WAYLAND_HANDLE_PREFIX) + 1;
362 handle_str = SDL_malloc(len * sizeof(char));
363 if (!handle_str) {
364 callback(userdata, NULL, -1);
365 return;
366 }
367
368 SDL_snprintf(handle_str, len, "%s%s", WAYLAND_HANDLE_PREFIX, parent_handle);
369 } else {
370 const Uint64 xid = (Uint64)SDL_GetNumberProperty(window_props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
371 if (xid) {
372 const size_t len = sizeof(X11_HANDLE_PREFIX) + 24; // A 64-bit number can be 20 characters max.
373 handle_str = SDL_malloc(len * sizeof(char));
374 if (!handle_str) {
375 callback(userdata, NULL, -1);
376 return;
377 }
378
379 // The portal wants X11 window ID numbers in hex.
380 SDL_snprintf(handle_str, len, "%s%" SDL_PRIx64, X11_HANDLE_PREFIX, xid);
381 }
382 }
383 }
384
385 dbus->message_iter_append_basic(&params, DBUS_TYPE_STRING, &handle_str);
386 if (handle_str != default_parent_window) {
387 SDL_free(handle_str);
388 }
389
390 dbus->message_iter_append_basic(&params, DBUS_TYPE_STRING, &method_title);
391 dbus->message_iter_open_container(&params, DBUS_TYPE_ARRAY, "{sv}", &options);
392
393 handle_str = SDL_malloc(sizeof(char) * (HANDLE_LEN + 1));
394 if (!handle_str) {
395 callback(userdata, NULL, -1);
396 return;
397 }
398 SDL_snprintf(handle_str, HANDLE_LEN, "%u", ++handle_id);
399 DBus_AppendStringOption(dbus, &options, "handle_token", handle_str);
400 SDL_free(handle_str);
401
402 DBus_AppendBoolOption(dbus, &options, "modal", !!window);
403 if (allow_many) {
404 DBus_AppendBoolOption(dbus, &options, "multiple", 1);
405 }
406 if (open_folders) {
407 DBus_AppendBoolOption(dbus, &options, "directory", 1);
408 }
409 if (filters) {
410 DBus_AppendFilters(dbus, &options, filters, nfilters);
411 }
412 if (default_location) {
413 DBus_AppendByteArray(dbus, &options, "current_folder", default_location);
414 }
415 if (accept) {
416 DBus_AppendStringOption(dbus, &options, "accept_label", accept);
417 }
418 dbus->message_iter_close_container(&params, &options);
419
420 DBusMessage *reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, DBUS_TIMEOUT_INFINITE, NULL);
421 if (reply) {
422 DBusMessageIter reply_iter;
423 dbus->message_iter_init(reply, &reply_iter);
424
425 if (dbus->message_iter_get_arg_type(&reply_iter) == DBUS_TYPE_OBJECT_PATH) {
426 dbus->message_iter_get_basic(&reply_iter, &signal_id);
427 }
428 }
429
430 if (!signal_id) {
431 SDL_SetError("Invalid response received by DBus");
432 callback(userdata, NULL, -1);
433 goto incorrect_type;
434 }
435
436 dbus->message_unref(msg);
437
438 filter_len = SDL_strlen(SIGNAL_FILTER) + SDL_strlen(signal_id) + 2;
439 filter = SDL_malloc(sizeof(char) * filter_len);
440 if (!filter) {
441 callback(userdata, NULL, -1);
442 goto incorrect_type;
443 }
444
445 SDL_snprintf(filter, filter_len, SIGNAL_FILTER"%s'", signal_id);
446 dbus->bus_add_match(dbus->session_conn, filter, NULL);
447 SDL_free(filter);
448
449 SignalCallback *data = SDL_malloc(sizeof(SignalCallback));
450 if (!data) {
451 callback(userdata, NULL, -1);
452 goto incorrect_type;
453 }
454 data->callback = callback;
455 data->userdata = userdata;
456 data->path = SDL_strdup(signal_id);
457 if (!data->path) {
458 SDL_free(data);
459 callback(userdata, NULL, -1);
460 goto incorrect_type;
461 }
462
463 /* TODO: This should be registered before opening the portal, or the filter will not catch
464 the message if it is sent before we register the filter.
465 */
466 dbus->connection_add_filter(dbus->session_conn,
467 &DBus_MessageFilter, data, NULL);
468 dbus->connection_flush(dbus->session_conn);
469
470incorrect_type:
471 dbus->message_unref(reply);
472}
473
474bool SDL_Portal_detect(void)
475{
476 SDL_DBusContext *dbus = SDL_DBus_GetContext();
477 DBusMessage *msg = NULL, *reply = NULL;
478 char *reply_str = NULL;
479 DBusMessageIter reply_iter;
480 static int portal_present = -1;
481
482 // No need for this if the result is cached.
483 if (portal_present != -1) {
484 return (portal_present > 0);
485 }
486
487 portal_present = 0;
488
489 if (!dbus) {
490 SDL_SetError("%s", "Failed to connect to DBus!");
491 return false;
492 }
493
494 // Use introspection to get the available services.
495 msg = dbus->message_new_method_call(PORTAL_DESTINATION, PORTAL_PATH, "org.freedesktop.DBus.Introspectable", "Introspect");
496 if (!msg) {
497 goto done;
498 }
499
500 reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, DBUS_TIMEOUT_USE_DEFAULT, NULL);
501 dbus->message_unref(msg);
502 if (!reply) {
503 goto done;
504 }
505
506 if (!dbus->message_iter_init(reply, &reply_iter)) {
507 goto done;
508 }
509
510 if (dbus->message_iter_get_arg_type(&reply_iter) != DBUS_TYPE_STRING) {
511 goto done;
512 }
513
514 /* Introspection gives us a dump of all the services on the destination in XML format, so search the
515 * giant string for the file chooser protocol.
516 */
517 dbus->message_iter_get_basic(&reply_iter, &reply_str);
518 if (SDL_strstr(reply_str, PORTAL_INTERFACE)) {
519 portal_present = 1; // Found it!
520 }
521
522done:
523 if (reply) {
524 dbus->message_unref(reply);
525 }
526
527 return (portal_present > 0);
528}
529
530#else
531
532// Dummy implementation to avoid compilation problems
533
534void SDL_Portal_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props)
535{
536 SDL_Unsupported();
537 callback(userdata, NULL, -1);
538}
539
540bool SDL_Portal_detect(void)
541{
542 return false;
543}
544
545#endif // SDL_USE_LIBDBUS
546