diff --git a/nfd/nfd_cocoa.m b/nfd/nfd_cocoa.m index a2968133..f7024356 100644 --- a/nfd/nfd_cocoa.m +++ b/nfd/nfd_cocoa.m @@ -9,17 +9,23 @@ #include #include "nfd.h" -// At least one of NFD_NEEDS_ALLOWEDCONTENTTYPES and NFD_NEEDS_ALLOWEDFILETYPES will be defined -#if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && defined(__MAC_12_0) && \ - __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_12_0 -#include -#define NFD_NEEDS_ALLOWEDCONTENTTYPES -#if !defined(__MAC_OS_X_VERSION_MIN_ALLOWED) || !defined(__MAC_12_0) || \ - __MAC_OS_X_VERSION_MIN_ALLOWED < __MAC_12_0 -#define NFD_NEEDS_ALLOWEDFILETYPES -#endif +// MacOS is deprecating the allowedFileTypes property in favour of allowedContentTypes, so we have +// to introduce this breaking change. Define NFD_MACOS_ALLOWEDCONTENTTYPES to 1 to have it set the +// allowedContentTypes property of the SavePanel or OpenPanel. Define +// NFD_MACOS_ALLOWEDCONTENTTYPES to 0 to have it set the allowedFileTypes property of the SavePanel +// or OpenPanel. If NFD_MACOS_ALLOWEDCONTENTTYPES is undefined, then it will set it to 1 if +// __MAC_OS_X_VERSION_MIN_REQUIRED >= 11.0, and 0 otherwise. +#if !defined(NFD_MACOS_ALLOWEDCONTENTTYPES) +#if !defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || !defined(__MAC_11_0) || \ + __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_11_0 +#define NFD_MACOS_ALLOWEDCONTENTTYPES 0 #else -#define NFD_NEEDS_ALLOWEDFILETYPES +#define NFD_MACOS_ALLOWEDCONTENTTYPES 1 +#endif +#endif + +#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1 +#include #endif static const char* g_errorstr = NULL; @@ -40,7 +46,7 @@ static void NFDi_Free(void* ptr) { free(ptr); } -#if defined(NFD_NEEDS_ALLOWEDCONTENTTYPES) +#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1 // Returns an NSArray of UTType representing the content types. static NSArray* BuildAllowedContentTypes(const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount) { @@ -81,9 +87,7 @@ static NSArray* BuildAllowedContentTypes(const nfdnfilteritem_t* filterList, return returnArray; } -#endif - -#if defined(NFD_NEEDS_ALLOWEDFILETYPES) +#else // Returns an NSArray of NSString representing the file types. static NSArray* BuildAllowedFileTypes(const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount) { @@ -130,21 +134,8 @@ static void AddFilterListToDialog(NSSavePanel* dialog, assert(filterList); // Make NSArray of file types and set it on the dialog -// We use setAllowedFileTypes or setAllowedContentTypes depending on the OS version -#if defined(NFD_NEEDS_ALLOWEDCONTENTTYPES) && defined(NFD_NEEDS_ALLOWEDFILETYPES) - // If both are needed, it means we have to do a runtime check - if (@available(macOS 12.0, *)) { - NSArray* allowedContentTypes = BuildAllowedContentTypes(filterList, filterCount); - [dialog setAllowedContentTypes:allowedContentTypes]; - } else { - NSArray* allowedFileTypes = BuildAllowedFileTypes(filterList, filterCount); - // Unfortunately @available doesn't silence deprecation warnings so we need these pragmas -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - [dialog setAllowedFileTypes:allowedFileTypes]; -#pragma clang diagnostic pop - } -#elif defined(NFD_NEEDS_ALLOWEDCONTENTTYPES) +// We use setAllowedFileTypes or setAllowedContentTypes depending on the deployment target +#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1 NSArray* allowedContentTypes = BuildAllowedContentTypes(filterList, filterCount); [dialog setAllowedContentTypes:allowedContentTypes]; #else diff --git a/nfd/nfd_portal.cpp b/nfd/nfd_portal.cpp index f5a9302f..8ef9158f 100644 --- a/nfd/nfd_portal.cpp +++ b/nfd/nfd_portal.cpp @@ -20,13 +20,12 @@ #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. +Define NFD_APPEND_EXTENSION if you want the file extension to be appended when missing. Linux +programs usually don't append the file extension, but for consistency with other OSes you might want +to append it. However, when using portals, the file overwrite prompt and the Flatpak sandbox won't +know that we appended an extension, so they will not check or whitelist the correct file. Enabling +NFD_APPEND_EXTENSION is not recommended for portals. */ -#ifndef NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION -#define NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION 1 -#endif namespace { @@ -720,14 +719,14 @@ nfdresult_t ReadResponseUrisSingle(DBusMessage* msg, const char*& file) { 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."); + NFDi_SetError("D-Bus response signal URI sub iter is not a string."); return NFD_ERROR; } dbus_message_iter_get_basic(&uri_iter, &file); return NFD_OKAY; } -#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1 +#ifdef NFD_APPEND_EXTENSION // 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 @@ -775,7 +774,7 @@ nfdresult_t ReadResponseUrisSingleAndCurrentExtension(DBusMessage* msg, } if (dbus_message_iter_get_arg_type(¤t_filter_struct_iter) != DBUS_TYPE_ARRAY) { - // NFDi_SetError("D-Bus response signal URI sub iter is not an string."); + // NFDi_SetError("D-Bus response signal URI sub iter is not a string."); return NFD_OKAY; } DBusMessageIter current_filter_array_iter; @@ -788,7 +787,7 @@ nfdresult_t ReadResponseUrisSingleAndCurrentExtension(DBusMessage* msg, DBusMessageIter current_filter_extn_iter; dbus_message_iter_recurse(¤t_filter_array_iter, ¤t_filter_extn_iter); if (dbus_message_iter_get_arg_type(¤t_filter_extn_iter) != DBUS_TYPE_UINT32) { - // NFDi_SetError("D-Bus response signal URI sub iter is not an string."); + // NFDi_SetError("D-Bus response signal URI sub iter is not a string."); return NFD_OKAY; } dbus_uint32_t type; @@ -803,7 +802,7 @@ nfdresult_t ReadResponseUrisSingleAndCurrentExtension(DBusMessage* msg, return NFD_OKAY; } if (dbus_message_iter_get_arg_type(¤t_filter_extn_iter) != DBUS_TYPE_STRING) { - // NFDi_SetError("D-Bus response signal URI sub iter is not an string."); + // NFDi_SetError("D-Bus response signal URI sub iter is not a string."); return NFD_OKAY; } dbus_message_iter_get_basic(¤t_filter_extn_iter, &tmp_extn); @@ -942,12 +941,69 @@ class DBusSignalSubscriptionHandler { } }; +// Returns true if ch is in [0-9A-Za-z], false otherwise. +bool IsHex(char ch) { + return ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'F') || ('a' <= ch && ch <= 'f'); +} + +// Returns the hexadecimal value contained in the char. Precondition: IsHex(ch) +char ParseHexUnchecked(char ch) { + if ('0' <= ch && ch <= '9') return ch - '0'; + if ('A' <= ch && ch <= 'F') return ch - ('A' - 10); + if ('a' <= ch && ch <= 'f') return ch - ('a' - 10); +#if defined(__GNUC__) + __builtin_unreachable(); +#endif +} + +// Returns true if the given file URI is decodable (i.e. not malformed), and false otherwise. +// If this function returns true, then `out` will be populated with the length of the decoded URI +// and `fileUriEnd` will point to the trailing null byte of `fileUri`. Otherwise, `out` and +// `fileUriEnd` will be unmodified. +bool TryUriDecodeLen(const char* fileUri, size_t& out, const char*& fileUriEnd) { + size_t len = 0; + while (*fileUri) { + if (*fileUri != '%') { + ++fileUri; + } else { + if (*(fileUri + 1) == '\0' || *(fileUri + 2) == '\0') { + return false; + } + if (!IsHex(*(fileUri + 1)) || !IsHex(*(fileUri + 2))) { + return false; + } + fileUri += 3; + } + ++len; + } + out = len; + fileUriEnd = fileUri; + return true; +} + +// Decodes the given URI and writes it to `outPath`. The caller must ensure that the given URI is +// not malformed (typically with a prior call to `TryUriDecodeLen`). This function does not write +// any trailing null character. +char* UriDecodeUnchecked(const char* fileUri, const char* fileUriEnd, char* outPath) { + while (fileUri != fileUriEnd) { + if (*fileUri != '%') { + *outPath++ = *fileUri++; + } else { + ++fileUri; + const char high_nibble = ParseHexUnchecked(*fileUri++); + const char low_nibble = ParseHexUnchecked(*fileUri++); + *outPath++ = (high_nibble << 4) | low_nibble; + } + } + return outPath; +} + 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) +// If fileUri starts with "file://", strips that prefix and URI-decodes the remaining part 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; @@ -957,14 +1013,20 @@ nfdresult_t AllocAndCopyFilePath(const char* fileUri, char*& outPath) { return NFD_ERROR; } } - size_t len = strlen(fileUri); - char* path_without_prefix = NFDi_Malloc(len + 1); - copy(fileUri, fileUri + (len + 1), path_without_prefix); + size_t decoded_len; + const char* file_uri_end; + if (!TryUriDecodeLen(fileUri, decoded_len, file_uri_end)) { + NFDi_SetError("D-Bus freedesktop portal returned a malformed URI."); + return NFD_ERROR; + } + char* const path_without_prefix = NFDi_Malloc(decoded_len + 1); + char* const out_end = UriDecodeUnchecked(fileUri, file_uri_end, path_without_prefix); + *out_end = '\0'; outPath = path_without_prefix; return NFD_OKAY; } -#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1 +#ifdef NFD_APPEND_EXTENSION bool TryGetValidExtension(const char* extn, const char*& trimmed_extn, const char*& trimmed_extn_end) { @@ -994,10 +1056,18 @@ nfdresult_t AllocAndCopyFilePathWithExtn(const char* fileUri, const char* extn, } } - const char* file_end = fileUri; - for (; *file_end != '\0'; ++file_end) - ; - const char* file_it = file_end; + size_t decoded_len; + const char* file_uri_end; + if (!TryUriDecodeLen(fileUri, decoded_len, file_uri_end)) { + NFDi_SetError("D-Bus freedesktop portal returned a malformed URI."); + return NFD_ERROR; + } + + const char* file_it = file_uri_end; + // The following loop condition is safe because `FILE_URI_PREFIX` ends with '/', + // so we won't iterate past the beginning of the URI. + // Also in UTF-8 all non-ASCII code points are encoded using bytes 128-255 so every '.' or '/' + // is also '.' or '/' in UTF-8. do { --file_it; } while (*file_it != '/' && *file_it != '.'); @@ -1005,16 +1075,17 @@ nfdresult_t AllocAndCopyFilePathWithExtn(const char* fileUri, const char* extn, 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(file_end - fileUri); - copy(fileUri, file_end, path_without_prefix); + char* const path_without_prefix = NFDi_Malloc(decoded_len + 1); + char* const out_end = UriDecodeUnchecked(fileUri, file_uri_end, path_without_prefix); + *out_end = '\0'; outPath = path_without_prefix; } else { // no file extension and we have a valid extension - char* path_without_prefix = - NFDi_Malloc((file_end - fileUri) + (trimmed_extn_end - trimmed_extn)); - char* out = copy(fileUri, file_end, path_without_prefix); - copy(trimmed_extn, trimmed_extn_end, out); + char* const path_without_prefix = + NFDi_Malloc(decoded_len + (trimmed_extn_end - trimmed_extn)); + char* const out_mid = UriDecodeUnchecked(fileUri, file_uri_end, path_without_prefix); + char* const out_end = copy(trimmed_extn, trimmed_extn_end, out_mid); + *out_end = '\0'; outPath = path_without_prefix; } return NFD_OKAY; @@ -1244,15 +1315,15 @@ nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, } DBusMessage_Guard msg_guard(msg); - const char* file; + const char* uri; { - const nfdresult_t res = ReadResponseUrisSingle(msg, file); + const nfdresult_t res = ReadResponseUrisSingle(msg, uri); if (res != NFD_OKAY) { return res; } } - return AllocAndCopyFilePath(file, *outPath); + return AllocAndCopyFilePath(uri, *outPath); } nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, @@ -1295,27 +1366,27 @@ nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, } DBusMessage_Guard msg_guard(msg); -#if NFD_PORTAL_AUTO_APPEND_FILE_EXTENSION == 1 - const char* file; +#ifdef NFD_APPEND_EXTENSION + const char* uri; const char* extn; { - const nfdresult_t res = ReadResponseUrisSingleAndCurrentExtension(msg, file, extn); + const nfdresult_t res = ReadResponseUrisSingleAndCurrentExtension(msg, uri, extn); if (res != NFD_OKAY) { return res; } } - return AllocAndCopyFilePathWithExtn(file, extn, *outPath); + return AllocAndCopyFilePathWithExtn(uri, extn, *outPath); #else - const char* file; + const char* uri; { - const nfdresult_t res = ReadResponseUrisSingle(msg, file); + const nfdresult_t res = ReadResponseUrisSingle(msg, uri); if (res != NFD_OKAY) { return res; } } - return AllocAndCopyFilePath(file, *outPath); + return AllocAndCopyFilePath(uri, *outPath); #endif } @@ -1331,15 +1402,15 @@ nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) } DBusMessage_Guard msg_guard(msg); - const char* file; + const char* uri; { - const nfdresult_t res = ReadResponseUrisSingle(msg, file); + const nfdresult_t res = ReadResponseUrisSingle(msg, uri); if (res != NFD_OKAY) { return res; } } - return AllocAndCopyFilePath(file, *outPath); + return AllocAndCopyFilePath(uri, *outPath); } nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count) { @@ -1364,12 +1435,12 @@ nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, } } 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."); + NFDi_SetError("D-Bus response signal URI sub iter is not a string."); return NFD_ERROR; } - const char* file; - dbus_message_iter_get_basic(&uri_iter, &file); - return AllocAndCopyFilePath(file, *outPath); + const char* uri; + dbus_message_iter_get_basic(&uri_iter, &uri); + return AllocAndCopyFilePath(uri, *outPath); } void NFD_PathSet_FreePathN(const nfdnchar_t* filePath) { @@ -1402,12 +1473,12 @@ nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** out return NFD_OKAY; } if (arg_type != DBUS_TYPE_STRING) { - NFDi_SetError("D-Bus response signal URI sub iter is not an string."); + NFDi_SetError("D-Bus response signal URI sub iter is not a string."); return NFD_ERROR; } - const char* file; - dbus_message_iter_get_basic(&uri_iter, &file); - const nfdresult_t res = AllocAndCopyFilePath(file, *outPath); + const char* uri; + dbus_message_iter_get_basic(&uri_iter, &uri); + const nfdresult_t res = AllocAndCopyFilePath(uri, *outPath); if (res != NFD_OKAY) return res; dbus_message_iter_next(&uri_iter); return NFD_OKAY;