tracy/nfd/nfd_portal.cpp

1415 lines
56 KiB
C++
Raw Normal View History

2022-04-20 16:09:28 +00:00
/*
Native File Dialog Extended
Repository: https://github.com/btzy/nativefiledialog-extended
License: Zlib
Authors: Bernard Teo
Note: We do not check for malloc failure on Linux - Linux overcommits memory!
*/
#include <assert.h>
#include <dbus/dbus.h>
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h> // for the random token string
#include <unistd.h> // for access()
#include "nfd.h"
/*
Define NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION to 0 if you don't want the file extension to be
appended when missing. Linux programs usually doesn't append the file extension, but for consistency
with other OSes we append it by default.
*/
#ifndef NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION
#define NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION 1
#endif
namespace {
template <typename T = void>
T* NFDi_Malloc(size_t bytes) {
void* ptr = malloc(bytes);
assert(ptr); // Linux malloc never fails
return static_cast<T*>(ptr);
}
template <typename T>
void NFDi_Free(T* ptr) {
assert(ptr);
free(static_cast<void*>(ptr));
}
template <typename T>
struct Free_Guard {
T* data;
Free_Guard(T* freeable) noexcept : data(freeable) {}
~Free_Guard() { NFDi_Free(data); }
};
template <typename T>
struct FreeCheck_Guard {
T* data;
FreeCheck_Guard(T* freeable = nullptr) noexcept : data(freeable) {}
~FreeCheck_Guard() {
if (data) NFDi_Free(data);
}
};
struct DBusMessage_Guard {
DBusMessage* data;
DBusMessage_Guard(DBusMessage* freeable) noexcept : data(freeable) {}
~DBusMessage_Guard() { dbus_message_unref(data); }
};
/* D-Bus connection handle */
DBusConnection* dbus_conn;
/* current D-Bus error */
DBusError dbus_err;
/* current error (may be a pointer to the D-Bus error message above, or a pointer to some string
* literal) */
const char* err_ptr = nullptr;
/* the unique name of our connection, used for the Request handle; owned by D-Bus so we don't free
* it */
const char* dbus_unique_name;
void NFDi_SetError(const char* msg) {
err_ptr = msg;
}
template <typename T>
T* copy(const T* begin, const T* end, T* out) {
for (; begin != end; ++begin) {
*out++ = *begin;
}
return out;
}
template <typename T, typename Callback>
T* transform(const T* begin, const T* end, T* out, Callback callback) {
for (; begin != end; ++begin) {
*out++ = callback(*begin);
}
return out;
}
constexpr const char* STR_EMPTY = "";
constexpr const char* STR_OPEN_FILE = "Open File";
constexpr const char* STR_OPEN_FILES = "Open Files";
constexpr const char* STR_SAVE_FILE = "Save File";
constexpr const char* STR_SELECT_FOLDER = "Select Folder";
constexpr const char* STR_HANDLE_TOKEN = "handle_token";
constexpr const char* STR_MULTIPLE = "multiple";
constexpr const char* STR_DIRECTORY = "directory";
constexpr const char* STR_FILTERS = "filters";
constexpr const char* STR_CURRENT_FILTER = "current_filter";
constexpr const char* STR_CURRENT_NAME = "current_name";
constexpr const char* STR_CURRENT_FOLDER = "current_folder";
constexpr const char* STR_CURRENT_FILE = "current_file";
constexpr const char* STR_ALL_FILES = "All files";
constexpr const char* STR_ASTERISK = "*";
template <bool Multiple, bool Directory>
void AppendOpenFileQueryTitle(DBusMessageIter&);
template <>
void AppendOpenFileQueryTitle<false, false>(DBusMessageIter& iter) {
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_OPEN_FILE);
}
template <>
void AppendOpenFileQueryTitle<true, false>(DBusMessageIter& iter) {
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_OPEN_FILES);
}
template <>
void AppendOpenFileQueryTitle<false, true>(DBusMessageIter& iter) {
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_SELECT_FOLDER);
}
void AppendSaveFileQueryTitle(DBusMessageIter& iter) {
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_SAVE_FILE);
}
void AppendOpenFileQueryDictEntryHandleToken(DBusMessageIter& sub_iter, const char* handle_token) {
DBusMessageIter sub_sub_iter;
DBusMessageIter variant_iter;
dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter);
dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_HANDLE_TOKEN);
dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "s", &variant_iter);
dbus_message_iter_append_basic(&variant_iter, DBUS_TYPE_STRING, &handle_token);
dbus_message_iter_close_container(&sub_sub_iter, &variant_iter);
dbus_message_iter_close_container(&sub_iter, &sub_sub_iter);
}
template <bool Multiple>
void AppendOpenFileQueryDictEntryMultiple(DBusMessageIter&);
template <>
void AppendOpenFileQueryDictEntryMultiple<true>(DBusMessageIter& sub_iter) {
DBusMessageIter sub_sub_iter;
DBusMessageIter variant_iter;
dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter);
dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_MULTIPLE);
dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "b", &variant_iter);
{
int b = true;
dbus_message_iter_append_basic(&variant_iter, DBUS_TYPE_BOOLEAN, &b);
}
dbus_message_iter_close_container(&sub_sub_iter, &variant_iter);
dbus_message_iter_close_container(&sub_iter, &sub_sub_iter);
}
template <>
void AppendOpenFileQueryDictEntryMultiple<false>(DBusMessageIter&) {}
template <bool Directory>
void AppendOpenFileQueryDictEntryDirectory(DBusMessageIter&);
template <>
void AppendOpenFileQueryDictEntryDirectory<true>(DBusMessageIter& sub_iter) {
DBusMessageIter sub_sub_iter;
DBusMessageIter variant_iter;
dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter);
dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_DIRECTORY);
dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "b", &variant_iter);
{
int b = true;
dbus_message_iter_append_basic(&variant_iter, DBUS_TYPE_BOOLEAN, &b);
}
dbus_message_iter_close_container(&sub_sub_iter, &variant_iter);
dbus_message_iter_close_container(&sub_iter, &sub_sub_iter);
}
template <>
void AppendOpenFileQueryDictEntryDirectory<false>(DBusMessageIter&) {}
void AppendSingleFilter(DBusMessageIter& base_iter, const nfdnfilteritem_t& filter) {
DBusMessageIter filter_list_struct_iter;
DBusMessageIter filter_sublist_iter;
DBusMessageIter filter_sublist_struct_iter;
dbus_message_iter_open_container(
&base_iter, DBUS_TYPE_STRUCT, nullptr, &filter_list_struct_iter);
// count number of file extensions
size_t sep = 1;
for (const char* p = filter.spec; *p; ++p) {
if (*p == L',') {
++sep;
}
}
{
const size_t name_len = strlen(filter.name);
const size_t spec_len = strlen(filter.spec);
char* buf = static_cast<char*>(alloca(sep + name_len + 2 + spec_len + 1));
char* buf_end = buf;
buf_end = copy(filter.name, filter.name + name_len, buf_end);
*buf_end++ = ' ';
*buf_end++ = '(';
const char* spec_ptr = filter.spec;
do {
*buf_end++ = *spec_ptr;
if (*spec_ptr == ',') *buf_end++ = ' ';
++spec_ptr;
} while (*spec_ptr != '\0');
*buf_end++ = ')';
*buf_end = '\0';
dbus_message_iter_append_basic(&filter_list_struct_iter, DBUS_TYPE_STRING, &buf);
}
{
dbus_message_iter_open_container(
&filter_list_struct_iter, DBUS_TYPE_ARRAY, "(us)", &filter_sublist_iter);
const char* extn_begin = filter.spec;
const char* extn_end = extn_begin;
while (true) {
dbus_message_iter_open_container(
&filter_sublist_iter, DBUS_TYPE_STRUCT, nullptr, &filter_sublist_struct_iter);
{
const unsigned zero = 0;
dbus_message_iter_append_basic(
&filter_sublist_struct_iter, DBUS_TYPE_UINT32, &zero);
}
do {
++extn_end;
} while (*extn_end != ',' && *extn_end != '\0');
char* buf = static_cast<char*>(alloca(2 + (extn_end - extn_begin) + 1));
char* buf_end = buf;
*buf_end++ = '*';
*buf_end++ = '.';
buf_end = copy(extn_begin, extn_end, buf_end);
*buf_end = '\0';
dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf);
dbus_message_iter_close_container(&filter_sublist_iter, &filter_sublist_struct_iter);
if (*extn_end == '\0') {
break;
}
extn_begin = extn_end + 1;
extn_end = extn_begin;
}
}
dbus_message_iter_close_container(&filter_list_struct_iter, &filter_sublist_iter);
dbus_message_iter_close_container(&base_iter, &filter_list_struct_iter);
}
bool AppendSingleFilterCheckExtn(DBusMessageIter& base_iter,
const nfdnfilteritem_t& filter,
const nfdnchar_t* match_extn) {
DBusMessageIter filter_list_struct_iter;
DBusMessageIter filter_sublist_iter;
DBusMessageIter filter_sublist_struct_iter;
dbus_message_iter_open_container(
&base_iter, DBUS_TYPE_STRUCT, nullptr, &filter_list_struct_iter);
// count number of file extensions
size_t sep = 1;
for (const char* p = filter.spec; *p; ++p) {
if (*p == L',') {
++sep;
}
}
{
const size_t name_len = strlen(filter.name);
const size_t spec_len = strlen(filter.spec);
char* buf = static_cast<char*>(alloca(sep + name_len + 2 + spec_len + 1));
char* buf_end = buf;
buf_end = copy(filter.name, filter.name + name_len, buf_end);
*buf_end++ = ' ';
*buf_end++ = '(';
const char* spec_ptr = filter.spec;
do {
*buf_end++ = *spec_ptr;
if (*spec_ptr == ',') *buf_end++ = ' ';
++spec_ptr;
} while (*spec_ptr != '\0');
*buf_end++ = ')';
*buf_end = '\0';
dbus_message_iter_append_basic(&filter_list_struct_iter, DBUS_TYPE_STRING, &buf);
}
bool extn_matched = false;
{
dbus_message_iter_open_container(
&filter_list_struct_iter, DBUS_TYPE_ARRAY, "(us)", &filter_sublist_iter);
const char* extn_begin = filter.spec;
const char* extn_end = extn_begin;
while (true) {
dbus_message_iter_open_container(
&filter_sublist_iter, DBUS_TYPE_STRUCT, nullptr, &filter_sublist_struct_iter);
{
const unsigned zero = 0;
dbus_message_iter_append_basic(
&filter_sublist_struct_iter, DBUS_TYPE_UINT32, &zero);
}
do {
++extn_end;
} while (*extn_end != ',' && *extn_end != '\0');
char* buf = static_cast<char*>(alloca(2 + (extn_end - extn_begin) + 1));
char* buf_end = buf;
*buf_end++ = '*';
*buf_end++ = '.';
buf_end = copy(extn_begin, extn_end, buf_end);
*buf_end = '\0';
dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &buf);
dbus_message_iter_close_container(&filter_sublist_iter, &filter_sublist_struct_iter);
if (!extn_matched) {
const char* match_extn_p;
const char* p;
for (p = extn_begin, match_extn_p = match_extn; p != extn_end && *match_extn_p;
++p, ++match_extn_p) {
if (*p != *match_extn_p) break;
}
if (p == extn_end && !*match_extn_p) {
extn_matched = true;
}
}
if (*extn_end == '\0') {
break;
}
extn_begin = extn_end + 1;
extn_end = extn_begin;
}
}
dbus_message_iter_close_container(&filter_list_struct_iter, &filter_sublist_iter);
dbus_message_iter_close_container(&base_iter, &filter_list_struct_iter);
return extn_matched;
}
void AppendWildcardFilter(DBusMessageIter& base_iter) {
DBusMessageIter filter_list_struct_iter;
DBusMessageIter filter_sublist_iter;
DBusMessageIter filter_sublist_struct_iter;
dbus_message_iter_open_container(
&base_iter, DBUS_TYPE_STRUCT, nullptr, &filter_list_struct_iter);
dbus_message_iter_append_basic(&filter_list_struct_iter, DBUS_TYPE_STRING, &STR_ALL_FILES);
dbus_message_iter_open_container(
&filter_list_struct_iter, DBUS_TYPE_ARRAY, "(us)", &filter_sublist_iter);
dbus_message_iter_open_container(
&filter_sublist_iter, DBUS_TYPE_STRUCT, nullptr, &filter_sublist_struct_iter);
{
const unsigned zero = 0;
dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_UINT32, &zero);
}
dbus_message_iter_append_basic(&filter_sublist_struct_iter, DBUS_TYPE_STRING, &STR_ASTERISK);
dbus_message_iter_close_container(&filter_sublist_iter, &filter_sublist_struct_iter);
dbus_message_iter_close_container(&filter_list_struct_iter, &filter_sublist_iter);
dbus_message_iter_close_container(&base_iter, &filter_list_struct_iter);
}
template <bool FilterEnabled>
void AppendOpenFileQueryDictEntryFilters(DBusMessageIter&,
const nfdnfilteritem_t*,
nfdfiltersize_t);
template <>
void AppendOpenFileQueryDictEntryFilters<true>(DBusMessageIter& sub_iter,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) {
if (filterCount != 0) {
DBusMessageIter sub_sub_iter;
DBusMessageIter variant_iter;
DBusMessageIter filter_list_iter;
// filters
dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter);
dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_FILTERS);
dbus_message_iter_open_container(
&sub_sub_iter, DBUS_TYPE_VARIANT, "a(sa(us))", &variant_iter);
dbus_message_iter_open_container(
&variant_iter, DBUS_TYPE_ARRAY, "(sa(us))", &filter_list_iter);
for (nfdfiltersize_t i = 0; i != filterCount; ++i) {
AppendSingleFilter(filter_list_iter, filterList[i]);
}
AppendWildcardFilter(filter_list_iter);
dbus_message_iter_close_container(&variant_iter, &filter_list_iter);
dbus_message_iter_close_container(&sub_sub_iter, &variant_iter);
dbus_message_iter_close_container(&sub_iter, &sub_sub_iter);
// current_filter
dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter);
dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_CURRENT_FILTER);
dbus_message_iter_open_container(
&sub_sub_iter, DBUS_TYPE_VARIANT, "(sa(us))", &variant_iter);
AppendSingleFilter(variant_iter, filterList[0]);
dbus_message_iter_close_container(&sub_sub_iter, &variant_iter);
dbus_message_iter_close_container(&sub_iter, &sub_sub_iter);
}
}
template <>
void AppendOpenFileQueryDictEntryFilters<false>(DBusMessageIter&,
const nfdnfilteritem_t*,
nfdfiltersize_t) {}
void AppendSaveFileQueryDictEntryFilters(DBusMessageIter& sub_iter,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultName) {
if (filterCount != 0) {
DBusMessageIter sub_sub_iter;
DBusMessageIter variant_iter;
DBusMessageIter filter_list_iter;
// The extension of the defaultName (without the '.'). If NULL, it means that there is no
// extension.
const nfdnchar_t* extn = NULL;
if (defaultName) {
const nfdnchar_t* p = defaultName;
while (*p) ++p;
while (*--p != '.')
;
++p;
if (*p) extn = p;
}
bool extn_matched = false;
size_t selected_filter_index;
// filters
dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter);
dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_FILTERS);
dbus_message_iter_open_container(
&sub_sub_iter, DBUS_TYPE_VARIANT, "a(sa(us))", &variant_iter);
dbus_message_iter_open_container(
&variant_iter, DBUS_TYPE_ARRAY, "(sa(us))", &filter_list_iter);
for (nfdfiltersize_t i = 0; i != filterCount; ++i) {
if (!extn_matched && extn) {
extn_matched = AppendSingleFilterCheckExtn(filter_list_iter, filterList[i], extn);
if (extn_matched) selected_filter_index = i;
} else {
AppendSingleFilter(filter_list_iter, filterList[i]);
}
}
AppendWildcardFilter(filter_list_iter);
dbus_message_iter_close_container(&variant_iter, &filter_list_iter);
dbus_message_iter_close_container(&sub_sub_iter, &variant_iter);
dbus_message_iter_close_container(&sub_iter, &sub_sub_iter);
// current_filter
dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter);
dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_CURRENT_FILTER);
dbus_message_iter_open_container(
&sub_sub_iter, DBUS_TYPE_VARIANT, "(sa(us))", &variant_iter);
if (extn_matched) {
AppendSingleFilter(variant_iter, filterList[selected_filter_index]);
} else {
AppendWildcardFilter(variant_iter);
}
dbus_message_iter_close_container(&sub_sub_iter, &variant_iter);
dbus_message_iter_close_container(&sub_iter, &sub_sub_iter);
}
}
void AppendSaveFileQueryDictEntryCurrentName(DBusMessageIter& sub_iter, const char* name) {
if (!name) return;
DBusMessageIter sub_sub_iter;
DBusMessageIter variant_iter;
dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter);
dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_CURRENT_NAME);
dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "s", &variant_iter);
dbus_message_iter_append_basic(&variant_iter, DBUS_TYPE_STRING, &name);
dbus_message_iter_close_container(&sub_sub_iter, &variant_iter);
dbus_message_iter_close_container(&sub_iter, &sub_sub_iter);
}
void AppendSaveFileQueryDictEntryCurrentFolder(DBusMessageIter& sub_iter, const char* path) {
if (!path) return;
DBusMessageIter sub_sub_iter;
DBusMessageIter variant_iter;
DBusMessageIter array_iter;
dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter);
dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_CURRENT_FOLDER);
dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "ay", &variant_iter);
dbus_message_iter_open_container(&variant_iter, DBUS_TYPE_ARRAY, "y", &array_iter);
// Append string as byte array, including the terminating null byte as required by the portal.
const char* p = path;
do {
dbus_message_iter_append_basic(&array_iter, DBUS_TYPE_BYTE, p);
} while (*p++);
dbus_message_iter_close_container(&variant_iter, &array_iter);
dbus_message_iter_close_container(&sub_sub_iter, &variant_iter);
dbus_message_iter_close_container(&sub_iter, &sub_sub_iter);
}
void AppendSaveFileQueryDictEntryCurrentFile(DBusMessageIter& sub_iter,
const char* path,
const char* name) {
if (!path || !name) return;
const size_t path_len = strlen(path);
const size_t name_len = strlen(name);
char* pathname;
char* pathname_end;
size_t pathname_len;
if (path_len && path[path_len - 1] == '/') {
pathname_len = path_len + name_len;
pathname = NFDi_Malloc<char>(pathname_len + 1);
pathname_end = pathname;
pathname_end = copy(path, path + path_len, pathname_end);
pathname_end = copy(name, name + name_len, pathname_end);
*pathname_end++ = '\0';
} else {
pathname_len = path_len + 1 + name_len;
pathname = NFDi_Malloc<char>(pathname_len + 1);
pathname_end = pathname;
pathname_end = copy(path, path + path_len, pathname_end);
*pathname_end++ = '/';
pathname_end = copy(name, name + name_len, pathname_end);
*pathname_end++ = '\0';
}
Free_Guard<char> guard(pathname);
if (access(pathname, F_OK) != 0) return;
DBusMessageIter sub_sub_iter;
DBusMessageIter variant_iter;
DBusMessageIter array_iter;
dbus_message_iter_open_container(&sub_iter, DBUS_TYPE_DICT_ENTRY, nullptr, &sub_sub_iter);
dbus_message_iter_append_basic(&sub_sub_iter, DBUS_TYPE_STRING, &STR_CURRENT_FILE);
dbus_message_iter_open_container(&sub_sub_iter, DBUS_TYPE_VARIANT, "ay", &variant_iter);
dbus_message_iter_open_container(&variant_iter, DBUS_TYPE_ARRAY, "y", &array_iter);
// This includes the terminating null character, which is required by the portal.
for (const char* p = pathname; p != pathname_end; ++p) {
dbus_message_iter_append_basic(&array_iter, DBUS_TYPE_BYTE, p);
}
dbus_message_iter_close_container(&variant_iter, &array_iter);
dbus_message_iter_close_container(&sub_sub_iter, &variant_iter);
dbus_message_iter_close_container(&sub_iter, &sub_sub_iter);
}
// Append OpenFile() portal params to the given query.
template <bool Multiple, bool Directory>
void AppendOpenFileQueryParams(DBusMessage* query,
const char* handle_token,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) {
DBusMessageIter iter;
dbus_message_iter_init_append(query, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY);
AppendOpenFileQueryTitle<Multiple, Directory>(iter);
DBusMessageIter sub_iter;
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &sub_iter);
AppendOpenFileQueryDictEntryHandleToken(sub_iter, handle_token);
AppendOpenFileQueryDictEntryMultiple<Multiple>(sub_iter);
AppendOpenFileQueryDictEntryDirectory<Directory>(sub_iter);
AppendOpenFileQueryDictEntryFilters<!Directory>(sub_iter, filterList, filterCount);
dbus_message_iter_close_container(&iter, &sub_iter);
}
// Append SaveFile() portal params to the given query.
void AppendSaveFileQueryParams(DBusMessage* query,
const char* handle_token,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath,
const nfdnchar_t* defaultName) {
DBusMessageIter iter;
dbus_message_iter_init_append(query, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_EMPTY);
AppendSaveFileQueryTitle(iter);
DBusMessageIter sub_iter;
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &sub_iter);
AppendOpenFileQueryDictEntryHandleToken(sub_iter, handle_token);
AppendSaveFileQueryDictEntryFilters(sub_iter, filterList, filterCount, defaultName);
AppendSaveFileQueryDictEntryCurrentName(sub_iter, defaultName);
AppendSaveFileQueryDictEntryCurrentFolder(sub_iter, defaultPath);
AppendSaveFileQueryDictEntryCurrentFile(sub_iter, defaultPath, defaultName);
dbus_message_iter_close_container(&iter, &sub_iter);
}
nfdresult_t ReadDictImpl(const char*, DBusMessageIter&) {
return NFD_OKAY;
}
template <typename Callback, typename... Args>
nfdresult_t ReadDictImpl(const char* key,
DBusMessageIter& iter,
const char*& candidate_key,
Callback& candidate_callback,
Args&... args) {
if (strcmp(key, candidate_key) == 0) {
// this is the correct callback
return candidate_callback(iter);
} else {
return ReadDictImpl(key, iter, args...);
}
}
// Read a dictionary from the given iterator. The type of the element under this iterator will be
// checked. The args are alternately key and callback. Key is a const char*, and callback is a
// function that returns nfdresult_t. Return NFD_ERROR to stop processing and return immediately.
template <typename... Args>
nfdresult_t ReadDict(DBusMessageIter iter, Args... args) {
if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) {
NFDi_SetError("D-Bus response signal argument is not an array.");
return NFD_ERROR;
}
DBusMessageIter sub_iter;
dbus_message_iter_recurse(&iter, &sub_iter);
while (dbus_message_iter_get_arg_type(&sub_iter) == DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter de_iter;
dbus_message_iter_recurse(&sub_iter, &de_iter);
if (dbus_message_iter_get_arg_type(&de_iter) != DBUS_TYPE_STRING) {
NFDi_SetError("D-Bus response signal dict entry does not start with a string.");
return NFD_ERROR;
}
const char* key;
dbus_message_iter_get_basic(&de_iter, &key);
if (!dbus_message_iter_next(&de_iter)) {
NFDi_SetError("D-Bus response signal dict entry is missing one or more arguments.");
return NFD_ERROR;
}
// unwrap the variant
if (dbus_message_iter_get_arg_type(&de_iter) != DBUS_TYPE_VARIANT) {
NFDi_SetError("D-Bus response signal dict entry value is not a variant.");
return NFD_ERROR;
}
DBusMessageIter de_variant_iter;
dbus_message_iter_recurse(&de_iter, &de_variant_iter);
if (ReadDictImpl(key, de_variant_iter, args...) == NFD_ERROR) return NFD_ERROR;
if (!dbus_message_iter_next(&sub_iter)) break;
}
return NFD_OKAY;
}
// Read the message, returning an iterator to the `results` dictionary of the response. If response
// was okay, then returns NFD_OKAY and set `resultsIter` to the results dictionary iterator (this is
// the iterator to the entire dictionary (which has type DBUS_TYPE_ARRAY), not an iterator to the
// first item in the dictionary). It does not check that this iterator is DBUS_TYPE_ARRAY; you
// should use ReadDict() which will check it. Otherwise, returns NFD_CANCEL or NFD_ERROR as
// appropriate, and does not modify `resultsIter`. `resultsIter` can be copied by value.
nfdresult_t ReadResponseResults(DBusMessage* msg, DBusMessageIter& resultsIter) {
DBusMessageIter iter;
if (!dbus_message_iter_init(msg, &iter)) {
NFDi_SetError("D-Bus response signal is missing one or more arguments.");
return NFD_ERROR;
}
if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) {
NFDi_SetError("D-Bus response signal argument is not a uint32.");
return NFD_ERROR;
}
dbus_uint32_t resp_code;
dbus_message_iter_get_basic(&iter, &resp_code);
if (resp_code != 0) {
if (resp_code == 1) {
// User pressed cancel
return NFD_CANCEL;
} else {
// Some error occurred
NFDi_SetError("D-Bus file dialog interaction was ended abruptly.");
return NFD_ERROR;
}
}
// User successfully responded
if (!dbus_message_iter_next(&iter)) {
NFDi_SetError("D-Bus response signal is missing one or more arguments.");
return NFD_ERROR;
}
resultsIter = iter;
return NFD_OKAY;
}
// Read the message. If response was okay, then returns NFD_OKAY and set `uriIter` to the URI array
// iterator. Otherwise, returns NFD_CANCEL or NFD_ERROR as appropriate, and does not modify
// `uriIter`. `uriIter` can be copied by value.
nfdresult_t ReadResponseUris(DBusMessage* msg, DBusMessageIter& uriIter) {
DBusMessageIter iter;
const nfdresult_t res = ReadResponseResults(msg, iter);
if (res != NFD_OKAY) return res;
bool has_uris = false;
if (ReadDict(iter, "uris", [&uriIter, &has_uris](DBusMessageIter& uris_iter) {
if (dbus_message_iter_get_arg_type(&uris_iter) != DBUS_TYPE_ARRAY) {
NFDi_SetError("D-Bus response signal URI iter is not an array.");
return NFD_ERROR;
}
dbus_message_iter_recurse(&uris_iter, &uriIter);
has_uris = true;
return NFD_OKAY;
}) == NFD_ERROR)
return NFD_ERROR;
if (!has_uris) {
NFDi_SetError("D-Bus response signal has no URI field.");
return NFD_ERROR;
}
return NFD_OKAY;
}
// Same as ReadResponseUris, but does not perform any message type checks.
// You should only use this if you previously used ReadResponseUris and it returned NFD_OKAY!
void ReadResponseUrisUnchecked(DBusMessage* msg, DBusMessageIter& uriIter) {
DBusMessageIter iter;
dbus_message_iter_init(msg, &iter);
dbus_message_iter_next(&iter);
ReadDict(iter, "uris", [&uriIter](DBusMessageIter& uris_iter) {
dbus_message_iter_recurse(&uris_iter, &uriIter);
return NFD_OKAY;
});
}
nfdpathsetsize_t ReadResponseUrisUncheckedGetArraySize(DBusMessage* msg) {
DBusMessageIter iter;
dbus_message_iter_init(msg, &iter);
dbus_message_iter_next(&iter);
nfdpathsetsize_t sz = 0; // Initialization will never be used, but we initialize it to prevent
// the uninitialized warning otherwise.
ReadDict(iter, "uris", [&sz](DBusMessageIter& uris_iter) {
sz = dbus_message_iter_get_element_count(&uris_iter);
return NFD_OKAY;
});
return sz;
}
// Read the response URI. If response was okay, then returns NFD_OKAY and set file to it (the
// pointer is set to some string owned by msg, so you should not manually free it). Otherwise,
// returns NFD_CANCEL or NFD_ERROR as appropriate, and does not modify `file`.
nfdresult_t ReadResponseUrisSingle(DBusMessage* msg, const char*& file) {
DBusMessageIter uri_iter;
const nfdresult_t res = ReadResponseUris(msg, uri_iter);
if (res != NFD_OKAY) return res; // can be NFD_CANCEL or NFD_ERROR
if (dbus_message_iter_get_arg_type(&uri_iter) != DBUS_TYPE_STRING) {
NFDi_SetError("D-Bus response signal URI sub iter is not an string.");
return NFD_ERROR;
}
dbus_message_iter_get_basic(&uri_iter, &file);
return NFD_OKAY;
}
#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1
// Read the response URI and selected extension (in the form "*.abc" or "*") (if any). If response
// was okay, then returns NFD_OKAY and set file and extn to them (the pointer is set to some string
// owned by msg, so you should not manually free it). `file` is the user-entered file name, and
// `extn` is the selected file extension (the first one if there are multiple extensions in the
// selected option) (this is NULL if "All files" is selected). Otherwise, returns NFD_CANCEL or
// NFD_ERROR as appropriate, and does not modify `file` and `extn`.
nfdresult_t ReadResponseUrisSingleAndCurrentExtension(DBusMessage* msg,
const char*& file,
const char*& extn) {
DBusMessageIter iter;
const nfdresult_t res = ReadResponseResults(msg, iter);
if (res != NFD_OKAY) return res;
const char* tmp_file = nullptr;
const char* tmp_extn = nullptr;
if (ReadDict(
iter,
"uris",
[&tmp_file](DBusMessageIter& uris_iter) {
if (dbus_message_iter_get_arg_type(&uris_iter) != DBUS_TYPE_ARRAY) {
NFDi_SetError("D-Bus response signal URI iter is not an array.");
return NFD_ERROR;
}
DBusMessageIter uri_iter;
dbus_message_iter_recurse(&uris_iter, &uri_iter);
if (dbus_message_iter_get_arg_type(&uri_iter) != DBUS_TYPE_STRING) {
NFDi_SetError("D-Bus response signal URI sub iter is not a string.");
return NFD_ERROR;
}
dbus_message_iter_get_basic(&uri_iter, &tmp_file);
return NFD_OKAY;
},
"current_filter",
[&tmp_extn](DBusMessageIter& current_filter_iter) {
// current_filter is best_effort, so if we fail, we still return NFD_OKAY.
if (dbus_message_iter_get_arg_type(&current_filter_iter) != DBUS_TYPE_STRUCT) {
// NFDi_SetError("D-Bus response signal current_filter iter is not a struct.");
return NFD_OKAY;
}
DBusMessageIter current_filter_struct_iter;
dbus_message_iter_recurse(&current_filter_iter, &current_filter_struct_iter);
if (!dbus_message_iter_next(&current_filter_struct_iter)) {
// NFDi_SetError("D-Bus response signal current_filter struct iter ended
// prematurely.");
return NFD_OKAY;
}
if (dbus_message_iter_get_arg_type(&current_filter_struct_iter) !=
DBUS_TYPE_ARRAY) {
// NFDi_SetError("D-Bus response signal URI sub iter is not an string.");
return NFD_OKAY;
}
DBusMessageIter current_filter_array_iter;
dbus_message_iter_recurse(&current_filter_struct_iter, &current_filter_array_iter);
if (dbus_message_iter_get_arg_type(&current_filter_array_iter) !=
DBUS_TYPE_STRUCT) {
// NFDi_SetError("D-Bus response signal current_filter iter is not a struct.");
return NFD_OKAY;
}
DBusMessageIter current_filter_extn_iter;
dbus_message_iter_recurse(&current_filter_array_iter, &current_filter_extn_iter);
if (dbus_message_iter_get_arg_type(&current_filter_extn_iter) != DBUS_TYPE_UINT32) {
// NFDi_SetError("D-Bus response signal URI sub iter is not an string.");
return NFD_OKAY;
}
dbus_uint32_t type;
dbus_message_iter_get_basic(&current_filter_extn_iter, &type);
if (type != 0) {
// NFDi_SetError("Wrong filter type.");
return NFD_OKAY;
}
if (!dbus_message_iter_next(&current_filter_extn_iter)) {
// NFDi_SetError("D-Bus response signal current_filter struct iter ended
// prematurely.");
return NFD_OKAY;
}
if (dbus_message_iter_get_arg_type(&current_filter_extn_iter) != DBUS_TYPE_STRING) {
// NFDi_SetError("D-Bus response signal URI sub iter is not an string.");
return NFD_OKAY;
}
dbus_message_iter_get_basic(&current_filter_extn_iter, &tmp_extn);
return NFD_OKAY;
}) == NFD_ERROR)
return NFD_ERROR;
if (!tmp_file) {
NFDi_SetError("D-Bus response signal has no URI field.");
return NFD_ERROR;
}
file = tmp_file;
extn = tmp_extn;
return NFD_OKAY;
}
#endif
// Appends up to 64 random chars to the given pointer. Returns the end of the appended chars.
char* Generate64RandomChars(char* out) {
size_t amount = 32;
while (amount > 0) {
unsigned char buf[32];
ssize_t res = getrandom(buf, amount, 0);
if (res == -1) {
if (errno == EINTR)
continue;
else
break; // too bad, urandom isn't working well
}
amount -= res;
// we encode each random char using two chars, since they must be [A-Z][a-z][0-9]_
for (size_t i = 0; i != static_cast<size_t>(res); ++i) {
*out++ = 'A' + static_cast<char>(buf[i] & 15);
*out++ = 'A' + static_cast<char>(buf[i] >> 4);
}
}
return out;
}
constexpr const char STR_RESPONSE_HANDLE_PREFIX[] = "/org/freedesktop/portal/desktop/request/";
constexpr size_t STR_RESPONSE_HANDLE_PREFIX_LEN =
sizeof(STR_RESPONSE_HANDLE_PREFIX) - 1; // -1 to remove the \0.
// Allocates and returns a path like "/org/freedesktop/portal/desktop/request/SENDER/TOKEN" with
// randomly generated TOKEN as recommended by flatpak. `handle_token_ptr` is a pointer to the
// TOKEN part.
char* MakeUniqueObjectPath(const char** handle_token_ptr) {
const char* sender = dbus_unique_name;
if (*sender == ':') ++sender;
const size_t sender_len = strlen(sender);
const size_t sz = STR_RESPONSE_HANDLE_PREFIX_LEN + sender_len + 1 +
64; // 1 for '/', followed by 64 random chars
char* path = NFDi_Malloc<char>(sz + 1);
char* path_ptr = path;
path_ptr = copy(STR_RESPONSE_HANDLE_PREFIX,
STR_RESPONSE_HANDLE_PREFIX + STR_RESPONSE_HANDLE_PREFIX_LEN,
path_ptr);
path_ptr = transform(
sender, sender + sender_len, path_ptr, [](char ch) { return ch != '.' ? ch : '_'; });
*path_ptr++ = '/';
*handle_token_ptr = path_ptr;
path_ptr = Generate64RandomChars(path_ptr);
*path_ptr = '\0';
return path;
}
constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_1[] =
"type='signal',sender='org.freedesktop.portal.Desktop',path='";
constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_1_LEN =
sizeof(STR_RESPONSE_SUBSCRIPTION_PATH_1) - 1;
constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_2[] =
"',interface='org.freedesktop.portal.Request',member='Response',destination='";
constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_2_LEN =
sizeof(STR_RESPONSE_SUBSCRIPTION_PATH_2) - 1;
constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_3[] = "'";
constexpr const char STR_RESPONSE_SUBSCRIPTION_PATH_3_LEN =
sizeof(STR_RESPONSE_SUBSCRIPTION_PATH_3) - 1;
class DBusSignalSubscriptionHandler {
private:
char* sub_cmd;
public:
DBusSignalSubscriptionHandler() : sub_cmd(nullptr) {}
~DBusSignalSubscriptionHandler() {
if (sub_cmd) Unsubscribe();
}
nfdresult_t Subscribe(const char* handle_path) {
if (sub_cmd) Unsubscribe();
sub_cmd = MakeResponseSubscriptionPath(handle_path, dbus_unique_name);
DBusError err;
dbus_error_init(&err);
dbus_bus_add_match(dbus_conn, sub_cmd, &err);
if (dbus_error_is_set(&err)) {
dbus_error_free(&dbus_err);
dbus_move_error(&err, &dbus_err);
NFDi_SetError(dbus_err.message);
return NFD_ERROR;
}
return NFD_OKAY;
}
void Unsubscribe() {
DBusError err;
dbus_error_init(&err);
dbus_bus_remove_match(dbus_conn, sub_cmd, &err);
NFDi_Free(sub_cmd);
sub_cmd = nullptr;
dbus_error_free(
&err); // silence unsubscribe errors, because this is intuitively part of 'cleanup'
}
private:
static char* MakeResponseSubscriptionPath(const char* handle_path, const char* unique_name) {
const size_t handle_path_len = strlen(handle_path);
const size_t unique_name_len = strlen(unique_name);
const size_t sz = STR_RESPONSE_SUBSCRIPTION_PATH_1_LEN + handle_path_len +
STR_RESPONSE_SUBSCRIPTION_PATH_2_LEN + unique_name_len +
STR_RESPONSE_SUBSCRIPTION_PATH_3_LEN;
char* res = NFDi_Malloc<char>(sz + 1);
char* res_ptr = res;
res_ptr = copy(STR_RESPONSE_SUBSCRIPTION_PATH_1,
STR_RESPONSE_SUBSCRIPTION_PATH_1 + STR_RESPONSE_SUBSCRIPTION_PATH_1_LEN,
res_ptr);
res_ptr = copy(handle_path, handle_path + handle_path_len, res_ptr);
res_ptr = copy(STR_RESPONSE_SUBSCRIPTION_PATH_2,
STR_RESPONSE_SUBSCRIPTION_PATH_2 + STR_RESPONSE_SUBSCRIPTION_PATH_2_LEN,
res_ptr);
res_ptr = copy(unique_name, unique_name + unique_name_len, res_ptr);
res_ptr = copy(STR_RESPONSE_SUBSCRIPTION_PATH_3,
STR_RESPONSE_SUBSCRIPTION_PATH_3 + STR_RESPONSE_SUBSCRIPTION_PATH_3_LEN,
res_ptr);
*res_ptr = '\0';
return res;
}
};
constexpr const char FILE_URI_PREFIX[] = "file://";
constexpr size_t FILE_URI_PREFIX_LEN = sizeof(FILE_URI_PREFIX) - 1;
// If fileUri starts with "file://", strips that prefix and copies it to a new buffer, and make
// outPath point to it, and returns NFD_OKAY. Otherwise, does not modify outPath and returns
// NFD_ERROR (with the correct error set)
nfdresult_t AllocAndCopyFilePath(const char* fileUri, char*& outPath) {
const char* prefix_begin = FILE_URI_PREFIX;
const char* const prefix_end = FILE_URI_PREFIX + FILE_URI_PREFIX_LEN;
for (; prefix_begin != prefix_end; ++prefix_begin, ++fileUri) {
if (*prefix_begin != *fileUri) {
NFDi_SetError("D-Bus freedesktop portal returned a URI that is not a file URI.");
return NFD_ERROR;
}
}
size_t len = strlen(fileUri);
char* path_without_prefix = NFDi_Malloc<char>(len + 1);
copy(fileUri, fileUri + (len + 1), path_without_prefix);
outPath = path_without_prefix;
return NFD_OKAY;
}
#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1
bool TryGetValidExtension(const char* extn,
const char*& trimmed_extn,
const char*& trimmed_extn_end) {
if (!extn) return false;
if (*extn != '*') return false;
++extn;
if (*extn != '.') return false;
trimmed_extn = extn;
for (++extn; *extn != '\0'; ++extn)
;
++extn;
trimmed_extn_end = extn;
return true;
}
// Like AllocAndCopyFilePath, but if `fileUri` has no extension and `extn` is usable, appends the
// extension. `extn` could be null, in which case no extension will ever be appended. `extn` is
// expected to be either in the form "*.abc" or "*", but this function will check for it, and ignore
// the extension if it is not in the correct form.
nfdresult_t AllocAndCopyFilePathWithExtn(const char* fileUri, const char* extn, char*& outPath) {
const char* prefix_begin = FILE_URI_PREFIX;
const char* const prefix_end = FILE_URI_PREFIX + FILE_URI_PREFIX_LEN;
for (; prefix_begin != prefix_end; ++prefix_begin, ++fileUri) {
if (*prefix_begin != *fileUri) {
NFDi_SetError("D-Bus freedesktop portal returned a URI that is not a file URI.");
return NFD_ERROR;
}
}
const char* file_end = fileUri;
for (; *file_end != '\0'; ++file_end)
;
const char* file_it = file_end;
do {
--file_it;
} while (*file_it != '/' && *file_it != '.');
const char* trimmed_extn; // includes the '.'
const char* trimmed_extn_end; // includes the '\0'
if (*file_it == '.' || !TryGetValidExtension(extn, trimmed_extn, trimmed_extn_end)) {
// has file extension already or no valid extension in `extn`
++file_end; // includes the '\0'
char* path_without_prefix = NFDi_Malloc<char>(file_end - fileUri);
copy(fileUri, file_end, path_without_prefix);
outPath = path_without_prefix;
} else {
// no file extension and we have a valid extension
char* path_without_prefix =
NFDi_Malloc<char>((file_end - fileUri) + (trimmed_extn_end - trimmed_extn));
char* out = copy(fileUri, file_end, path_without_prefix);
copy(trimmed_extn, trimmed_extn_end, out);
outPath = path_without_prefix;
}
return NFD_OKAY;
}
#endif
// DBus wrapper function that helps invoke the portal for all OpenFile() variants.
// This function returns NFD_OKAY iff outMsg gets set (to the returned message).
// Caller is responsible for freeing the outMsg using dbus_message_unref() (or use
// DBusMessage_Guard).
template <bool Multiple, bool Directory>
nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount) {
const char* handle_token_ptr;
char* handle_obj_path = MakeUniqueObjectPath(&handle_token_ptr);
Free_Guard<char> handle_obj_path_guard(handle_obj_path);
DBusError err; // need a separate error object because we don't want to mess with the old one
// if it's stil set
dbus_error_init(&err);
// Subscribe to the signal using the handle_obj_path
DBusSignalSubscriptionHandler signal_sub;
nfdresult_t res = signal_sub.Subscribe(handle_obj_path);
if (res != NFD_OKAY) return res;
// TODO: use XOpenDisplay()/XGetInputFocus() to find xid of window... but what should one do on
// Wayland?
DBusMessage* query = dbus_message_new_method_call("org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.FileChooser",
"OpenFile");
DBusMessage_Guard query_guard(query);
AppendOpenFileQueryParams<Multiple, Directory>(
query, handle_token_ptr, filterList, filterCount);
DBusMessage* reply =
dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err);
if (!reply) {
dbus_error_free(&dbus_err);
dbus_move_error(&err, &dbus_err);
NFDi_SetError(dbus_err.message);
return NFD_ERROR;
}
DBusMessage_Guard reply_guard(reply);
// Check the reply and update our signal subscription if necessary
{
DBusMessageIter iter;
if (!dbus_message_iter_init(reply, &iter)) {
NFDi_SetError("D-Bus reply is missing an argument.");
return NFD_ERROR;
}
if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_OBJECT_PATH) {
NFDi_SetError("D-Bus reply is not an object path.");
return NFD_ERROR;
}
const char* path;
dbus_message_iter_get_basic(&iter, &path);
if (strcmp(path, handle_obj_path) != 0) {
// needs to change our signal subscription
signal_sub.Subscribe(path);
}
}
// Wait and read the response
// const char* file = nullptr;
do {
while (true) {
DBusMessage* msg = dbus_connection_pop_message(dbus_conn);
if (!msg) break;
if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) {
// this is the response we're looking for
outMsg = msg;
return NFD_OKAY;
}
dbus_message_unref(msg);
}
} while (dbus_connection_read_write(dbus_conn, -1));
NFDi_SetError("D-Bus freedesktop portal did not give us a reply.");
return NFD_ERROR;
}
// DBus wrapper function that helps invoke the portal for the SaveFile() API.
// This function returns NFD_OKAY iff outMsg gets set (to the returned message).
// Caller is responsible for freeing the outMsg using dbus_message_unref() (or use
// DBusMessage_Guard).
nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath,
const nfdnchar_t* defaultName) {
const char* handle_token_ptr;
char* handle_obj_path = MakeUniqueObjectPath(&handle_token_ptr);
Free_Guard<char> handle_obj_path_guard(handle_obj_path);
DBusError err; // need a separate error object because we don't want to mess with the old one
// if it's stil set
dbus_error_init(&err);
// Subscribe to the signal using the handle_obj_path
DBusSignalSubscriptionHandler signal_sub;
nfdresult_t res = signal_sub.Subscribe(handle_obj_path);
if (res != NFD_OKAY) return res;
// TODO: use XOpenDisplay()/XGetInputFocus() to find xid of window... but what should one do on
// Wayland?
DBusMessage* query = dbus_message_new_method_call("org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.FileChooser",
"SaveFile");
DBusMessage_Guard query_guard(query);
AppendSaveFileQueryParams(
query, handle_token_ptr, filterList, filterCount, defaultPath, defaultName);
DBusMessage* reply =
dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err);
if (!reply) {
dbus_error_free(&dbus_err);
dbus_move_error(&err, &dbus_err);
NFDi_SetError(dbus_err.message);
return NFD_ERROR;
}
DBusMessage_Guard reply_guard(reply);
// Check the reply and update our signal subscription if necessary
{
DBusMessageIter iter;
if (!dbus_message_iter_init(reply, &iter)) {
NFDi_SetError("D-Bus reply is missing an argument.");
return NFD_ERROR;
}
if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_OBJECT_PATH) {
NFDi_SetError("D-Bus reply is not an object path.");
return NFD_ERROR;
}
const char* path;
dbus_message_iter_get_basic(&iter, &path);
if (strcmp(path, handle_obj_path) != 0) {
// needs to change our signal subscription
signal_sub.Subscribe(path);
}
}
// Wait and read the response
// const char* file = nullptr;
do {
while (true) {
DBusMessage* msg = dbus_connection_pop_message(dbus_conn);
if (!msg) break;
if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) {
// this is the response we're looking for
outMsg = msg;
return NFD_OKAY;
}
dbus_message_unref(msg);
}
} while (dbus_connection_read_write(dbus_conn, -1));
NFDi_SetError("D-Bus freedesktop portal did not give us a reply.");
return NFD_ERROR;
}
} // namespace
/* public */
const char* NFD_GetError(void) {
return err_ptr;
}
void NFD_ClearError(void) {
NFDi_SetError(nullptr);
dbus_error_free(&dbus_err);
}
nfdresult_t NFD_Init(void) {
// Initialize dbus_error
dbus_error_init(&dbus_err);
// Get DBus connection
dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &dbus_err);
if (!dbus_conn) {
NFDi_SetError(dbus_err.message);
return NFD_ERROR;
}
dbus_unique_name = dbus_bus_get_unique_name(dbus_conn);
if (!dbus_unique_name) {
NFDi_SetError("Unable to get the unique name of our D-Bus connection.");
return NFD_ERROR;
}
return NFD_OKAY;
}
void NFD_Quit(void) {
dbus_connection_unref(dbus_conn);
// Note: We do not free dbus_error since NFD_Init might set it.
// To avoid leaking memory, the caller should explicitly call NFD_ClearError after reading the
// error.
}
void NFD_FreePathN(nfdnchar_t* filePath) {
assert(filePath);
NFDi_Free(filePath);
}
nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath) {
(void)defaultPath; // Default path not supported for portal backend
DBusMessage* msg;
{
const nfdresult_t res = NFD_DBus_OpenFile<false, false>(msg, filterList, filterCount);
if (res != NFD_OKAY) {
return res;
}
}
DBusMessage_Guard msg_guard(msg);
const char* file;
{
const nfdresult_t res = ReadResponseUrisSingle(msg, file);
if (res != NFD_OKAY) {
return res;
}
}
return AllocAndCopyFilePath(file, *outPath);
}
nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath) {
(void)defaultPath; // Default path not supported for portal backend
DBusMessage* msg;
{
const nfdresult_t res = NFD_DBus_OpenFile<true, false>(msg, filterList, filterCount);
if (res != NFD_OKAY) {
return res;
}
}
DBusMessageIter uri_iter;
const nfdresult_t res = ReadResponseUris(msg, uri_iter);
if (res != NFD_OKAY) {
dbus_message_unref(msg);
return res;
}
*outPaths = msg;
return NFD_OKAY;
}
nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath,
const nfdnfilteritem_t* filterList,
nfdfiltersize_t filterCount,
const nfdnchar_t* defaultPath,
const nfdnchar_t* defaultName) {
DBusMessage* msg;
{
const nfdresult_t res =
NFD_DBus_SaveFile(msg, filterList, filterCount, defaultPath, defaultName);
if (res != NFD_OKAY) {
return res;
}
}
DBusMessage_Guard msg_guard(msg);
#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1
const char* file;
const char* extn;
{
const nfdresult_t res = ReadResponseUrisSingleAndCurrentExtension(msg, file, extn);
if (res != NFD_OKAY) {
return res;
}
}
return AllocAndCopyFilePathWithExtn(file, extn, *outPath);
#else
const char* file;
{
const nfdresult_t res = ReadResponseUrisSingle(msg, file);
if (res != NFD_OKAY) {
return res;
}
}
return AllocAndCopyFilePath(file, *outPath);
#endif
}
nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) {
(void)defaultPath; // Default path not supported for portal backend
DBusMessage* msg;
{
const nfdresult_t res = NFD_DBus_OpenFile<false, true>(msg, nullptr, 0);
if (res != NFD_OKAY) {
return res;
}
}
DBusMessage_Guard msg_guard(msg);
const char* file;
{
const nfdresult_t res = ReadResponseUrisSingle(msg, file);
if (res != NFD_OKAY) {
return res;
}
}
return AllocAndCopyFilePath(file, *outPath);
}
nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) {
assert(pathSet);
DBusMessage* msg = const_cast<DBusMessage*>(static_cast<const DBusMessage*>(pathSet));
*count = ReadResponseUrisUncheckedGetArraySize(msg);
return NFD_OKAY;
}
nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet,
nfdpathsetsize_t index,
nfdnchar_t** outPath) {
assert(pathSet);
DBusMessage* msg = const_cast<DBusMessage*>(static_cast<const DBusMessage*>(pathSet));
DBusMessageIter uri_iter;
ReadResponseUrisUnchecked(msg, uri_iter);
while (index > 0) {
--index;
if (!dbus_message_iter_next(&uri_iter)) {
NFDi_SetError("Index out of bounds.");
return NFD_ERROR;
}
}
if (dbus_message_iter_get_arg_type(&uri_iter) != DBUS_TYPE_STRING) {
NFDi_SetError("D-Bus response signal URI sub iter is not an string.");
return NFD_ERROR;
}
const char* file;
dbus_message_iter_get_basic(&uri_iter, &file);
return AllocAndCopyFilePath(file, *outPath);
}
void NFD_PathSet_FreePathN(const nfdnchar_t* filePath) {
assert(filePath);
NFD_FreePathN(const_cast<nfdnchar_t*>(filePath));
}
void NFD_PathSet_Free(const nfdpathset_t* pathSet) {
assert(pathSet);
DBusMessage* msg = const_cast<DBusMessage*>(static_cast<const DBusMessage*>(pathSet));
dbus_message_unref(msg);
}
nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator) {
assert(pathSet);
DBusMessage* msg = const_cast<DBusMessage*>(static_cast<const DBusMessage*>(pathSet));
ReadResponseUrisUnchecked(msg, *reinterpret_cast<DBusMessageIter*>(outEnumerator));
return NFD_OKAY;
}
void NFD_PathSet_FreeEnum(nfdpathsetenum_t*) {
// Do nothing, because the enumeration is just a message iterator
}
nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath) {
DBusMessageIter& uri_iter = *reinterpret_cast<DBusMessageIter*>(enumerator);
const int arg_type = dbus_message_iter_get_arg_type(&uri_iter);
if (arg_type == DBUS_TYPE_INVALID) {
*outPath = nullptr;
return NFD_OKAY;
}
if (arg_type != DBUS_TYPE_STRING) {
NFDi_SetError("D-Bus response signal URI sub iter is not an string.");
return NFD_ERROR;
}
const char* file;
dbus_message_iter_get_basic(&uri_iter, &file);
const nfdresult_t res = AllocAndCopyFilePath(file, *outPath);
if (res != NFD_OKAY) return res;
dbus_message_iter_next(&uri_iter);
return NFD_OKAY;
}