diff --git a/deps/nfde/.github/workflows/cmake.yml b/deps/nfde/.github/workflows/cmake.yml index dfe102ac..984851a2 100644 --- a/deps/nfde/.github/workflows/cmake.yml +++ b/deps/nfde/.github/workflows/cmake.yml @@ -16,21 +16,43 @@ jobs: - name: Checkout uses: actions/checkout@v2 - name: Format code - run: find src/ test/ -iname '*.c' -or -iname '*.cpp' -or -iname '*.m' -or -iname '*.mm' -or -iname '*.h' -or -iname '*.hpp' | xargs clang-format -i -style=file + # ClangFormat 14 has a bug, which seems to be fixed in ClangFormat 15. Until GitHub-hosted runners support ClangFormat 15, we will stay at ClangFormat 13. + run: find src/ test/ -iname '*.c' -or -iname '*.cpp' -or -iname '*.m' -or -iname '*.mm' -or -iname '*.h' -or -iname '*.hpp' | xargs clang-format-13 -i -style=file - name: Check diff run: git diff --exit-code build-ubuntu: - name: Ubuntu (${{ matrix.os }}, ${{ matrix.portal.name }}, ${{ matrix.compiler.c }}, C++${{ matrix.cppstd }}) - runs-on: ${{ matrix.os }} + name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }} + runs-on: ${{ matrix.os.label }} strategy: matrix: - os: [ubuntu-latest, ubuntu-18.04] + os: [ {label: ubuntu-latest, name: latest}, {label: ubuntu-20.04, name: 20.04} ] portal: [ {flag: OFF, dep: libgtk-3-dev, name: GTK}, {flag: ON, dep: libdbus-1-dev, name: Portal} ] # The NFD_PORTAL setting defaults to OFF (i.e. uses GTK) - compiler: [ {c: gcc, cpp: g++}, {c: clang, cpp: clang++} ] # The default compiler is gcc/g++ - cppstd: [23, 11] + autoappend: [ {flag: OFF, name: NoAppendExtn} ] # By default the NFD_PORTAL mode does not append extensions, because it breaks some features of the portal + compiler: [ {c: gcc, cpp: g++, name: GCC}, {c: clang, cpp: clang++, name: Clang} ] # The default compiler is gcc/g++ + cppstd: [20, 11] + shared_lib: [ {flag: OFF, name: Static} ] + include: + - os: {label: ubuntu-latest, name: latest} + portal: {flag: ON, dep: libdbus-1-dev, name: Portal} + autoappend: {flag: ON, name: AutoAppendExtn} + compiler: {c: gcc, cpp: g++, name: GCC} + cppstd: 11 + shared_lib: {flag: OFF, name: Static} + - os: {label: ubuntu-latest, name: latest} + portal: {flag: ON, dep: libdbus-1-dev, name: Portal} + autoappend: {flag: ON, name: AutoAppendExtn} + compiler: {c: clang, cpp: clang++, name: Clang} + cppstd: 11 + shared_lib: {flag: OFF, name: Static} + - os: {label: ubuntu-latest, name: latest} + portal: {flag: ON, dep: libdbus-1-dev, name: Portal} + autoappend: {flag: ON, name: NoAppendExtn} + compiler: {c: gcc, cpp: g++, name: GCC} + cppstd: 11 + shared_lib: {flag: ON, name: Shared} steps: - name: Checkout @@ -38,60 +60,72 @@ jobs: - name: Installing Dependencies run: sudo apt-get update && sudo apt-get install ${{ matrix.portal.dep }} - name: Configure - run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=${{ matrix.compiler.c }} -DCMAKE_CXX_COMPILER=${{ matrix.compiler.cpp }} -DCMAKE_CXX_STANDARD=${{ matrix.cppstd }} -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_PORTAL=${{ matrix.portal.flag }} -DNFD_BUILD_TESTS=ON .. + run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=${{ matrix.compiler.c }} -DCMAKE_CXX_COMPILER=${{ matrix.compiler.cpp }} -DCMAKE_CXX_STANDARD=${{ matrix.cppstd }} -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_PORTAL=${{ matrix.portal.flag }} -DNFD_APPEND_EXTENSION=${{ matrix.autoappend.flag }} -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON .. - name: Build run: cmake --build build --target install - name: Upload test binaries uses: actions/upload-artifact@v2 with: - name: $${{ matrix.os }} - $${{ matrix.compiler.c }} + name: Ubuntu ${{ matrix.os.name }} - ${{ matrix.compiler.name }}, ${{ matrix.portal.name }}, ${{ matrix.autoappend.name }}, ${{ matrix.shared_lib.name }}, C++${{ matrix.cppstd }} path: | - build/src/libnfd.a - build/test/test_* + build/src/* + build/test/* build-macos-clang: - name: MacOS latest - Clang - runs-on: macos-latest + name: MacOS ${{ matrix.os.name }} - Clang, ${{ matrix.shared_lib.name }} + runs-on: ${{ matrix.os.label }} + + strategy: + matrix: + os: [ {label: macos-latest, name: latest}, {label: macos-11, name: 11} ] + shared_lib: [ {flag: OFF, name: Static} ] + include: + - os: {label: macos-latest, name: latest} + shared_lib: {flag: ON, name: Shared} steps: - name: Checkout uses: actions/checkout@v2 - name: Configure - run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_BUILD_TESTS=ON .. + run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON .. - name: Build run: cmake --build build --target install - name: Upload test binaries uses: actions/upload-artifact@v2 with: - name: MacOS latest - Clang + name: MacOS ${{ matrix.os.name }} - Clang, ${{ matrix.shared_lib.name }} path: | - build/src/libnfd.a - build/test/test_* + build/src/* + build/test/* build-windows-msvc: - name: Windows latest - MSVC + name: Windows latest - MSVC, ${{ matrix.shared_lib.name }} runs-on: windows-latest + strategy: + matrix: + shared_lib: [ {flag: OFF, name: Static}, {flag: ON, name: Shared} ] + steps: - name: Checkout uses: actions/checkout@v2 - name: Configure - run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DNFD_BUILD_TESTS=ON .. + run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DBUILD_SHARED_LIBS=${{ matrix.shared_lib.flag }} -DNFD_BUILD_TESTS=ON .. - name: Build run: cmake --build build --target install --config Release - name: Upload test binaries uses: actions/upload-artifact@v2 with: - name: Windows latest - MSVC + name: Windows latest - MSVC, ${{ matrix.shared_lib.name }} path: | - build/src/Release/nfd.lib - build/test/Release/test_* + build/src/Release/* + build/test/Release/* build-windows-clang: - name: Windows latest - Clang + name: Windows latest - Clang, Static runs-on: windows-latest steps: @@ -104,14 +138,14 @@ jobs: - name: Upload test binaries uses: actions/upload-artifact@v2 with: - name: Windows latest - Clang + name: Windows latest - Clang, Static path: | - build/src/Release/nfd.lib - build/test/Release/test_* + build/src/Release/* + build/test/Release/* build-windows-mingw: - name: Windows latest - MinGW + name: Windows latest - MinGW, Static runs-on: windows-latest defaults: @@ -130,13 +164,13 @@ jobs: mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake - name: Configure - run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_BUILD_TESTS=ON .. + run: mkdir build && mkdir install && cd build && cmake -DCMAKE_INSTALL_PREFIX="../install" -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -pedantic" -DCMAKE_CXX_FLAGS="-Wall -Wextra -Werror -pedantic" -DNFD_BUILD_TESTS=ON .. - name: Build run: cmake --build build --target install - name: Upload test binaries uses: actions/upload-artifact@v2 with: - name: Windows latest - MinGW + name: Windows latest - MinGW, Static path: | - build/src/libnfd.a - build/test/test_* + build/src/* + build/test/* diff --git a/deps/nfde/CMakeLists.txt b/deps/nfde/CMakeLists.txt index 863eb4fe..1285ec30 100644 --- a/deps/nfde/CMakeLists.txt +++ b/deps/nfde/CMakeLists.txt @@ -1,5 +1,14 @@ -cmake_minimum_required(VERSION 3.10) -project(nativefiledialog-extended) +cmake_minimum_required(VERSION 3.5) +project(nativefiledialog-extended VERSION 1.1.1) + +set(nfd_ROOT_PROJECT OFF) +if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(nfd_ROOT_PROJECT ON) +endif () + +option(BUILD_SHARED_LIBS "Build a shared library instead of static" OFF) +option(NFD_BUILD_TESTS "Build tests for nfd" ${nfd_ROOT_PROJECT}) +option(NFD_INSTALL "Generate install target for nfd" ${nfd_ROOT_PROJECT}) if(NOT MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w") @@ -30,12 +39,19 @@ message("nfd Compiler: ${nfd_COMPILER}") # Use latest C++ by default (should be the best one), but let user override it if(NOT DEFINED CMAKE_CXX_STANDARD) - set (CMAKE_CXX_STANDARD 23) + if(CMAKE_VERSION VERSION_LESS "3.8") + set (CMAKE_CXX_STANDARD 14) + elseif(CMAKE_VERSION VERSION_LESS "3.12") + set (CMAKE_CXX_STANDARD 17) + elseif(CMAKE_VERSION VERSION_LESS "3.20") + set (CMAKE_CXX_STANDARD 20) + else() + set (CMAKE_CXX_STANDARD 23) + endif() endif() add_subdirectory(src) -option(NFD_BUILD_TESTS "Build tests for nfd" OFF) if(${NFD_BUILD_TESTS}) add_subdirectory(test) endif() diff --git a/deps/nfde/README.md b/deps/nfde/README.md index 77376c0b..3626e910 100644 --- a/deps/nfde/README.md +++ b/deps/nfde/README.md @@ -43,6 +43,8 @@ Features added in Native File Dialog Extended: There is also significant code refractoring, especially for the Windows implementation. +The [wiki](https://github.com/btzy/nativefiledialog-extended/wiki) keeps track of known language bindings and known popular projects that depend on this library. + # Basic Usage ```C @@ -84,9 +86,12 @@ If you are using a platform abstraction framework such as SDL or GLFW, also see # Screenshots # -![Windows 10](screens/open_win10.png?raw=true) -![MacOS 10.13](screens/open_macos_11.0.png?raw=true) -![GTK3 on Ubuntu 20.04](screens/open_gtk3.png?raw=true) +![Windows 10](screens/open_win10.png?raw=true#gh-light-mode-only) +![Windows 10](screens/open_win10_dark.png?raw=true#gh-dark-mode-only) +![MacOS 10.13](screens/open_macos_11.0.png?raw=true#gh-light-mode-only) +![MacOS 10.13](screens/open_macos_11.0_dark.png?raw=true#gh-dark-mode-only) +![GTK3 on Ubuntu 20.04](screens/open_gtk3.png?raw=true#gh-light-mode-only) +![GTK3 on Ubuntu 20.04](screens/open_gtk3_dark.png?raw=true#gh-dark-mode-only) # Building @@ -99,6 +104,9 @@ target_link_libraries(MyProgram PRIVATE nfd) ``` Make sure that you also have the needed [dependencies](#dependencies). +When included as a subproject, sample programs are not built and the install target is disabled by default. +Add `-DNFD_BUILD_TESTS=ON` to build sample programs and `-DNFD_INSTALL=ON` to enable the install target. + ## Standalone Library If you want to build the standalone static library, execute the following commands (starting from the project root directory): @@ -114,8 +122,8 @@ and build the project (in release mode) there. If you are developing NFDe, you may want to do `-DCMAKE_BUILD_TYPE=Debug` to build a debug version of the library instead. -If you want to build the sample programs, -add `-DNFD_BUILD_TESTS=ON` (sample programs are not built by default). +When building as a standalone library, sample programs are built and the install target is enabled by default. +Add `-DNFD_BUILD_TESTS=OFF` to disable building sample programs and `-DNFD_INSTALL=OFF` to disable the install target. On Linux, if you want to use the Flatpak desktop portal instead of GTK, add `-DNFD_PORTAL=ON`. (Otherwise, GTK will be used.) See the "Usage" section below for more information. @@ -148,10 +156,10 @@ Make sure `libgtk-3-dev` is installed on your system. Make sure `libdbus-1-dev` is installed on your system. ### MacOS -On MacOS, add `AppKit` to the list of frameworks. +On MacOS, add `AppKit` and `UniformTypeIdentifiers` to the list of frameworks. ### Windows -On Windows (both MSVC and MinGW), ensure you are building against `ole32.lib` and `uuid.lib`. +On Windows (both MSVC and MinGW), ensure you are building against `ole32.lib`, `uuid.lib`, and `shell32.lib`. # Usage @@ -244,11 +252,11 @@ SDL_Quit(); // Then deinitialize SDL2 On Linux, you can use the portal implementation instead of GTK, which will open the "native" file chooser selected by the OS or customized by the user. The user must have `xdg-desktop-portal` and a suitable backend installed (this comes pre-installed with most common desktop distros), otherwise `NFD_ERROR` will be returned. -The portal implementation is much less battle-tested than the GTK implementation. There may be bugs — please report them on the issue tracker. - To use the portal implementation, add `-DNFD_PORTAL=ON` to the build command. -*Note: Setting a default path is not supported by the portal implementation, and any default path passed to NFDe will be ignored. This is a limitation of the portal API, so there is no way NFDe can work around it.* +*Note: Setting a default path is not supported by the portal implementation, and any default path passed to NFDe will be ignored. This is a limitation of the portal API, so there is no way NFDe can work around it. If this feature is something you desire, please show your interest on https://github.com/flatpak/xdg-desktop-portal/pull/874.* + +*Note 2: The folder picker is only supported on org.freedesktop.portal.FileChooser interface version >= 3, which corresponds to xdg-desktop-portal version >= 1.7.1. `NFD_PickFolder()` will query the interface version at runtime, and return `NFD_ERROR` if the version is too low. ### What is a portal? @@ -256,6 +264,12 @@ Unlike Windows and MacOS, Linux does not have a file chooser baked into the oper Flatpak was introduced in 2015, and with it came a standardized interface to open a file chooser. Applications using this interface did not need to come with a file chooser, and could use the one provided by Flatpak. This interface became known as the desktop portal, and its use expanded to non-Flatpak applications. Now, most major desktop Linux distros come with the desktop portal installed, with file choosers that fit the theme of the distro. Users can also install a different portal backend if desired. There are currently two known backends: GTK and KDE. (XFCE does not currently seem to have a portal backend.) +## Platform-specific Quirks + +### MacOS + +- If the MacOS deployment target is ≥ 11.0, the [allowedContentTypes](https://developer.apple.com/documentation/appkit/nssavepanel/3566857-allowedcontenttypes?language=objc) property of NSSavePanel is used instead of the deprecated [allowedFileTypes](https://developer.apple.com/documentation/appkit/nssavepanel/1534419-allowedfiletypes?language=objc) property for file filters. Thus, if you are filtering by a custom file extension specific to your application, you will need to define the data type in your `Info.plist` file as per the [Apple documentation](https://developer.apple.com/documentation/uniformtypeidentifiers/defining_file_and_data_types_for_your_app). (It is possible to force NFDe to use allowedFileTypes by adding `-DNFD_USE_ALLOWEDCONTENTTYPES_IF_AVAILABLE=OFF` to your CMake build command, but this is not recommended. If you need to support older MacOS versions, you should be setting the correct deployment target instead.) + # Known Limitations # - No support for Windows XP's legacy dialogs such as `GetOpenFileName`. (There are no plans to support this; you shouldn't be still using Windows XP anyway.) diff --git a/deps/nfde/src/CMakeLists.txt b/deps/nfde/src/CMakeLists.txt index 7915cdfe..eac718ca 100644 --- a/deps/nfde/src/CMakeLists.txt +++ b/deps/nfde/src/CMakeLists.txt @@ -25,17 +25,49 @@ if(nfd_PLATFORM STREQUAL PLATFORM_UNIX) endif() if(nfd_PLATFORM STREQUAL PLATFORM_MACOS) + # For setting the filter list, macOS introduced allowedContentTypes in version 11.0 and deprecated allowedFileTypes in 12.0. + # By default (set to ON), NFDe will use allowedContentTypes when targeting macOS >= 11.0. + # Set this option to OFF to always use allowedFileTypes regardless of the target macOS version. + # This is mainly needed for applications that are built on macOS >= 11.0 but should be able to run on lower versions + # and should not be used otherwise. + option(NFD_USE_ALLOWEDCONTENTTYPES_IF_AVAILABLE "Use allowedContentTypes for filter lists on macOS >= 11.0" ON) + find_library(APPKIT_LIBRARY AppKit) + if(NFD_USE_ALLOWEDCONTENTTYPES_IF_AVAILABLE) + include(CheckCXXSourceCompiles) + check_cxx_source_compiles( + " + #include + #if !defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || !defined(__MAC_11_0) || __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_11_0 + static_assert(false); + #endif + int main() { return 0; } + " + NFD_USE_ALLOWEDCONTENTTYPES + ) + if(NFD_USE_ALLOWEDCONTENTTYPES) + find_library(UNIFORMTYPEIDENTIFIERS_LIBRARY UniformTypeIdentifiers) + if(NOT UNIFORMTYPEIDENTIFIERS_LIBRARY) + message(FATAL_ERROR "UniformTypeIdentifiers framework is not available even though we are targeting macOS >= 11.0") + endif() + endif() + endif() list(APPEND SOURCE_FILES nfd_cocoa.m) endif() # Define the library -add_library(${TARGET_NAME} - ${SOURCE_FILES}) +add_library(${TARGET_NAME} ${SOURCE_FILES}) + +if (BUILD_SHARED_LIBS) + target_compile_definitions(${TARGET_NAME} PRIVATE NFD_EXPORT INTERFACE NFD_SHARED) +endif () # Allow includes from include/ target_include_directories(${TARGET_NAME} - PUBLIC include/) + PUBLIC + $ + $ +) if(nfd_PLATFORM STREQUAL PLATFORM_UNIX) if(NOT NFD_PORTAL) @@ -45,15 +77,25 @@ if(nfd_PLATFORM STREQUAL PLATFORM_UNIX) target_include_directories(${TARGET_NAME} PRIVATE ${DBUS_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} - PRIVATE ${DBUS_LIBRARIES}) + PRIVATE ${DBUS_LINK_LIBRARIES}) target_compile_definitions(${TARGET_NAME} PUBLIC NFD_PORTAL) endif() + + option(NFD_APPEND_EXTENSION "Automatically append file extension to an extensionless selection in SaveDialog()" OFF) + if(NFD_APPEND_EXTENSION) + target_compile_definitions(${TARGET_NAME} PRIVATE NFD_APPEND_EXTENSION) + endif() endif() if(nfd_PLATFORM STREQUAL PLATFORM_MACOS) - target_link_libraries(${TARGET_NAME} - PRIVATE ${APPKIT_LIBRARY}) + if(NFD_USE_ALLOWEDCONTENTTYPES) + target_link_libraries(${TARGET_NAME} PRIVATE ${APPKIT_LIBRARY} ${UNIFORMTYPEIDENTIFIERS_LIBRARY}) + target_compile_definitions(${TARGET_NAME} PRIVATE NFD_MACOS_ALLOWEDCONTENTTYPES=1) + else() + target_link_libraries(${TARGET_NAME} PRIVATE ${APPKIT_LIBRARY}) + target_compile_definitions(${TARGET_NAME} PRIVATE NFD_MACOS_ALLOWEDCONTENTTYPES=0) + endif() endif() if(nfd_COMPILER STREQUAL COMPILER_MSVC) @@ -71,6 +113,20 @@ if(nfd_COMPILER STREQUAL COMPILER_GNU) target_compile_options(${TARGET_NAME} PRIVATE -nostdlib -fno-exceptions -fno-rtti) endif() -set_target_properties(${TARGET_NAME} PROPERTIES PUBLIC_HEADER "${PUBLIC_HEADER_FILES}") +set_target_properties(${TARGET_NAME} PROPERTIES + PUBLIC_HEADER "${PUBLIC_HEADER_FILES}" + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR}) -install(TARGETS ${TARGET_NAME} LIBRARY DESTINATION lib ARCHIVE DESTINATION lib PUBLIC_HEADER DESTINATION include) +if (NFD_INSTALL) + include(GNUInstallDirs) + + install(TARGETS ${TARGET_NAME} EXPORT ${TARGET_NAME}-export + LIBRARY DESTINATION ${LIB_INSTALL_DIR} ARCHIVE DESTINATION ${LIB_INSTALL_DIR} PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + ) + install(EXPORT ${TARGET_NAME}-export + DESTINATION lib/cmake/${TARGET_NAME} + NAMESPACE ${TARGET_NAME}:: + FILE ${TARGET_NAME}-config.cmake + ) +endif() diff --git a/deps/nfde/src/include/nfd.h b/deps/nfde/src/include/nfd.h index eb9ba6d8..495e8831 100644 --- a/deps/nfde/src/include/nfd.h +++ b/deps/nfde/src/include/nfd.h @@ -10,6 +10,23 @@ #ifndef _NFD_H #define _NFD_H +#if defined(_WIN32) +#if defined(NFD_EXPORT) +#define NFD_API __declspec(dllexport) +#elif defined(NFD_SHARED) +#define NFD_API __declspec(dllimport) +#endif +#else +#if defined(NFD_EXPORT) || defined(NFD_SHARED) +#if defined(__GNUC__) || defined(__clang__) +#define NFD_API __attribute__((visibility("default"))) +#endif +#endif +#endif +#ifndef NFD_API +#define NFD_API +#endif + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -64,60 +81,60 @@ typedef struct { /* free a file path that was returned by the dialogs */ /* Note: use NFD_PathSet_FreePath to free path from pathset instead of this function */ -void NFD_FreePathN(nfdnchar_t* filePath); +NFD_API void NFD_FreePathN(nfdnchar_t* filePath); /* initialize NFD - call this for every thread that might use NFD, before calling any other NFD * functions on that thread */ -nfdresult_t NFD_Init(void); +NFD_API nfdresult_t NFD_Init(void); /* call this to de-initialize NFD, if NFD_Init returned NFD_OKAY */ -void NFD_Quit(void); +NFD_API void NFD_Quit(void); /* single file open dialog */ /* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns * NFD_OKAY */ /* If filterCount is zero, filterList is ignored (you can use NULL) */ /* If defaultPath is NULL, the operating system will decide */ -nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, - const nfdnfilteritem_t* filterList, - nfdfiltersize_t filterCount, - const nfdnchar_t* defaultPath); +NFD_API nfdresult_t NFD_OpenDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath); /* multiple file open dialog */ /* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function * returns NFD_OKAY */ /* If filterCount is zero, filterList is ignored (you can use NULL) */ /* If defaultPath is NULL, the operating system will decide */ -nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, - const nfdnfilteritem_t* filterList, - nfdfiltersize_t filterCount, - const nfdnchar_t* defaultPath); +NFD_API nfdresult_t NFD_OpenDialogMultipleN(const nfdpathset_t** outPaths, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath); /* save dialog */ /* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns * NFD_OKAY */ /* If filterCount is zero, filterList is ignored (you can use NULL) */ /* If defaultPath is NULL, the operating system will decide */ -nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, - const nfdnfilteritem_t* filterList, - nfdfiltersize_t filterCount, - const nfdnchar_t* defaultPath, - const nfdnchar_t* defaultName); +NFD_API nfdresult_t NFD_SaveDialogN(nfdnchar_t** outPath, + const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath, + const nfdnchar_t* defaultName); /* select folder dialog */ /* It is the caller's responsibility to free `outPath` via NFD_FreePathN() if this function returns * NFD_OKAY */ /* If defaultPath is NULL, the operating system will decide */ -nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath); +NFD_API nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath); /* Get last error -- set when nfdresult_t returns NFD_ERROR */ /* Returns the last error that was set, or NULL if there is no error. */ /* The memory is owned by NFD and should not be freed by user code. */ /* This is *always* ASCII printable characters, so it can be interpreted as UTF-8 without any * conversion. */ -const char* NFD_GetError(void); +NFD_API const char* NFD_GetError(void); /* clear the error */ -void NFD_ClearError(void); +NFD_API void NFD_ClearError(void); /* path set operations */ #ifdef _WIN32 @@ -131,36 +148,37 @@ typedef unsigned int nfdpathsetsize_t; /* Gets the number of entries stored in pathSet */ /* note that some paths might be invalid (NFD_ERROR will be returned by NFD_PathSet_GetPath), so we * might not actually have this number of usable paths */ -nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count); +NFD_API nfdresult_t NFD_PathSet_GetCount(const nfdpathset_t* pathSet, nfdpathsetsize_t* count); /* Gets the UTF-8 path at offset index */ /* It is the caller's responsibility to free `outPath` via NFD_PathSet_FreePathN() if this function * returns NFD_OKAY */ -nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, - nfdpathsetsize_t index, - nfdnchar_t** outPath); +NFD_API nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, + nfdpathsetsize_t index, + nfdnchar_t** outPath); /* Free the path gotten by NFD_PathSet_GetPathN */ #ifdef _WIN32 #define NFD_PathSet_FreePathN NFD_FreePathN #elif __APPLE__ #define NFD_PathSet_FreePathN NFD_FreePathN #else -void NFD_PathSet_FreePathN(const nfdnchar_t* filePath); +NFD_API void NFD_PathSet_FreePathN(const nfdnchar_t* filePath); #endif // _WIN32, __APPLE__ /* Gets an enumerator of the path set. */ /* It is the caller's responsibility to free `enumerator` via NFD_PathSet_FreeEnum() if this * function returns NFD_OKAY, and it should be freed before freeing the pathset. */ -nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, nfdpathsetenum_t* outEnumerator); +NFD_API nfdresult_t NFD_PathSet_GetEnum(const nfdpathset_t* pathSet, + nfdpathsetenum_t* outEnumerator); /* Frees an enumerator of the path set. */ -void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator); +NFD_API void NFD_PathSet_FreeEnum(nfdpathsetenum_t* enumerator); /* Gets the next item from the path set enumerator. * If there are no more items, then *outPaths will be set to NULL. */ /* It is the caller's responsibility to free `*outPath` via NFD_PathSet_FreePath() if this * function returns NFD_OKAY and `*outPath` is not null */ -nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath); +NFD_API nfdresult_t NFD_PathSet_EnumNextN(nfdpathsetenum_t* enumerator, nfdnchar_t** outPath); /* Free the pathSet */ -void NFD_PathSet_Free(const nfdpathset_t* pathSet); +NFD_API void NFD_PathSet_Free(const nfdpathset_t* pathSet); #ifdef _WIN32 @@ -177,50 +195,50 @@ typedef struct { /* UTF-8 compatibility functions */ /* free a file path that was returned */ -void NFD_FreePathU8(nfdu8char_t* outPath); +NFD_API void NFD_FreePathU8(nfdu8char_t* outPath); /* single file open dialog */ /* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns * NFD_OKAY */ -nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, - const nfdu8filteritem_t* filterList, - nfdfiltersize_t count, - const nfdu8char_t* defaultPath); - -/* multiple file open dialog */ -/* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function - * returns NFD_OKAY */ -nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, +NFD_API nfdresult_t NFD_OpenDialogU8(nfdu8char_t** outPath, const nfdu8filteritem_t* filterList, nfdfiltersize_t count, const nfdu8char_t* defaultPath); +/* multiple file open dialog */ +/* It is the caller's responsibility to free `outPaths` via NFD_PathSet_Free() if this function + * returns NFD_OKAY */ +NFD_API nfdresult_t NFD_OpenDialogMultipleU8(const nfdpathset_t** outPaths, + const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + const nfdu8char_t* defaultPath); + /* save dialog */ /* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns * NFD_OKAY */ -nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, - const nfdu8filteritem_t* filterList, - nfdfiltersize_t count, - const nfdu8char_t* defaultPath, - const nfdu8char_t* defaultName); +NFD_API nfdresult_t NFD_SaveDialogU8(nfdu8char_t** outPath, + const nfdu8filteritem_t* filterList, + nfdfiltersize_t count, + const nfdu8char_t* defaultPath, + const nfdu8char_t* defaultName); /* select folder dialog */ /* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns * NFD_OKAY */ -nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath); +NFD_API nfdresult_t NFD_PickFolderU8(nfdu8char_t** outPath, const nfdu8char_t* defaultPath); /* Get the UTF-8 path at offset index */ /* It is the caller's responsibility to free `outPath` via NFD_FreePathU8() if this function returns * NFD_OKAY */ -nfdresult_t NFD_PathSet_GetPathU8(const nfdpathset_t* pathSet, - nfdpathsetsize_t index, - nfdu8char_t** outPath); +NFD_API nfdresult_t NFD_PathSet_GetPathU8(const nfdpathset_t* pathSet, + nfdpathsetsize_t index, + nfdu8char_t** outPath); /* Gets the next item from the path set enumerator. * If there are no more items, then *outPaths will be set to NULL. */ /* It is the caller's responsibility to free `*outPath` via NFD_PathSet_FreePathU8() if this * function returns NFD_OKAY and `*outPath` is not null */ -nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath); +NFD_API nfdresult_t NFD_PathSet_EnumNextU8(nfdpathsetenum_t* enumerator, nfdu8char_t** outPath); #define NFD_PathSet_FreePathU8 NFD_FreePathU8 diff --git a/deps/nfde/src/nfd_cocoa.m b/deps/nfde/src/nfd_cocoa.m index 5d74b13d..aeffb1c4 100644 --- a/deps/nfde/src/nfd_cocoa.m +++ b/deps/nfde/src/nfd_cocoa.m @@ -6,8 +6,28 @@ */ #include +#include #include "nfd.h" +// 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_MACOS_ALLOWEDCONTENTTYPES 1 +#endif +#endif + +#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1 +#include +#endif + static const char* g_errorstr = NULL; static void NFDi_SetError(const char* msg) { @@ -26,10 +46,51 @@ static void NFDi_Free(void* ptr) { free(ptr); } +#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1 +// Returns an NSArray of UTType representing the content types. +static NSArray* BuildAllowedContentTypes(const nfdnfilteritem_t* filterList, + nfdfiltersize_t filterCount) { + NSMutableArray* buildFilterList = [[NSMutableArray alloc] init]; + + for (nfdfiltersize_t filterIndex = 0; filterIndex != filterCount; ++filterIndex) { + // this is the spec to parse (we don't use the friendly name on OS X) + const nfdnchar_t* filterSpec = filterList[filterIndex].spec; + + const nfdnchar_t* p_currentFilterBegin = filterSpec; + for (const nfdnchar_t* p_filterSpec = filterSpec; *p_filterSpec; ++p_filterSpec) { + if (*p_filterSpec == ',') { + // add the extension to the array + NSString* filterStr = [[NSString alloc] + initWithBytes:(const void*)p_currentFilterBegin + length:(sizeof(nfdnchar_t) * (p_filterSpec - p_currentFilterBegin)) + encoding:NSUTF8StringEncoding]; + UTType* filterType = [UTType typeWithFilenameExtension:filterStr + conformingToType:UTTypeData]; + [filterStr release]; + if (filterType) [buildFilterList addObject:filterType]; + p_currentFilterBegin = p_filterSpec + 1; + } + } + // add the extension to the array + NSString* filterStr = [[NSString alloc] initWithUTF8String:p_currentFilterBegin]; + UTType* filterType = [UTType typeWithFilenameExtension:filterStr + conformingToType:UTTypeData]; + [filterStr release]; + if (filterType) [buildFilterList addObject:filterType]; + } + + NSArray* returnArray = [NSArray arrayWithArray:buildFilterList]; + + [buildFilterList release]; + + assert([returnArray count] != 0); + + return returnArray; +} +#else +// Returns an NSArray of NSString representing the file types. static NSArray* BuildAllowedFileTypes(const nfdnfilteritem_t* filterList, nfdfiltersize_t filterCount) { - // Commas and semicolons are the same thing on this platform - NSMutableArray* buildFilterList = [[NSMutableArray alloc] init]; for (nfdfiltersize_t filterIndex = 0; filterIndex != filterCount; ++filterIndex) { @@ -61,6 +122,7 @@ static NSArray* BuildAllowedFileTypes(const nfdnfilteritem_t* filterList, return returnArray; } +#endif static void AddFilterListToDialog(NSSavePanel* dialog, const nfdnfilteritem_t* filterList, @@ -71,11 +133,15 @@ static void AddFilterListToDialog(NSSavePanel* dialog, assert(filterList); - // make NSArray of file types +// Make NSArray of file types and set it on the dialog +// We use setAllowedFileTypes or setAllowedContentTypes depending on the deployment target +#if NFD_MACOS_ALLOWEDCONTENTTYPES == 1 + NSArray* allowedContentTypes = BuildAllowedContentTypes(filterList, filterCount); + [dialog setAllowedContentTypes:allowedContentTypes]; +#else NSArray* allowedFileTypes = BuildAllowedFileTypes(filterList, filterCount); - - // set it on the dialog [dialog setAllowedFileTypes:allowedFileTypes]; +#endif } static void SetDefaultPath(NSSavePanel* dialog, const nfdnchar_t* defaultPath) { @@ -114,6 +180,10 @@ const char* NFD_GetError(void) { return g_errorstr; } +void NFD_ClearError(void) { + NFDi_SetError(NULL); +} + void NFD_FreePathN(nfdnchar_t* filePath) { NFDi_Free((void*)filePath); } diff --git a/deps/nfde/src/nfd_portal.cpp b/deps/nfde/src/nfd_portal.cpp index f5a9302f..23e1f1f4 100644 --- a/deps/nfde/src/nfd_portal.cpp +++ b/deps/nfde/src/nfd_portal.cpp @@ -14,19 +14,26 @@ #include #include #include -#include // for the random token string -#include // for access() +#include // for access() + +#if !defined(__has_include) || !defined(__linux__) +#include // for getrandom() - the random token string +#elif __has_include() +#include +#else // for GLIBC < 2.25 +#include +#define getrandom(buf, sz, flags) syscall(SYS_getrandom, buf, sz, flags) +#endif #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 { @@ -70,7 +77,10 @@ struct DBusMessage_Guard { 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 +/* current non D-Bus error */ +constexpr size_t OWNED_ERR_LEN = 1024; +char owned_err[OWNED_ERR_LEN]{}; +/* current error (may be a pointer to dbus_err.message, owned_err, 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 @@ -81,6 +91,14 @@ void NFDi_SetError(const char* msg) { err_ptr = msg; } +void NFDi_SetFormattedError(const char* format, ...) { + va_list args; + va_start(args, format); + vsnprintf(owned_err, OWNED_ERR_LEN, format, args); + va_end(args); + err_ptr = owned_err; +} + template T* copy(const T* begin, const T* end, T* out) { for (; begin != end; ++begin) { @@ -112,6 +130,10 @@ 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 = "*"; +constexpr const char* DBUS_DESTINATION = "org.freedesktop.portal.Desktop"; +constexpr const char* DBUS_PATH = "/org/freedesktop/portal/desktop"; +constexpr const char* DBUS_FILECHOOSER_IFACE = "org.freedesktop.portal.FileChooser"; +constexpr const char* DBUS_REQUEST_IFACE = "org.freedesktop.portal.Request"; template void AppendOpenFileQueryTitle(DBusMessageIter&); @@ -462,7 +484,7 @@ void AppendSaveFileQueryDictEntryCurrentName(DBusMessageIter& sub_iter, const ch dbus_message_iter_close_container(&sub_iter, &sub_sub_iter); } -void AppendSaveFileQueryDictEntryCurrentFolder(DBusMessageIter& sub_iter, const char* path) { +void AppendOpenFileQueryDictEntryCurrentFolder(DBusMessageIter& sub_iter, const char* path) { if (!path) return; DBusMessageIter sub_sub_iter; DBusMessageIter variant_iter; @@ -529,7 +551,8 @@ template void AppendOpenFileQueryParams(DBusMessage* query, const char* handle_token, const nfdnfilteritem_t* filterList, - nfdfiltersize_t filterCount) { + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { DBusMessageIter iter; dbus_message_iter_init_append(query, &iter); @@ -543,6 +566,7 @@ void AppendOpenFileQueryParams(DBusMessage* query, AppendOpenFileQueryDictEntryMultiple(sub_iter); AppendOpenFileQueryDictEntryDirectory(sub_iter); AppendOpenFileQueryDictEntryFilters(sub_iter, filterList, filterCount); + AppendOpenFileQueryDictEntryCurrentFolder(sub_iter, defaultPath); dbus_message_iter_close_container(&iter, &sub_iter); } @@ -565,7 +589,7 @@ void AppendSaveFileQueryParams(DBusMessage* query, AppendOpenFileQueryDictEntryHandleToken(sub_iter, handle_token); AppendSaveFileQueryDictEntryFilters(sub_iter, filterList, filterCount, defaultName); AppendSaveFileQueryDictEntryCurrentName(sub_iter, defaultName); - AppendSaveFileQueryDictEntryCurrentFolder(sub_iter, defaultPath); + AppendOpenFileQueryDictEntryCurrentFolder(sub_iter, defaultPath); AppendSaveFileQueryDictEntryCurrentFile(sub_iter, defaultPath, defaultName); dbus_message_iter_close_container(&iter, &sub_iter); } @@ -649,7 +673,9 @@ nfdresult_t ReadResponseResults(DBusMessage* msg, DBusMessageIter& resultsIter) return NFD_CANCEL; } else { // Some error occurred - NFDi_SetError("D-Bus file dialog interaction was ended abruptly."); + NFDi_SetFormattedError( + "D-Bus file dialog interaction was ended abruptly with response code %u.", + resp_code); return NFD_ERROR; } } @@ -720,14 +746,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 @@ -763,47 +789,37 @@ nfdresult_t ReadResponseUrisSingleAndCurrentExtension(DBusMessage* msg, [&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(¤t_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(¤t_filter_iter, ¤t_filter_struct_iter); if (!dbus_message_iter_next(¤t_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(¤t_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(¤t_filter_struct_iter, ¤t_filter_array_iter); if (dbus_message_iter_get_arg_type(¤t_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(¤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."); return NFD_OKAY; } dbus_uint32_t type; dbus_message_iter_get_basic(¤t_filter_extn_iter, &type); if (type != 0) { - // NFDi_SetError("Wrong filter type."); return NFD_OKAY; } if (!dbus_message_iter_next(¤t_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(¤t_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(¤t_filter_extn_iter, &tmp_extn); @@ -942,29 +958,95 @@ 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* file_uri_iter = fileUri; 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."); + for (; prefix_begin != prefix_end; ++prefix_begin, ++file_uri_iter) { + if (*prefix_begin != *file_uri_iter) { + NFDi_SetFormattedError( + "D-Bus freedesktop portal returned \"%s\", which is not a file URI.", fileUri); 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(file_uri_iter, decoded_len, file_uri_end)) { + NFDi_SetFormattedError("D-Bus freedesktop portal returned a malformed URI \"%s\".", + fileUri); + return NFD_ERROR; + } + char* const path_without_prefix = NFDi_Malloc(decoded_len + 1); + char* const out_end = UriDecodeUnchecked(file_uri_iter, 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) { @@ -985,19 +1067,30 @@ bool TryGetValidExtension(const char* extn, // 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* file_uri_iter = fileUri; 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."); + for (; prefix_begin != prefix_end; ++prefix_begin, ++file_uri_iter) { + if (*prefix_begin != *file_uri_iter) { + NFDi_SetFormattedError( + "D-Bus freedesktop portal returned \"%s\", which is not a file URI.", fileUri); return NFD_ERROR; } } - 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(file_uri_iter, decoded_len, file_uri_end)) { + NFDi_SetFormattedError("D-Bus freedesktop portal returned a malformed URI \"%s\".", + fileUri); + 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 +1098,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(file_uri_iter, 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(file_uri_iter, 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; @@ -1028,7 +1122,8 @@ nfdresult_t AllocAndCopyFilePathWithExtn(const char* fileUri, const char* extn, template nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg, const nfdnfilteritem_t* filterList, - nfdfiltersize_t filterCount) { + nfdfiltersize_t filterCount, + const nfdnchar_t* defaultPath) { const char* handle_token_ptr; char* handle_obj_path = MakeUniqueObjectPath(&handle_token_ptr); Free_Guard handle_obj_path_guard(handle_obj_path); @@ -1045,13 +1140,11 @@ nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg, // 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* query = dbus_message_new_method_call( + DBUS_DESTINATION, DBUS_PATH, DBUS_FILECHOOSER_IFACE, "OpenFile"); DBusMessage_Guard query_guard(query); AppendOpenFileQueryParams( - query, handle_token_ptr, filterList, filterCount); + query, handle_token_ptr, filterList, filterCount, defaultPath); DBusMessage* reply = dbus_connection_send_with_reply_and_block(dbus_conn, query, DBUS_TIMEOUT_INFINITE, &err); @@ -1090,7 +1183,7 @@ nfdresult_t NFD_DBus_OpenFile(DBusMessage*& outMsg, DBusMessage* msg = dbus_connection_pop_message(dbus_conn); if (!msg) break; - if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) { + if (dbus_message_is_signal(msg, DBUS_REQUEST_IFACE, "Response")) { // this is the response we're looking for outMsg = msg; return NFD_OKAY; @@ -1129,10 +1222,8 @@ nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg, // 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* query = dbus_message_new_method_call( + DBUS_DESTINATION, DBUS_PATH, DBUS_FILECHOOSER_IFACE, "SaveFile"); DBusMessage_Guard query_guard(query); AppendSaveFileQueryParams( query, handle_token_ptr, filterList, filterCount, defaultPath, defaultName); @@ -1174,7 +1265,7 @@ nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg, DBusMessage* msg = dbus_connection_pop_message(dbus_conn); if (!msg) break; - if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) { + if (dbus_message_is_signal(msg, DBUS_REQUEST_IFACE, "Response")) { // this is the response we're looking for outMsg = msg; return NFD_OKAY; @@ -1188,6 +1279,57 @@ nfdresult_t NFD_DBus_SaveFile(DBusMessage*& outMsg, return NFD_ERROR; } +nfdresult_t NFD_DBus_GetVersion(dbus_uint32_t& outVersion) { + 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); + + DBusMessage* query = dbus_message_new_method_call("org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.DBus.Properties", + "Get"); + DBusMessage_Guard query_guard(query); + { + DBusMessageIter iter; + dbus_message_iter_init_append(query, &iter); + + constexpr const char* STR_INTERFACE = "org.freedesktop.portal.FileChooser"; + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_INTERFACE); + constexpr const char* STR_VERSION = "version"; + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &STR_VERSION); + } + + 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); + { + DBusMessageIter iter; + if (!dbus_message_iter_init(reply, &iter)) { + NFDi_SetError("D-Bus reply for version query is missing an argument."); + return NFD_ERROR; + } + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) { + NFDi_SetError("D-Bus reply for version query is not a variant."); + return NFD_ERROR; + } + DBusMessageIter variant_iter; + dbus_message_iter_recurse(&iter, &variant_iter); + if (dbus_message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_UINT32) { + NFDi_SetError("D-Bus reply for version query is not a uint32."); + return NFD_ERROR; + } + dbus_message_iter_get_basic(&variant_iter, &outVersion); + } + + return NFD_OKAY; +} + } // namespace /* public */ @@ -1202,7 +1344,7 @@ void NFD_ClearError(void) { } nfdresult_t NFD_Init(void) { - // Initialize dbus_error + // Initialize dbus_err dbus_error_init(&dbus_err); // Get DBus connection dbus_conn = dbus_bus_get(DBUS_BUS_SESSION, &dbus_err); @@ -1233,37 +1375,35 @@ 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(msg, filterList, filterCount); + const nfdresult_t res = + NFD_DBus_OpenFile(msg, filterList, filterCount, defaultPath); if (res != NFD_OKAY) { return res; } } 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, 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(msg, filterList, filterCount); + const nfdresult_t res = + NFD_DBus_OpenFile(msg, filterList, filterCount, defaultPath); if (res != NFD_OKAY) { return res; } @@ -1295,51 +1435,67 @@ 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 } nfdresult_t NFD_PickFolderN(nfdnchar_t** outPath, const nfdnchar_t* defaultPath) { (void)defaultPath; // Default path not supported for portal backend + { + dbus_uint32_t version; + const nfdresult_t res = NFD_DBus_GetVersion(version); + if (res != NFD_OKAY) { + return res; + } + if (version < 3) { + NFDi_SetFormattedError( + "The xdg-desktop-portal installed on this system does not support a folder picker; " + "at least version 3 of the org.freedesktop.portal.FileChooser interface is " + "required but the installed interface version is %u.", + version); + return NFD_ERROR; + } + } + DBusMessage* msg; { - const nfdresult_t res = NFD_DBus_OpenFile(msg, nullptr, 0); + const nfdresult_t res = NFD_DBus_OpenFile(msg, nullptr, 0, defaultPath); if (res != NFD_OKAY) { return res; } } 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) { @@ -1356,20 +1512,25 @@ nfdresult_t NFD_PathSet_GetPathN(const nfdpathset_t* pathSet, DBusMessage* msg = const_cast(static_cast(pathSet)); DBusMessageIter uri_iter; ReadResponseUrisUnchecked(msg, uri_iter); - while (index > 0) { - --index; + nfdpathsetsize_t rem_index = index; + while (rem_index > 0) { + --rem_index; if (!dbus_message_iter_next(&uri_iter)) { - NFDi_SetError("Index out of bounds."); + NFDi_SetFormattedError( + "Index out of bounds; you asked for index %u but there are only %u file paths " + "available.", + index, + index - rem_index); 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."); + 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 +1563,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;