From 13935af42b804eaf842b83e0c4adaaa684c4b9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:12:07 +0200 Subject: [PATCH 01/15] Update to SFML 3.1.0 - Require CMake 3.28 to match SFML - Install HarfBuzz, MbedTLS and libssh2 for Linux CI builds - Bump CSFML and NuGet package version to 3.1.0 --- .github/workflows/ci.yml | 13 ++++++------- .github/workflows/release.yml | 2 +- CMakeLists.txt | 6 +++--- include/CSFML/Config.h | 2 +- readme.md | 2 +- tools/BuildMacOS.sh | 4 ++-- tools/nuget/CSFML/CSFML.csproj | 2 +- tools/nuget/build.linux.sh | 8 ++++---- tools/nuget/build.macos.sh | 8 ++++---- tools/nuget/build.win.ps1 | 2 +- 10 files changed, 24 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70e4b812..03ffc978 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ concurrency: cancel-in-progress: true env: - SFML_VERSION: 3.0.2 + SFML_VERSION: 3.1.0 defaults: run: @@ -26,8 +26,7 @@ jobs: platform: - { name: Windows VS2026 x86, os: windows-2025-vs2026, flags: -AWin32 } - { name: Windows VS2026 x64, os: windows-2025-vs2026, flags: -Ax64 } - # ClangCL is disabled until we build against SFML 3.1, the FLAC version bundled with SFML 3.0 fails to compile with VS2026's ClangCL - # - { name: Windows ClangCL, os: windows-2025-vs2026, flags: -T ClangCL } + - { name: Windows ClangCL, os: windows-2025-vs2026, flags: -T ClangCL } - { name: Windows Clang, os: windows-2025-vs2026, flags: -GNinja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ } - { name: Linux GCC, os: ubuntu-24.04, flags: -GNinja } - { name: Linux Clang, os: ubuntu-24.04, flags: -GNinja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ } @@ -43,12 +42,12 @@ jobs: - name: Get CMake and Ninja uses: lukka/get-cmake@latest with: - cmakeVersion: ${{ runner.os == 'Windows' && '4.2' || '3.22' }} # Visual Studio 2026 generator requires CMake 4.2 or later + cmakeVersion: ${{ runner.os == 'Windows' && '4.2' || '3.28' }} # Visual Studio 2026 generator requires CMake 4.2 or later ninjaVersion: latest - name: Install Linux Dependencies if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install libxrandr-dev libxcursor-dev libxi-dev libudev-dev libflac-dev libvorbis-dev libgl1-mesa-dev libegl1-mesa-dev libfreetype-dev + run: sudo apt-get update && sudo apt-get install libxrandr-dev libxcursor-dev libxi-dev libudev-dev libflac-dev libvorbis-dev libgl1-mesa-dev libegl1-mesa-dev libfreetype-dev libharfbuzz-dev libmbedtls-dev libssh2-1-dev - name: Checkout SFML uses: actions/checkout@v7 @@ -128,7 +127,7 @@ jobs: - name: Get CMake and Ninja uses: lukka/get-cmake@latest with: - cmakeVersion: ${{ runner.os == 'Windows' && '4.2' || '3.22' }} # Visual Studio 2026 generator requires CMake 4.2 or later + cmakeVersion: ${{ runner.os == 'Windows' && '4.2' || '3.28' }} # Visual Studio 2026 generator requires CMake 4.2 or later ninjaVersion: latest - name: Install Dependencies @@ -171,7 +170,7 @@ jobs: - name: Get CMake and Ninja uses: lukka/get-cmake@latest with: - cmakeVersion: ${{ runner.os == 'Windows' && '4.2' || '3.22' }} # Visual Studio 2026 generator requires CMake 4.2 or later + cmakeVersion: ${{ runner.os == 'Windows' && '4.2' || '3.28' }} # Visual Studio 2026 generator requires CMake 4.2 or later ninjaVersion: latest - name: Install Doxygen diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 46cb8645..0c9f99c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ concurrency: cancel-in-progress: true env: - SFML_VERSION: 3.0.2 + SFML_VERSION: 3.1.0 defaults: run: diff --git a/CMakeLists.txt b/CMakeLists.txt index baba8f5e..f24030b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.22) +cmake_minimum_required(VERSION 3.28) # define a macro that helps defining an option macro(csfml_set_option var default type docstring) @@ -13,7 +13,7 @@ endmacro() csfml_set_option(CMAKE_BUILD_TYPE Release STRING "Choose the type of build (Debug or Release)") # project name -project(CSFML VERSION 3.0.0) +project(CSFML VERSION 3.1.0) # we use the paths from the cmake GNUInstallDirs module as defaults # you can override these if you like @@ -66,7 +66,7 @@ endif() if(CSFML_BUILD_NETWORK) list(PREPEND SFML_MODULES "Network") endif() -find_package(SFML 3.0 COMPONENTS ${SFML_MODULES} REQUIRED) +find_package(SFML 3.1 COMPONENTS ${SFML_MODULES} REQUIRED) # add the subdirectories add_subdirectory(src/CSFML) diff --git a/include/CSFML/Config.h b/include/CSFML/Config.h index 12b49ec3..5d99b00b 100644 --- a/include/CSFML/Config.h +++ b/include/CSFML/Config.h @@ -36,7 +36,7 @@ // Define the CSFML version //////////////////////////////////////////////////////////// #define CSFML_VERSION_MAJOR 3 -#define CSFML_VERSION_MINOR 0 +#define CSFML_VERSION_MINOR 1 #define CSFML_VERSION_PATCH 0 diff --git a/readme.md b/readme.md index 4280ce49..ffe06530 100644 --- a/readme.md +++ b/readme.md @@ -38,7 +38,7 @@ Here's how to get started. 1. Install SFML. This may be done in a variety of ways including using a system package manager or building and installing SFML from source. -The major version of SFML must match the major version of CSFML. +The major version of SFML must match the major version of CSFML and the minor version of SFML must be the same or newer than the one of CSFML. 2. Configure CSFML. If you are building CSFML for the purpose of contributing, we recommend using the `dev` CMake preset. This will enable a number of useful settings related to developing the library. diff --git a/tools/BuildMacOS.sh b/tools/BuildMacOS.sh index ab8291b4..b6eaccc4 100644 --- a/tools/BuildMacOS.sh +++ b/tools/BuildMacOS.sh @@ -1,7 +1,7 @@ #!/bin/sh -VERSION="3.0.2" -VERSION_C="3.0.0" +VERSION="3.1.0" +VERSION_C="3.1.0" # BUILD_CSFML=FALSE BUILD_CSFML=TRUE BUILD_SFML=FALSE diff --git a/tools/nuget/CSFML/CSFML.csproj b/tools/nuget/CSFML/CSFML.csproj index 6b2d69f3..b115a13f 100644 --- a/tools/nuget/CSFML/CSFML.csproj +++ b/tools/nuget/CSFML/CSFML.csproj @@ -4,7 +4,7 @@ netstandard2.0 true - 3.0.0 + 3.1.0 Laurent Gomila sfml csfml Copyright © Laurent Gomila diff --git a/tools/nuget/build.linux.sh b/tools/nuget/build.linux.sh index c2db8915..6090f102 100755 --- a/tools/nuget/build.linux.sh +++ b/tools/nuget/build.linux.sh @@ -58,7 +58,7 @@ if [[ -n "$CXX_COMPILER" ]]; then echo "Using custom compilers: CXX=$CXX_COMPILER, CC=$C_COMPILER" fi -SFMLBranch="3.0.2" # The branch or tag of the SFML repository to be cloned +SFMLBranch="3.1.0" # The branch or tag of the SFML repository to be cloned CSFMLDir="$(realpath ../../)" # The directory of the source code of CSFML OutDir="./CSFML/runtimes/$RID/native" # The base directory of all CSFML modules, used to copy the final libraries @@ -145,8 +145,8 @@ cmake --build . --config Release # STEP 5: Copy result to the NuGet folders # # ======================================== # -SFMLMajorMinor="3.0" -CSFMLMajorMinor="3.0" +SFMLMajorMinor="3.1" +CSFMLMajorMinor="3.1" # Copies one SFML and CSFML module into the NuGet package # The module name must be passed to this function as an argument, in lowercase @@ -161,7 +161,7 @@ copymodule() # SFML.Net only searches for the name with common pre- and suffixes # As such we need to ship e.g. libcsfml-graphics.so # But the CSFML libs will look for the major.minor version - # As such we also need to ship e.g. libcsfml-graphics.so.3.0 + # As such we also need to ship e.g. libcsfml-graphics.so.3.1 # Unfortunately NuGet package don't support symlinks: https://github.com/NuGet/Home/issues/10734 # For SFML, we can just ship one version that CSFML will be looking for cp "$SFMLLibDir/libsfml-$MODULE.so.$SFMLMajorMinor" "$OutDir" diff --git a/tools/nuget/build.macos.sh b/tools/nuget/build.macos.sh index cfa76fc8..9c17e58e 100755 --- a/tools/nuget/build.macos.sh +++ b/tools/nuget/build.macos.sh @@ -46,7 +46,7 @@ echo "Please note that all SFML dependencies must be installed and available to RID="$1" -SFMLBranch="3.0.2" # The branch or tag of the SFML repository to be cloned +SFMLBranch="3.1.0" # The branch or tag of the SFML repository to be cloned CSFMLDir="$(grealpath "$(git rev-parse --show-toplevel)")" # The directory of the source code of CSFML OutDir="./CSFML/runtimes/$RID/native" # The base directory of all CSFML modules, used to copy the final libraries @@ -150,9 +150,9 @@ cmake --build . --config Release --target install # STEP 5: Copy result to the NuGet folders # # ======================================== # -SFMLMajorMinor="3.0" +SFMLMajorMinor="3.1" SFMLMajorMinorPatch="$SFMLMajorMinor.0" -CSFMLMajorMinor="3.0" +CSFMLMajorMinor="3.1" CSFMLMajorMinorPatch="$CSFMLMajorMinor.0" # Copies one SFML and CSFML module into the NuGet package @@ -168,7 +168,7 @@ copymodule() # SFML.Net only searches for the name with common pre- and suffixes # As such we need to ship e.g. libcsfml-graphics.dylib # But the CSFML libs will look for the major.minor version - # As such we also need to ship e.g. libcsfml-graphics.3.0.dylib + # As such we also need to ship e.g. libcsfml-graphics.3.1.dylib # Unfortunately NuGet package don't support symlinks: https://github.com/NuGet/Home/issues/10734 # For SFML, we can just ship one version that CSFML will be looking for cp "$SFMLLibDir/libsfml-$MODULE.$SFMLMajorMinor.dylib" "$OutDir" diff --git a/tools/nuget/build.win.ps1 b/tools/nuget/build.win.ps1 index 33858675..4cdbeef1 100644 --- a/tools/nuget/build.win.ps1 +++ b/tools/nuget/build.win.ps1 @@ -43,7 +43,7 @@ Write-Output "Building $RID" Write-Output "Using $Generator as the cmake generator" Write-Output "Using architecture $ArchitectureCMake" -$SFMLBranch = "3.0.2" # The branch or tag of the SFML repository to be cloned +$SFMLBranch = "3.1.0" # The branch or tag of the SFML repository to be cloned $CSFMLDir = (Get-Item (git rev-parse --show-toplevel)).FullName # The directory of the source code of CSFML $OutDir = "./CSFML/runtimes/$RID/native" # The directory of all CSFML modules, used to copy the final dlls From 0a4871cfbec372aae68ddb608da09b6da04ca8b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:19:41 +0200 Subject: [PATCH 02/15] Mark C functions of deprecated SFML APIs as deprecated The deprecated functions remain available, matching SFML. Font kerning now calls the char32_t overload, since the std::uint32_t one is deprecated. --- include/CSFML/Graphics/RenderWindow.h | 5 +- include/CSFML/Graphics/Text.h | 4 +- include/CSFML/Network/Ftp.h | 145 ++++++++++++++++++++------ include/CSFML/Window/Touch.h | 12 ++- src/CSFML/Graphics/Font.cpp | 4 +- 5 files changed, 129 insertions(+), 41 deletions(-) diff --git a/include/CSFML/Graphics/RenderWindow.h b/include/CSFML/Graphics/RenderWindow.h index d6d894c7..bcbad5af 100644 --- a/include/CSFML/Graphics/RenderWindow.h +++ b/include/CSFML/Graphics/RenderWindow.h @@ -708,8 +708,11 @@ CSFML_GRAPHICS_API void sfMouse_setPositionRenderWindow(sfVector2i position, con /// /// \return Current position of \a finger, or undefined if it's not down /// +/// \deprecated Use the position of the touch events (sfEvtTouchBegan, sfEvtTouchMoved and sfEvtTouchEnded) instead +/// //////////////////////////////////////////////////////////// -CSFML_GRAPHICS_API sfVector2i sfTouch_getPositionRenderWindow(unsigned int finger, const sfRenderWindow* relativeTo); +CSFML_GRAPHICS_API CSFML_DEPRECATED sfVector2i sfTouch_getPositionRenderWindow(unsigned int finger, + const sfRenderWindow* relativeTo); //////////////////////////////////////////////////////////// /// \brief Create a Vulkan rendering surface diff --git a/include/CSFML/Graphics/Text.h b/include/CSFML/Graphics/Text.h index a8a0df8f..ba52b190 100644 --- a/include/CSFML/Graphics/Text.h +++ b/include/CSFML/Graphics/Text.h @@ -490,8 +490,10 @@ CSFML_GRAPHICS_API float sfText_getOutlineThickness(const sfText* text); /// /// \return Position of the character /// +/// \deprecated Use sfText_getShapedGlyphs instead +/// //////////////////////////////////////////////////////////// -CSFML_GRAPHICS_API sfVector2f sfText_findCharacterPos(const sfText* text, size_t index); +CSFML_GRAPHICS_API CSFML_DEPRECATED sfVector2f sfText_findCharacterPos(const sfText* text, size_t index); //////////////////////////////////////////////////////////// /// \brief Get the local bounding rectangle of a text diff --git a/include/CSFML/Network/Ftp.h b/include/CSFML/Network/Ftp.h index 1469e703..87937c9a 100644 --- a/include/CSFML/Network/Ftp.h +++ b/include/CSFML/Network/Ftp.h @@ -119,8 +119,10 @@ typedef enum /// /// \param ftpListingResponse Ftp listing response to destroy /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API void sfFtpListingResponse_destroy(const sfFtpListingResponse* ftpListingResponse); +CSFML_NETWORK_API CSFML_DEPRECATED void sfFtpListingResponse_destroy(const sfFtpListingResponse* ftpListingResponse); //////////////////////////////////////////////////////////// /// \brief Check if a FTP listing response status code means a success @@ -132,8 +134,10 @@ CSFML_NETWORK_API void sfFtpListingResponse_destroy(const sfFtpListingResponse* /// /// \return true if the status is a success, false if it is a failure /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API bool sfFtpListingResponse_isOk(const sfFtpListingResponse* ftpListingResponse); +CSFML_NETWORK_API CSFML_DEPRECATED bool sfFtpListingResponse_isOk(const sfFtpListingResponse* ftpListingResponse); //////////////////////////////////////////////////////////// /// \brief Get the status code of a FTP listing response @@ -142,8 +146,10 @@ CSFML_NETWORK_API bool sfFtpListingResponse_isOk(const sfFtpListingResponse* ftp /// /// \return Status code /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpStatus sfFtpListingResponse_getStatus(const sfFtpListingResponse* ftpListingResponse); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpStatus sfFtpListingResponse_getStatus(const sfFtpListingResponse* ftpListingResponse); //////////////////////////////////////////////////////////// /// \brief Get the full message contained in a FTP listing response @@ -152,8 +158,10 @@ CSFML_NETWORK_API sfFtpStatus sfFtpListingResponse_getStatus(const sfFtpListingR /// /// \return The response message /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API const char* sfFtpListingResponse_getMessage(const sfFtpListingResponse* ftpListingResponse); +CSFML_NETWORK_API CSFML_DEPRECATED const char* sfFtpListingResponse_getMessage(const sfFtpListingResponse* ftpListingResponse); //////////////////////////////////////////////////////////// /// \brief Return the number of directory/file names contained in a FTP listing response @@ -162,8 +170,10 @@ CSFML_NETWORK_API const char* sfFtpListingResponse_getMessage(const sfFtpListing /// /// \return Total number of names available /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API size_t sfFtpListingResponse_getCount(const sfFtpListingResponse* ftpListingResponse); +CSFML_NETWORK_API CSFML_DEPRECATED size_t sfFtpListingResponse_getCount(const sfFtpListingResponse* ftpListingResponse); //////////////////////////////////////////////////////////// /// \brief Return a directory/file name contained in a FTP listing response @@ -173,16 +183,21 @@ CSFML_NETWORK_API size_t sfFtpListingResponse_getCount(const sfFtpListingRespons /// /// \return The requested name /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API const char* sfFtpListingResponse_getName(const sfFtpListingResponse* ftpListingResponse, size_t index); +CSFML_NETWORK_API CSFML_DEPRECATED const char* sfFtpListingResponse_getName(const sfFtpListingResponse* ftpListingResponse, + size_t index); //////////////////////////////////////////////////////////// /// \brief Destroy a FTP directory response /// /// \param ftpDirectoryResponse Ftp directory response to destroy /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API void sfFtpDirectoryResponse_destroy(const sfFtpDirectoryResponse* ftpDirectoryResponse); +CSFML_NETWORK_API CSFML_DEPRECATED void sfFtpDirectoryResponse_destroy(const sfFtpDirectoryResponse* ftpDirectoryResponse); //////////////////////////////////////////////////////////// /// \brief Check if a FTP directory response status code means a success @@ -194,8 +209,10 @@ CSFML_NETWORK_API void sfFtpDirectoryResponse_destroy(const sfFtpDirectoryRespon /// /// \return true if the status is a success, false if it is a failure /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API bool sfFtpDirectoryResponse_isOk(const sfFtpDirectoryResponse* ftpDirectoryResponse); +CSFML_NETWORK_API CSFML_DEPRECATED bool sfFtpDirectoryResponse_isOk(const sfFtpDirectoryResponse* ftpDirectoryResponse); //////////////////////////////////////////////////////////// /// \brief Get the status code of a FTP directory response @@ -204,8 +221,11 @@ CSFML_NETWORK_API bool sfFtpDirectoryResponse_isOk(const sfFtpDirectoryResponse* /// /// \return Status code /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpStatus sfFtpDirectoryResponse_getStatus(const sfFtpDirectoryResponse* ftpDirectoryResponse); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpStatus + sfFtpDirectoryResponse_getStatus(const sfFtpDirectoryResponse* ftpDirectoryResponse); //////////////////////////////////////////////////////////// /// \brief Get the full message contained in a FTP directory response @@ -214,8 +234,11 @@ CSFML_NETWORK_API sfFtpStatus sfFtpDirectoryResponse_getStatus(const sfFtpDirect /// /// \return The response message /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API const char* sfFtpDirectoryResponse_getMessage(const sfFtpDirectoryResponse* ftpDirectoryResponse); +CSFML_NETWORK_API CSFML_DEPRECATED const char* sfFtpDirectoryResponse_getMessage( + const sfFtpDirectoryResponse* ftpDirectoryResponse); //////////////////////////////////////////////////////////// /// \brief Get the directory returned in a FTP directory response @@ -227,8 +250,11 @@ CSFML_NETWORK_API const char* sfFtpDirectoryResponse_getMessage(const sfFtpDirec /// /// \return Directory name or NULL if it failed /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API const char* sfFtpDirectoryResponse_getDirectory(const sfFtpDirectoryResponse* ftpDirectoryResponse); +CSFML_NETWORK_API CSFML_DEPRECATED const char* sfFtpDirectoryResponse_getDirectory( + const sfFtpDirectoryResponse* ftpDirectoryResponse); //////////////////////////////////////////////////////////// @@ -241,8 +267,11 @@ CSFML_NETWORK_API const char* sfFtpDirectoryResponse_getDirectory(const sfFtpDir /// /// \return Directory name or NULL if it failed /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API const sfChar32* sfFtpDirectoryResponse_getDirectoryUnicode(const sfFtpDirectoryResponse* ftpDirectoryResponse); +CSFML_NETWORK_API CSFML_DEPRECATED const sfChar32* sfFtpDirectoryResponse_getDirectoryUnicode( + const sfFtpDirectoryResponse* ftpDirectoryResponse); //////////////////////////////////////////////////////////// @@ -250,8 +279,10 @@ CSFML_NETWORK_API const sfChar32* sfFtpDirectoryResponse_getDirectoryUnicode(con /// /// \param ftpResponse Ftp response to destroy /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API void sfFtpResponse_destroy(const sfFtpResponse* ftpResponse); +CSFML_NETWORK_API CSFML_DEPRECATED void sfFtpResponse_destroy(const sfFtpResponse* ftpResponse); //////////////////////////////////////////////////////////// /// \brief Check if a FTP response status code means a success @@ -263,8 +294,10 @@ CSFML_NETWORK_API void sfFtpResponse_destroy(const sfFtpResponse* ftpResponse); /// /// \return true if the status is a success, false if it is a failure /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API bool sfFtpResponse_isOk(const sfFtpResponse* ftpResponse); +CSFML_NETWORK_API CSFML_DEPRECATED bool sfFtpResponse_isOk(const sfFtpResponse* ftpResponse); //////////////////////////////////////////////////////////// /// \brief Get the status code of a FTP response @@ -273,8 +306,10 @@ CSFML_NETWORK_API bool sfFtpResponse_isOk(const sfFtpResponse* ftpResponse); /// /// \return Status code /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpStatus sfFtpResponse_getStatus(const sfFtpResponse* ftpResponse); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpStatus sfFtpResponse_getStatus(const sfFtpResponse* ftpResponse); //////////////////////////////////////////////////////////// /// \brief Get the full message contained in a FTP response @@ -283,24 +318,30 @@ CSFML_NETWORK_API sfFtpStatus sfFtpResponse_getStatus(const sfFtpResponse* ftpRe /// /// \return The response message /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API const char* sfFtpResponse_getMessage(const sfFtpResponse* ftpResponse); +CSFML_NETWORK_API CSFML_DEPRECATED const char* sfFtpResponse_getMessage(const sfFtpResponse* ftpResponse); //////////////////////////////////////////////////////////// /// \brief Create a new Ftp object /// /// \return A new sfFtp object /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtp* sfFtp_create(void); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtp* sfFtp_create(void); //////////////////////////////////////////////////////////// /// \brief Destroy a Ftp object /// /// \param ftp Ftp object to destroy /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API void sfFtp_destroy(const sfFtp* ftp); +CSFML_NETWORK_API CSFML_DEPRECATED void sfFtp_destroy(const sfFtp* ftp); //////////////////////////////////////////////////////////// /// \brief Connect to the specified FTP server @@ -321,8 +362,10 @@ CSFML_NETWORK_API void sfFtp_destroy(const sfFtp* ftp); /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_connect(sfFtp* ftp, sfIpAddress server, unsigned short port, sfTime timeout); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_connect(sfFtp* ftp, sfIpAddress server, unsigned short port, sfTime timeout); //////////////////////////////////////////////////////////// /// \brief Log in using an anonymous account @@ -334,8 +377,10 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_connect(sfFtp* ftp, sfIpAddress server, u /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_loginAnonymous(sfFtp* ftp); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_loginAnonymous(sfFtp* ftp); //////////////////////////////////////////////////////////// /// \brief Log in using a username and a password @@ -349,8 +394,10 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_loginAnonymous(sfFtp* ftp); /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_login(sfFtp* ftp, const char* name, const char* password); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_login(sfFtp* ftp, const char* name, const char* password); //////////////////////////////////////////////////////////// /// \brief Close the connection with the server @@ -359,8 +406,10 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_login(sfFtp* ftp, const char* name, const /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_disconnect(sfFtp* ftp); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_disconnect(sfFtp* ftp); //////////////////////////////////////////////////////////// /// \brief Send a null command to keep the connection alive @@ -372,8 +421,10 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_disconnect(sfFtp* ftp); /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_keepAlive(sfFtp* ftp); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_keepAlive(sfFtp* ftp); //////////////////////////////////////////////////////////// /// \brief Get the current working directory @@ -385,8 +436,10 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_keepAlive(sfFtp* ftp); /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpDirectoryResponse* sfFtp_getWorkingDirectory(sfFtp* ftp); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpDirectoryResponse* sfFtp_getWorkingDirectory(sfFtp* ftp); //////////////////////////////////////////////////////////// /// \brief Get the contents of the given directory @@ -401,8 +454,10 @@ CSFML_NETWORK_API sfFtpDirectoryResponse* sfFtp_getWorkingDirectory(sfFtp* ftp); /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpListingResponse* sfFtp_getDirectoryListing(sfFtp* ftp, const char* directory); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpListingResponse* sfFtp_getDirectoryListing(sfFtp* ftp, const char* directory); //////////////////////////////////////////////////////////// /// \brief Change the current working directory @@ -414,8 +469,10 @@ CSFML_NETWORK_API sfFtpListingResponse* sfFtp_getDirectoryListing(sfFtp* ftp, co /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_changeDirectory(sfFtp* ftp, const char* directory); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_changeDirectory(sfFtp* ftp, const char* directory); //////////////////////////////////////////////////////////// /// \brief Go to the parent directory of the current one @@ -424,8 +481,10 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_changeDirectory(sfFtp* ftp, const char* d /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_parentDirectory(sfFtp* ftp); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_parentDirectory(sfFtp* ftp); //////////////////////////////////////////////////////////// /// \brief Create a new directory @@ -438,8 +497,10 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_parentDirectory(sfFtp* ftp); /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_createDirectory(sfFtp* ftp, const char* name); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_createDirectory(sfFtp* ftp, const char* name); //////////////////////////////////////////////////////////// /// \brief Remove an existing directory @@ -454,8 +515,10 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_createDirectory(sfFtp* ftp, const char* n /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_deleteDirectory(sfFtp* ftp, const char* name); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_deleteDirectory(sfFtp* ftp, const char* name); //////////////////////////////////////////////////////////// /// \brief Rename an existing file @@ -469,8 +532,10 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_deleteDirectory(sfFtp* ftp, const char* n /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_renameFile(sfFtp* ftp, const char* file, const char* newName); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_renameFile(sfFtp* ftp, const char* file, const char* newName); //////////////////////////////////////////////////////////// /// \brief Remove an existing file @@ -485,8 +550,10 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_renameFile(sfFtp* ftp, const char* file, /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_deleteFile(sfFtp* ftp, const char* name); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_deleteFile(sfFtp* ftp, const char* name); //////////////////////////////////////////////////////////// /// \brief Download a file from a FTP server @@ -503,8 +570,14 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_deleteFile(sfFtp* ftp, const char* name); /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_download(sfFtp* ftp, const char* remoteFile, const char* localPath, sfFtpTransferMode mode); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_download( + sfFtp* ftp, + const char* remoteFile, + const char* localPath, + sfFtpTransferMode mode); //////////////////////////////////////////////////////////// /// \brief Upload a file to a FTP server @@ -522,8 +595,10 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_download(sfFtp* ftp, const char* remoteFi /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_upload( +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_upload( sfFtp* ftp, const char* localFile, const char* remotePath, @@ -547,5 +622,7 @@ CSFML_NETWORK_API sfFtpResponse* sfFtp_upload( /// /// \return Server response to the request /// +/// \deprecated Use sfSftp if possible +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfFtpResponse* sfFtp_sendCommand(sfFtp* ftp, const char* command, const char* parameter); +CSFML_NETWORK_API CSFML_DEPRECATED sfFtpResponse* sfFtp_sendCommand(sfFtp* ftp, const char* command, const char* parameter); diff --git a/include/CSFML/Window/Touch.h b/include/CSFML/Window/Touch.h index 948fa2ac..e2d3e27b 100644 --- a/include/CSFML/Window/Touch.h +++ b/include/CSFML/Window/Touch.h @@ -40,8 +40,10 @@ /// /// \return true if \a finger is currently touching the screen, false otherwise /// +/// \deprecated Use the touch events (sfEvtTouchBegan, sfEvtTouchMoved and sfEvtTouchEnded) instead +/// //////////////////////////////////////////////////////////// -CSFML_WINDOW_API bool sfTouch_isDown(unsigned int finger); +CSFML_WINDOW_API CSFML_DEPRECATED bool sfTouch_isDown(unsigned int finger); //////////////////////////////////////////////////////////// /// \brief Get the current position of a touch in window coordinates @@ -54,8 +56,10 @@ CSFML_WINDOW_API bool sfTouch_isDown(unsigned int finger); /// /// \return Current position of \a finger, or undefined if it's not down /// +/// \deprecated Use the touch events (sfEvtTouchBegan, sfEvtTouchMoved and sfEvtTouchEnded) instead +/// //////////////////////////////////////////////////////////// -CSFML_WINDOW_API sfVector2i sfTouch_getPosition(unsigned int finger, const sfWindow* relativeTo); +CSFML_WINDOW_API CSFML_DEPRECATED sfVector2i sfTouch_getPosition(unsigned int finger, const sfWindow* relativeTo); //////////////////////////////////////////////////////////// /// \brief Get the current position of a touch in window coordinates @@ -68,5 +72,7 @@ CSFML_WINDOW_API sfVector2i sfTouch_getPosition(unsigned int finger, const sfWin /// /// \return Current position of \a finger, or undefined if it's not down /// +/// \deprecated Use the touch events (sfEvtTouchBegan, sfEvtTouchMoved and sfEvtTouchEnded) instead +/// //////////////////////////////////////////////////////////// -CSFML_WINDOW_API sfVector2i sfTouch_getPositionWindowBase(unsigned int finger, const sfWindowBase* relativeTo); +CSFML_WINDOW_API CSFML_DEPRECATED sfVector2i sfTouch_getPositionWindowBase(unsigned int finger, const sfWindowBase* relativeTo); diff --git a/src/CSFML/Graphics/Font.cpp b/src/CSFML/Graphics/Font.cpp index 46529bff..63991b1d 100644 --- a/src/CSFML/Graphics/Font.cpp +++ b/src/CSFML/Graphics/Font.cpp @@ -111,7 +111,7 @@ bool sfFont_hasGlyph(const sfFont* font, uint32_t codePoint) float sfFont_getKerning(const sfFont* font, uint32_t first, uint32_t second, unsigned int characterSize) { assert(font); - return font->getKerning(first, second, characterSize); + return font->getKerning(char32_t{first}, char32_t{second}, characterSize); } @@ -119,7 +119,7 @@ float sfFont_getKerning(const sfFont* font, uint32_t first, uint32_t second, uns float sfFont_getBoldKerning(const sfFont* font, uint32_t first, uint32_t second, unsigned int characterSize) { assert(font); - return font->getKerning(first, second, characterSize, true); + return font->getKerning(char32_t{first}, char32_t{second}, characterSize, true); } From a08585812ddfb3dbbc65637d867ad24346396b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:19:42 +0200 Subject: [PATCH 03/15] Add sfGetVersion to retrieve the runtime SFML version --- include/CSFML/System.h | 1 + include/CSFML/System/Version.h | 64 +++++++++++++++++++++++++++++++++ src/CSFML/System/CMakeLists.txt | 2 ++ src/CSFML/System/Version.cpp | 41 +++++++++++++++++++++ test/CMakeLists.txt | 1 + test/System/Version.test.cpp | 15 ++++++++ 6 files changed, 124 insertions(+) create mode 100644 include/CSFML/System/Version.h create mode 100644 src/CSFML/System/Version.cpp create mode 100644 test/System/Version.test.cpp diff --git a/include/CSFML/System.h b/include/CSFML/System.h index 83a73322..7050c01e 100644 --- a/include/CSFML/System.h +++ b/include/CSFML/System.h @@ -37,3 +37,4 @@ #include #include #include +#include diff --git a/include/CSFML/System/Version.h b/include/CSFML/System/Version.h new file mode 100644 index 00000000..b3302ecc --- /dev/null +++ b/include/CSFML/System/Version.h @@ -0,0 +1,64 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include +#include + + +//////////////////////////////////////////////////////////// +/// \brief Version information of the SFML library +/// +//////////////////////////////////////////////////////////// +typedef struct +{ + int8_t major; ///< SFML major version + int8_t minor; ///< SFML minor version + int8_t patch; ///< SFML patch version + bool isRelease; ///< true if this is a release version, false if this is a development version + const char* string; ///< String representation of the SFML version, e.g. 3.1.0 or 3.1.0-dev +} sfVersion; + + +//////////////////////////////////////////////////////////// +/// \brief Get the version of the SFML library loaded at runtime +/// +/// This is the version of the SFML library CSFML is linked +/// against at runtime, which can differ from the version +/// CSFML was compiled with (see CSFML_VERSION_MAJOR, +/// CSFML_VERSION_MINOR and CSFML_VERSION_PATCH). +/// +/// The returned string is owned by SFML and stays valid +/// for the whole lifetime of the program. +/// +/// \return Version information of the SFML library +/// +//////////////////////////////////////////////////////////// +CSFML_SYSTEM_API sfVersion sfGetVersion(void); diff --git a/src/CSFML/System/CMakeLists.txt b/src/CSFML/System/CMakeLists.txt index a973cd13..0178d6c6 100644 --- a/src/CSFML/System/CMakeLists.txt +++ b/src/CSFML/System/CMakeLists.txt @@ -23,6 +23,8 @@ set(SRC ${INCROOT}/Types.h ${INCROOT}/Vector2.h ${INCROOT}/Vector3.h + ${SRCROOT}/Version.cpp + ${INCROOT}/Version.h ) # define the csfml-system target diff --git a/src/CSFML/System/Version.cpp b/src/CSFML/System/Version.cpp new file mode 100644 index 00000000..0d80f83a --- /dev/null +++ b/src/CSFML/System/Version.cpp @@ -0,0 +1,41 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include + +#include + + +//////////////////////////////////////////////////////////// +sfVersion sfGetVersion() +{ + const auto& version = sf::version(); + static const std::string string(version.string); + return {version.major, version.minor, version.patch, version.isRelease, string.c_str()}; +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f32f9884..dd6cdaad 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,6 +27,7 @@ add_executable(test-csfml-system System/Time.test.cpp System/Vector2.test.cpp System/Vector3.test.cpp + System/Version.test.cpp ) target_link_libraries(test-csfml-system PRIVATE csfml-system Catch2::Catch2WithMain) set_target_warnings(test-csfml-system) diff --git a/test/System/Version.test.cpp b/test/System/Version.test.cpp new file mode 100644 index 00000000..028b8ab9 --- /dev/null +++ b/test/System/Version.test.cpp @@ -0,0 +1,15 @@ +#include + +#include + +#include + +TEST_CASE("[System] sfGetVersion") +{ + const sfVersion version = sfGetVersion(); + CHECK(version.major == 3); + CHECK(version.minor >= 1); + CHECK(version.patch >= 0); + REQUIRE(version.string != nullptr); + CHECK(std::string(version.string).rfind(std::to_string(version.major) + "." + std::to_string(version.minor), 0) == 0); +} From a4c9ecec5761dfd3f1e208b17244734613fb47ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:19:47 +0200 Subject: [PATCH 04/15] Add miter limit functions to all shapes --- include/CSFML/Graphics/CircleShape.h | 40 +++++++++++++++++++++++++ include/CSFML/Graphics/ConvexShape.h | 40 +++++++++++++++++++++++++ include/CSFML/Graphics/RectangleShape.h | 40 +++++++++++++++++++++++++ include/CSFML/Graphics/Shape.h | 40 +++++++++++++++++++++++++ src/CSFML/Graphics/CircleShape.cpp | 16 ++++++++++ src/CSFML/Graphics/ConvexShape.cpp | 16 ++++++++++ src/CSFML/Graphics/RectangleShape.cpp | 16 ++++++++++ src/CSFML/Graphics/Shape.cpp | 16 ++++++++++ test/Graphics/Shape.test.cpp | 9 ++++++ 9 files changed, 233 insertions(+) diff --git a/include/CSFML/Graphics/CircleShape.h b/include/CSFML/Graphics/CircleShape.h index 1d946ef9..6cc695ed 100644 --- a/include/CSFML/Graphics/CircleShape.h +++ b/include/CSFML/Graphics/CircleShape.h @@ -292,6 +292,36 @@ CSFML_GRAPHICS_API void sfCircleShape_setOutlineColor(sfCircleShape* shape, sfCo //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API void sfCircleShape_setOutlineThickness(sfCircleShape* shape, float thickness); +//////////////////////////////////////////////////////////// +/// \brief Set the limit on the ratio between miter length and outline thickness of a circle shape +/// +/// Outline segments around each corner are joined either +/// with a miter or a bevel join. +/// - A miter join is formed by extending outline segments until +/// they intersect. The distance between the point of +/// intersection and the shape's corner is the miter length. +/// - A bevel join is formed by connecting outline segments with +/// a straight line perpendicular to the corner's bisector. +/// +/// The miter limit is used to determine whether outline segments +/// around a corner are joined with a bevel or a miter. +/// When the ratio between the miter length and outline thickness +/// exceeds the miter limit, a bevel is used instead of a miter. +/// +/// The miter limit is linked to the maximum inner angle of a +/// corner below which a bevel is used by the following formula: +/// +/// miterLimit = 1 / sin(angle / 2) +/// +/// The miter limit must be greater than or equal to 1. +/// By default, the miter limit is 10. +/// +/// \param shape Shape object +/// \param miterLimit New miter limit +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfCircleShape_setMiterLimit(sfCircleShape* shape, float miterLimit); + //////////////////////////////////////////////////////////// /// \brief Get the source texture of a circle shape /// @@ -346,6 +376,16 @@ CSFML_GRAPHICS_API sfColor sfCircleShape_getOutlineColor(const sfCircleShape* sh //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API float sfCircleShape_getOutlineThickness(const sfCircleShape* shape); +//////////////////////////////////////////////////////////// +/// \brief Get the limit on the ratio between miter length and outline thickness of a circle shape +/// +/// \param shape Shape object +/// +/// \return Limit on the ratio between miter length and outline thickness +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API float sfCircleShape_getMiterLimit(const sfCircleShape* shape); + //////////////////////////////////////////////////////////// /// \brief Get the total number of points of a circle shape /// diff --git a/include/CSFML/Graphics/ConvexShape.h b/include/CSFML/Graphics/ConvexShape.h index aca5c398..d8f1d78c 100644 --- a/include/CSFML/Graphics/ConvexShape.h +++ b/include/CSFML/Graphics/ConvexShape.h @@ -292,6 +292,36 @@ CSFML_GRAPHICS_API void sfConvexShape_setOutlineColor(sfConvexShape* shape, sfCo //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API void sfConvexShape_setOutlineThickness(sfConvexShape* shape, float thickness); +//////////////////////////////////////////////////////////// +/// \brief Set the limit on the ratio between miter length and outline thickness of a convex shape +/// +/// Outline segments around each corner are joined either +/// with a miter or a bevel join. +/// - A miter join is formed by extending outline segments until +/// they intersect. The distance between the point of +/// intersection and the shape's corner is the miter length. +/// - A bevel join is formed by connecting outline segments with +/// a straight line perpendicular to the corner's bisector. +/// +/// The miter limit is used to determine whether outline segments +/// around a corner are joined with a bevel or a miter. +/// When the ratio between the miter length and outline thickness +/// exceeds the miter limit, a bevel is used instead of a miter. +/// +/// The miter limit is linked to the maximum inner angle of a +/// corner below which a bevel is used by the following formula: +/// +/// miterLimit = 1 / sin(angle / 2) +/// +/// The miter limit must be greater than or equal to 1. +/// By default, the miter limit is 10. +/// +/// \param shape Shape object +/// \param miterLimit New miter limit +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfConvexShape_setMiterLimit(sfConvexShape* shape, float miterLimit); + //////////////////////////////////////////////////////////// /// \brief Get the source texture of a convex shape /// @@ -346,6 +376,16 @@ CSFML_GRAPHICS_API sfColor sfConvexShape_getOutlineColor(const sfConvexShape* sh //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API float sfConvexShape_getOutlineThickness(const sfConvexShape* shape); +//////////////////////////////////////////////////////////// +/// \brief Get the limit on the ratio between miter length and outline thickness of a convex shape +/// +/// \param shape Shape object +/// +/// \return Limit on the ratio between miter length and outline thickness +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API float sfConvexShape_getMiterLimit(const sfConvexShape* shape); + //////////////////////////////////////////////////////////// /// \brief Get the total number of points of a convex shape /// diff --git a/include/CSFML/Graphics/RectangleShape.h b/include/CSFML/Graphics/RectangleShape.h index 2834a5f9..784ec87f 100644 --- a/include/CSFML/Graphics/RectangleShape.h +++ b/include/CSFML/Graphics/RectangleShape.h @@ -292,6 +292,36 @@ CSFML_GRAPHICS_API void sfRectangleShape_setOutlineColor(sfRectangleShape* shape //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API void sfRectangleShape_setOutlineThickness(sfRectangleShape* shape, float thickness); +//////////////////////////////////////////////////////////// +/// \brief Set the limit on the ratio between miter length and outline thickness of a rectangle shape +/// +/// Outline segments around each corner are joined either +/// with a miter or a bevel join. +/// - A miter join is formed by extending outline segments until +/// they intersect. The distance between the point of +/// intersection and the shape's corner is the miter length. +/// - A bevel join is formed by connecting outline segments with +/// a straight line perpendicular to the corner's bisector. +/// +/// The miter limit is used to determine whether outline segments +/// around a corner are joined with a bevel or a miter. +/// When the ratio between the miter length and outline thickness +/// exceeds the miter limit, a bevel is used instead of a miter. +/// +/// The miter limit is linked to the maximum inner angle of a +/// corner below which a bevel is used by the following formula: +/// +/// miterLimit = 1 / sin(angle / 2) +/// +/// The miter limit must be greater than or equal to 1. +/// By default, the miter limit is 10. +/// +/// \param shape Shape object +/// \param miterLimit New miter limit +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfRectangleShape_setMiterLimit(sfRectangleShape* shape, float miterLimit); + //////////////////////////////////////////////////////////// /// \brief Get the source texture of a rectangle shape /// @@ -346,6 +376,16 @@ CSFML_GRAPHICS_API sfColor sfRectangleShape_getOutlineColor(const sfRectangleSha //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API float sfRectangleShape_getOutlineThickness(const sfRectangleShape* shape); +//////////////////////////////////////////////////////////// +/// \brief Get the limit on the ratio between miter length and outline thickness of a rectangle shape +/// +/// \param shape Shape object +/// +/// \return Limit on the ratio between miter length and outline thickness +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API float sfRectangleShape_getMiterLimit(const sfRectangleShape* shape); + //////////////////////////////////////////////////////////// /// \brief Get the total number of points of a rectangle shape /// diff --git a/include/CSFML/Graphics/Shape.h b/include/CSFML/Graphics/Shape.h index d844ca63..be09ea46 100644 --- a/include/CSFML/Graphics/Shape.h +++ b/include/CSFML/Graphics/Shape.h @@ -291,6 +291,36 @@ CSFML_GRAPHICS_API void sfShape_setOutlineColor(sfShape* shape, sfColor color); //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API void sfShape_setOutlineThickness(sfShape* shape, float thickness); +//////////////////////////////////////////////////////////// +/// \brief Set the limit on the ratio between miter length and outline thickness of a shape +/// +/// Outline segments around each corner are joined either +/// with a miter or a bevel join. +/// - A miter join is formed by extending outline segments until +/// they intersect. The distance between the point of +/// intersection and the shape's corner is the miter length. +/// - A bevel join is formed by connecting outline segments with +/// a straight line perpendicular to the corner's bisector. +/// +/// The miter limit is used to determine whether outline segments +/// around a corner are joined with a bevel or a miter. +/// When the ratio between the miter length and outline thickness +/// exceeds the miter limit, a bevel is used instead of a miter. +/// +/// The miter limit is linked to the maximum inner angle of a +/// corner below which a bevel is used by the following formula: +/// +/// miterLimit = 1 / sin(angle / 2) +/// +/// The miter limit must be greater than or equal to 1. +/// By default, the miter limit is 10. +/// +/// \param shape Shape object +/// \param miterLimit New miter limit +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfShape_setMiterLimit(sfShape* shape, float miterLimit); + //////////////////////////////////////////////////////////// /// \brief Get the source texture of a shape /// @@ -345,6 +375,16 @@ CSFML_GRAPHICS_API sfColor sfShape_getOutlineColor(const sfShape* shape); //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API float sfShape_getOutlineThickness(const sfShape* shape); +//////////////////////////////////////////////////////////// +/// \brief Get the limit on the ratio between miter length and outline thickness of a shape +/// +/// \param shape Shape object +/// +/// \return Limit on the ratio between miter length and outline thickness +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API float sfShape_getMiterLimit(const sfShape* shape); + //////////////////////////////////////////////////////////// /// \brief Get the total number of points of a shape /// diff --git a/src/CSFML/Graphics/CircleShape.cpp b/src/CSFML/Graphics/CircleShape.cpp index 240702d2..701ea9a9 100644 --- a/src/CSFML/Graphics/CircleShape.cpp +++ b/src/CSFML/Graphics/CircleShape.cpp @@ -207,6 +207,14 @@ void sfCircleShape_setOutlineThickness(sfCircleShape* shape, float thickness) } +//////////////////////////////////////////////////////////// +void sfCircleShape_setMiterLimit(sfCircleShape* shape, float miterLimit) +{ + assert(shape); + shape->setMiterLimit(miterLimit); +} + + //////////////////////////////////////////////////////////// const sfTexture* sfCircleShape_getTexture(const sfCircleShape* shape) { @@ -247,6 +255,14 @@ float sfCircleShape_getOutlineThickness(const sfCircleShape* shape) } +//////////////////////////////////////////////////////////// +float sfCircleShape_getMiterLimit(const sfCircleShape* shape) +{ + assert(shape); + return shape->getMiterLimit(); +} + + //////////////////////////////////////////////////////////// size_t sfCircleShape_getPointCount(const sfCircleShape* shape) { diff --git a/src/CSFML/Graphics/ConvexShape.cpp b/src/CSFML/Graphics/ConvexShape.cpp index 58093715..a232a77f 100644 --- a/src/CSFML/Graphics/ConvexShape.cpp +++ b/src/CSFML/Graphics/ConvexShape.cpp @@ -204,6 +204,14 @@ void sfConvexShape_setOutlineThickness(sfConvexShape* shape, float thickness) } +//////////////////////////////////////////////////////////// +void sfConvexShape_setMiterLimit(sfConvexShape* shape, float miterLimit) +{ + assert(shape); + shape->setMiterLimit(miterLimit); +} + + //////////////////////////////////////////////////////////// const sfTexture* sfConvexShape_getTexture(const sfConvexShape* shape) { @@ -244,6 +252,14 @@ float sfConvexShape_getOutlineThickness(const sfConvexShape* shape) } +//////////////////////////////////////////////////////////// +float sfConvexShape_getMiterLimit(const sfConvexShape* shape) +{ + assert(shape); + return shape->getMiterLimit(); +} + + //////////////////////////////////////////////////////////// size_t sfConvexShape_getPointCount(const sfConvexShape* shape) { diff --git a/src/CSFML/Graphics/RectangleShape.cpp b/src/CSFML/Graphics/RectangleShape.cpp index 9563e7c6..2ba840a3 100644 --- a/src/CSFML/Graphics/RectangleShape.cpp +++ b/src/CSFML/Graphics/RectangleShape.cpp @@ -204,6 +204,14 @@ void sfRectangleShape_setOutlineThickness(sfRectangleShape* shape, float thickne } +//////////////////////////////////////////////////////////// +void sfRectangleShape_setMiterLimit(sfRectangleShape* shape, float miterLimit) +{ + assert(shape); + shape->setMiterLimit(miterLimit); +} + + //////////////////////////////////////////////////////////// const sfTexture* sfRectangleShape_getTexture(const sfRectangleShape* shape) { @@ -244,6 +252,14 @@ float sfRectangleShape_getOutlineThickness(const sfRectangleShape* shape) } +//////////////////////////////////////////////////////////// +float sfRectangleShape_getMiterLimit(const sfRectangleShape* shape) +{ + assert(shape); + return shape->getMiterLimit(); +} + + //////////////////////////////////////////////////////////// size_t sfRectangleShape_getPointCount(const sfRectangleShape* shape) { diff --git a/src/CSFML/Graphics/Shape.cpp b/src/CSFML/Graphics/Shape.cpp index 243f2f25..c14df101 100644 --- a/src/CSFML/Graphics/Shape.cpp +++ b/src/CSFML/Graphics/Shape.cpp @@ -196,6 +196,14 @@ void sfShape_setOutlineThickness(sfShape* shape, float thickness) } +//////////////////////////////////////////////////////////// +void sfShape_setMiterLimit(sfShape* shape, float miterLimit) +{ + assert(shape); + shape->setMiterLimit(miterLimit); +} + + //////////////////////////////////////////////////////////// const sfTexture* sfShape_getTexture(const sfShape* shape) { @@ -236,6 +244,14 @@ float sfShape_getOutlineThickness(const sfShape* shape) } +//////////////////////////////////////////////////////////// +float sfShape_getMiterLimit(const sfShape* shape) +{ + assert(shape); + return shape->getMiterLimit(); +} + + //////////////////////////////////////////////////////////// size_t sfShape_getPointCount(const sfShape* shape) { diff --git a/test/Graphics/Shape.test.cpp b/test/Graphics/Shape.test.cpp index e43ee421..8d20c6f5 100644 --- a/test/Graphics/Shape.test.cpp +++ b/test/Graphics/Shape.test.cpp @@ -47,6 +47,7 @@ TEST_CASE("[Graphics] sfShape") CHECK(outlineColor.b == sfWhite.b); CHECK(outlineColor.a == sfWhite.a); CHECK(sfShape_getOutlineThickness(shape) == 0); + CHECK(sfShape_getMiterLimit(shape) == 10); CHECK(sfShape_getPointCount(shape) == 3); const sfVector2f point = sfShape_getPoint(shape, 0); CHECK(point.x == 0); @@ -104,4 +105,12 @@ TEST_CASE("[Graphics] sfShape") CHECK(origin.y == 90); sfShape_destroy(shape); } + + SECTION("Set/get miter limit") + { + sfShape* shape = sfShape_create(getPointCount, getPoint, &points); + sfShape_setMiterLimit(shape, 2); + CHECK(sfShape_getMiterLimit(shape) == 2); + sfShape_destroy(shape); + } } From 6d7cc360a10ba66896c7454406e9012d9c610ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:28:07 +0200 Subject: [PATCH 05/15] Add font ascent, descent, glyph by ID and new font info --- include/CSFML/Graphics/Font.h | 63 ++++++++++++++++++++++++++++ include/CSFML/Graphics/FontInfo.h | 8 +++- src/CSFML/Graphics/CMakeLists.txt | 1 + src/CSFML/Graphics/ConvertGlyph.hpp | 42 +++++++++++++++++++ src/CSFML/Graphics/Font.cpp | 34 +++++++++++---- test/CMakeLists.txt | 1 + test/Graphics/Font.test.cpp | 27 ++++++++++++ test/Graphics/tuffy.ttf | Bin 0 -> 18444 bytes 8 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 src/CSFML/Graphics/ConvertGlyph.hpp create mode 100644 test/Graphics/Font.test.cpp create mode 100644 test/Graphics/tuffy.ttf diff --git a/include/CSFML/Graphics/Font.h b/include/CSFML/Graphics/Font.h index 48164f39..e80c53bd 100644 --- a/include/CSFML/Graphics/Font.h +++ b/include/CSFML/Graphics/Font.h @@ -101,6 +101,31 @@ CSFML_GRAPHICS_API void sfFont_destroy(const sfFont* font); CSFML_GRAPHICS_API sfGlyph sfFont_getGlyph(const sfFont* font, uint32_t codePoint, unsigned int characterSize, bool bold, float outlineThickness); +//////////////////////////////////////////////////////////// +/// \brief Get a glyph in a font by glyph ID +/// +/// If the font is a bitmap font, not all character sizes +/// might be available. If the glyph is not available at the +/// requested size, an empty glyph is returned. +/// +/// Glyph IDs are font specific and are for example +/// provided by the shaped glyphs of a text. +/// +/// Be aware that using a negative value for the outline +/// thickness will cause distorted rendering. +/// +/// \param font Source font +/// \param id ID of the glyph to get +/// \param characterSize Character size, in pixels +/// \param bold Retrieve the bold version or the regular one? +/// \param outlineThickness Thickness of outline (when != 0 the glyph will not be filled) +/// +/// \return The corresponding glyph +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API sfGlyph + sfFont_getGlyphById(const sfFont* font, uint32_t id, unsigned int characterSize, bool bold, float outlineThickness); + //////////////////////////////////////////////////////////// /// \brief Determine if this font has a glyph representing the requested code point /// @@ -157,6 +182,44 @@ CSFML_GRAPHICS_API float sfFont_getBoldKerning(const sfFont* font, uint32_t firs //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API float sfFont_getLineSpacing(const sfFont* font, unsigned int characterSize); +//////////////////////////////////////////////////////////// +/// \brief Get the ascent of a font +/// +/// The ascent is the largest distance between the baseline and +/// the top of all glyphs in the font. +/// +/// Be aware that there is no uniform definition of how the +/// ascent is calculated. It can vary from font to font. +/// +/// \param font Source font +/// \param characterSize Reference character size +/// +/// \return Ascent, in pixels +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API float sfFont_getAscent(const sfFont* font, unsigned int characterSize); + +//////////////////////////////////////////////////////////// +/// \brief Get the descent of a font +/// +/// The descent is the largest distance between the baseline and +/// the bottom of all glyphs in the font. +/// +/// Be aware that there is no uniform definition of how the +/// descent is calculated. It can vary from font to font. +/// +/// The descent shares the same coordinate system as the +/// ascent. This means that it will be negative for distances +/// below the baseline. +/// +/// \param font Source font +/// \param characterSize Reference character size +/// +/// \return Descent, in pixels +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API float sfFont_getDescent(const sfFont* font, unsigned int characterSize); + //////////////////////////////////////////////////////////// /// \brief Get the position of the underline /// diff --git a/include/CSFML/Graphics/FontInfo.h b/include/CSFML/Graphics/FontInfo.h index 6037e142..38067184 100644 --- a/include/CSFML/Graphics/FontInfo.h +++ b/include/CSFML/Graphics/FontInfo.h @@ -29,10 +29,16 @@ //////////////////////////////////////////////////////////// #include +#include +#include + //////////////////////////////////////////////////////////// /// sfFontInfo holds various information about a font //////////////////////////////////////////////////////////// typedef struct { - const char* family; + uint64_t id; ///< A unique ID that identifies the font + const char* family; ///< The font family + bool hasKerning; ///< Has kerning information + bool hasVerticalMetrics; ///< Has native vertical metrics } sfFontInfo; diff --git a/src/CSFML/Graphics/CMakeLists.txt b/src/CSFML/Graphics/CMakeLists.txt index e3e056ec..a1536a44 100644 --- a/src/CSFML/Graphics/CMakeLists.txt +++ b/src/CSFML/Graphics/CMakeLists.txt @@ -12,6 +12,7 @@ set(SRC ${SRCROOT}/Color.cpp ${INCROOT}/Color.h ${SRCROOT}/ConvertColor.hpp + ${SRCROOT}/ConvertGlyph.hpp ${SRCROOT}/ConvertRect.hpp ${SRCROOT}/ConvertRenderStates.hpp ${SRCROOT}/ConvertStencil.hpp diff --git a/src/CSFML/Graphics/ConvertGlyph.hpp b/src/CSFML/Graphics/ConvertGlyph.hpp new file mode 100644 index 00000000..ef52405b --- /dev/null +++ b/src/CSFML/Graphics/ConvertGlyph.hpp @@ -0,0 +1,42 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include +#include + +#include + + +//////////////////////////////////////////////////////////// +// Convert sf::Glyph to sfGlyph +//////////////////////////////////////////////////////////// +[[nodiscard]] inline sfGlyph convertGlyph(const sf::Glyph& glyph) +{ + return {glyph.advance, convertRect(glyph.bounds), convertRect(glyph.textureRect)}; +} diff --git a/src/CSFML/Graphics/Font.cpp b/src/CSFML/Graphics/Font.cpp index 63991b1d..a854015e 100644 --- a/src/CSFML/Graphics/Font.cpp +++ b/src/CSFML/Graphics/Font.cpp @@ -26,7 +26,7 @@ // Headers //////////////////////////////////////////////////////////// #include -#include +#include #include #include @@ -88,14 +88,15 @@ sfGlyph sfFont_getGlyph(const sfFont* font, uint32_t codePoint, unsigned int cha { assert(font); - sf::Glyph sfmlGlyph = font->getGlyph(codePoint, characterSize, bold, outlineThickness); + return convertGlyph(font->getGlyph(codePoint, characterSize, bold, outlineThickness)); +} - sfGlyph glyph{}; - glyph.advance = sfmlGlyph.advance; - glyph.bounds = convertRect(sfmlGlyph.bounds); - glyph.textureRect = convertRect(sfmlGlyph.textureRect); - return glyph; +//////////////////////////////////////////////////////////// +sfGlyph sfFont_getGlyphById(const sfFont* font, uint32_t id, unsigned int characterSize, bool bold, float outlineThickness) +{ + assert(font); + return convertGlyph(font->getGlyphById(id, characterSize, bold, outlineThickness)); } @@ -131,6 +132,22 @@ float sfFont_getLineSpacing(const sfFont* font, unsigned int characterSize) } +//////////////////////////////////////////////////////////// +float sfFont_getAscent(const sfFont* font, unsigned int characterSize) +{ + assert(font); + return font->getAscent(characterSize); +} + + +//////////////////////////////////////////////////////////// +float sfFont_getDescent(const sfFont* font, unsigned int characterSize) +{ + assert(font); + return font->getDescent(characterSize); +} + + //////////////////////////////////////////////////////////// float sfFont_getUnderlinePosition(const sfFont* font, unsigned int characterSize) { @@ -178,5 +195,6 @@ bool sfFont_isSmooth(const sfFont* font) sfFontInfo sfFont_getInfo(const sfFont* font) { assert(font); - return {font->getInfo().family.c_str()}; + const sf::Font::Info& info = font->getInfo(); + return {info.id, info.family.c_str(), info.hasKerning, info.hasVerticalMetrics}; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index dd6cdaad..24e28902 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -49,6 +49,7 @@ add_executable(test-csfml-graphics Graphics/BlendMode.test.cpp Graphics/Color.test.cpp Graphics/CoordinateType.test.cpp + Graphics/Font.test.cpp Graphics/Image.test.cpp Graphics/PrimitiveType.test.cpp Graphics/Rect.test.cpp diff --git a/test/Graphics/Font.test.cpp b/test/Graphics/Font.test.cpp new file mode 100644 index 00000000..3bf2855a --- /dev/null +++ b/test/Graphics/Font.test.cpp @@ -0,0 +1,27 @@ +#include + +#include + +#include + +TEST_CASE("[Graphics] sfFont") +{ + SECTION("sfFont_createFromFile") + { + CHECK(sfFont_createFromFile("does/not/exist.ttf") == nullptr); + + sfFont* font = sfFont_createFromFile("Graphics/tuffy.ttf"); + REQUIRE(font != nullptr); + + const sfFontInfo info = sfFont_getInfo(font); + CHECK(info.id != 0); + CHECK(std::string(info.family) == "Tuffy"); + CHECK(!info.hasVerticalMetrics); + + CHECK(sfFont_getAscent(font, 10) > 0); + CHECK(sfFont_getDescent(font, 10) < 0); + CHECK(sfFont_getAscent(font, 20) > sfFont_getAscent(font, 10)); + + sfFont_destroy(font); + } +} diff --git a/test/Graphics/tuffy.ttf b/test/Graphics/tuffy.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8ea647090f75e7d34bee4c511921299d1f3c1055 GIT binary patch literal 18444 zcmdUXd0bOh+VDB&-Xts`ke#q51QK?E5W>ERfQSl+h=|C(DElJfjvFe)UBnHoT5GLi zZL8L?*3q_BtyQ}@*6G;I>Eg80nd!70$C8`xIX7sn?Y!^z`+a|X(R=T?&)J{z?B`q% zMhLmVjYTLkvvA_L{ClMcW&H!7URfFAGEoq^0y%l`?vs@_dCI3J{=OaFHz0(|vZhQ; zZ`|0q9^P9aUpslqn5d%;SLQ(8YIrZIYN}{`BV^xK(D?TVk@Tv$om!+LJqU?nA)l?Q z=xDXs5CiqI;oYIGv8Q(3yQg6=4wn#$e!sq^qWY`nUQdVi#E>6T4*-{I>sG;g2E6;% zH+9bIem!R`ynliaqiAfYs`$f=s|C=0BEX(#s+iY`W#|l~>!G~1xuU6NO!eD$5lY&H zknl=tOGjr-_A4JFlmYqpHlnz|n(dCl=$B@>CjNk&9RPsP`5(r-``~F?!=(tlMVH~- z0TBv?AJh|F=2Fm1*IwJ2rAX+_w`9+=-!Jfx3#bwaP$A%&*2Fw46;cFnfSFnNd&s3f z@&~~8jze2NIOl+Hg!{R@0CE;GLOMRQ6s8J~2YsWw+HuwJuGJn9CUHr0B=|hM(JKg@SF?ReAFYD0`Esr4?cr-u$|EMO@Qa49-#+3{|)(ORLV|( zX9+wf0?dNK*bkxqe?s~;$`b~nLiQ}mfwpr6VW^t4qvcG9?GAx?F?-NTm}47%Ljj2BKbk6fMW^+tx70`1GK_3UdQE>w{`(J=PtKCrA1E z<>VB=0?|Acp#nh}%3xL^Gks@XMJ42GRDxU4B6zMOAEOj(MqW%H%45p`C&eg*EkZds z63QJ#A&dvgWsagCrXD4*1npo>0)C%DW#m2nHtxIqX=K3#G#Hvu^qZdA6I{|b!$jn0>WG8Z9g{S~< z+t2!=R@U&y1@Fvy)D8G8Vja*j!0j(Czz1EMWo!yM%-ltX1x{!X_o69q&4Q~6uG!=% zln+-0IfmNcn!%=_K41vSLG<$Cc@W`5%7Oeli?#4nAUA-pXbE}-J&#VImyu1Y(E4hR zfV=`;LrosjeL+V^r}OCNRRwc05TvW3Iy34j6zT- z3Pa&&42nRJC<>WSG%}+YWI?eg4#lGcl!%g0GD<>_b>lgi`@rAqCt@$mHW_VLx~{Pg|-24i4Qa7bua_?U>uC{wgK#u6JB zpOBc8oRXTBJ{B}jR`&Ru3Aqy|P0q_Nm{K^kXj<{~k{P8l%Vw2Vphb(9E?c{P>yGU^ zckdeByYI>U2M#{<^r6E?p8ef(M_+v5_zBcnRb7udhni=j_6D?MBWgpawqtaBXwUQW zDjVs`oNk1A7Oq3+^vmbYpLyjNEHrwZFV%JSRkUFF{1v^c`c|)eVgU7T-m(cYTHufIw^Wr^qGirP&kx1tabRrQQ}zc*y%Xz z_=4lxj{k6SaPo1Aa7uM5aH@7{bsBW~qqEpK)p@S-QRffD{^DG5v-p7cocIG5g-g6k ztII)`Ph2=jj-*zyPI5rh^7&+@P2-z$(ptZ-NOD{>XZifToxVxeNSVvAz0 z;)vpu;v2<1C98B(dMN{y(aLn?kn(`?nDUJBlJb2OQq`*3Rf|JiI*oJ!S(H*4h4r(*;+6o4kQS6+(SDHZ##NrYM!t{!puw0QEB% z4OpXBYy5%|jDblXriL2h9Y_kjGx(Io59>6{38TdlaR2XNDUtpTuAwOr0Z!0$H5J}lr+q|}?k8KcFARZGJeb99VFE|p-V!iT7($=Doiz!!So z-n2;HJt4lOCO7Q8<4f`v*XD-xd1@2Plf$MgoD=u{ZZfmeCvn{KjXmu`yx8qdURsS4 zr*`*G7~eDbz-RK_UQuvfX;xlvc!s}LKtdET(^eGieAUr<3||m;mBi&o`(EO(?1|Zh zO93|mRFCQf-hvE}qP`#rQ$WfA1*mC=B~nL@S!g*;pdKX%ee6$a8tox#i#`8-G_QE zP){EtGe;AnNhMP%2zENQWX+b&W3yNF6uyj~x%f7IEhr; zMETQbFh{3gksmx_OltaO0m7CMqE4F}~0P$d?Zz5hy;A?FTI;B!+5cmDy6%p)r zZ?!m3t_)VRG%13W@<4Ilzx{$EyuM#0Hb5br0}+1W<}OwRDizly`XF79q>GAfnVBmmS)t}2v)DzUp8&@BcVn=t7N2UY-*KVV*9=trx`dC-P@KmX$UaYe}T7~j-ADlp3)6_E(r zz_(%wg$}e$6Yx*pFu6Jmuka&0U2A}@WmJC3qh*w0Wfa>4uuY5xi=82pfjoGQm5}Q6 zo>MJP-3j+@`t#;PUxs@Pd-!c?%3Ctt;H4XwleaWCklfsXmG3R&?)_=?mh0T_-o=i8 zS~1W!Z|}v{m)>n!w`}+89j9;7`QSq^fIEU?)W5=di%F^{nZqBK?q*NkzYne!-~z^d zPjC}t2LhD(o1%Oer9vVUNf;%dCOVn1B#S_<0S{tfzY(KtY4aNM#&`y&#!objbq=m9 zPa*G(bVcX&v}QS{_$BGX++};`R3B!GENjncvud_ZH&sra9Gk2Ub?_-|?in*s_S~xF zrBM#S?hyfTF_{7V`<74~Vblh^FBaUQ<4D1y;wzklU^6es3?Ufgn2{pR2gI!ei|8;B zXrH#7hT$pw=i_E|Bu%QUSigCeFt10u#(Zf(&B?CxNk&8AL=*SL^+nvsn1cGl1LM0V z1TNarTD@-S_|%4_WiK=ptedECPMx_T>vhfs?mYq953$~WFK4h!LD!g+da7q|0v2H% zQ~t^;yRBPEnal3&+#6I=%#VD(>Hi|q60gL!D6KGl?d5`oA_DU7kxC>=Oe zm=@Io=}by>36TN*Qctq6jO?}ENuRNE|6lbkbKdDY{O|Bxg&(ij@94gVF^b$IYOxwm zAj1P(q2<*BDap%jueq?#>xc6@LyhfQ>9~8LUsiAw^gs~wJSzE>qmmv!?4R&f00>RS z0z$q;q&m>H^q^3+zhULG+_>?n`|j(U(mS53Irz2KQ9Z^lL4YUi#6undb$!RXbUw9< zpCfYL%R56%lXfnibaiuZSbRrwMztf!w$B}mMAl-bHAk$+Ix}mr-Z)8Et5R_6exAT{ zD{x0WjB^aEGu2nAz{@|YDSoibW4cMs8yX6c52pM=(dSQj9z?&L_etiOSB2`AI-WQs zazD04C|gzDG9Z*LtLa`jvi&13hci>Rs2%z~S@tY^HZOl_ zosXgA3Fcou-bIzm+PpmSD^_6XQ_Hns*PD%78Uq7+Z**VX8yz}n)$}-N(FSA0o*n8Q` zem5idO-|&Nn-EEz>~K8~Hy~FhUf&a*-pLQ+lHJNErMg1>-&1o*o*+Sbq_ZaO_2V zUqNa}i4}wn#Z{c`U(5km>oD-c5a@Bh3u{SsdqttnX;r#bL(RQaxoip?n+i!nNji~EK6hOq|OZVSCZ4rYP}hyv6?y%sS*Xflw9 z>?rZ^ykqC!hic~mhf&E5W{Cu5qgEkc@5f1l9h!LYKis!mnML{*E`NJ9vYlU}k;D{M zhFRtXh7L@ge?}v^v~KQEcacC`V2Z7ERW54m*FYB%9&SWuAnHlB5L=J5O7V^b&em&L|~B#ztBr8PcrX?vSrpqnbbcztQ4dDe5Q z^6HO%vm`C;1McGtlAzK|n7MFsIe)>Ph`G4Y7$$2dieE)Z&2Drdcrs>(6~SxBK=Rn~DMw^Yep)W;8b* z|ERm?_Omt1A)HKj9Q6W@BoNO4xg5@h@Tegc1p!h5hXoXxjQLOzoKv-a>JL9Gd8r}d z;N(r8uH`cDnKhqnnoMq{E$i!;5&hHb!oAn~c5wGDv`Prq*s@_^ zSqvQqony}E9PMr%@A~x&ldZ$U);Ta}>k`s?KacFQmeKM%;6`;2wZp(C+_9UdR=a({ z0vsY!a?_~2h0Rccw<3&&wgG3)eJ!QyGlx0u>Qi?OZbThz?mfQfbRDyOr0z%V)jP+t z9*pO`hvT8bRF6e~#bNUGdFwnfblrOW3>}n&d}r+#xlPWVwvIfgr+|7OgXO%d3N@@F zH88r{yiA383Qj^k_z5s>5UOByP&VqhiLcz@kAWX*`eP_pdGf}S^46{+2gCCjfY zm`9Upa$|1Ol(n5{c}t#ox~OGNL0?(rdt z5BS4@$0y|vlU7OvX$Uq#h=Ju*KG=XTCG|DjD|8H7j%^7GoVKMS-MWUK>!$(-n5zfx2|Xg6>0DzhMvFQIKM(-a5*MjOMk?FE zyEm|TDp^{^x}xfRHD;J|#ikiC3;fO$zfm_L4xF+4a!=*63oDak?o(3^mK@0)YOa2E z5xm0G+h-RIbfq=C@bQAC>VnmC#|HVW>)g5ld-0P`so$^@FtUsLs-UN9Nn%=J&A_&) z4ZQrNG@i%fz=Ps|7o1@60%-veP()A_i2(;?>k6Wzr!QV3E4V2)0T16{(;Elz3yjdu zoSPZAaKruEq}#eiaO~E(t$+5h&ZBe}25asFct`}gvple?g~o81sNKGfQ=MdlS1mK8lk6PQ|?GX|@*LRU>tP}0~E(}C9xFV|$ZEiCg-4iE9xDRfR!-$+yT zq}teJFBQ_5Bj#=HT`>O#RtIo+F`L0zhB-aRr2Dr8dMHL|9p*9s<|HORrB=^mauXENg)jqlY)cgrcYvKgQMs9GgaXY_!ziufLGIFE6=-|PW#c$JK zIJ60xjNJ!q(%3GjJh&L{0GIV4mwp(7+y=At*!_D@t`PY18z=|-DL2WXnB2sG?V~T; zdkV{5!?NG+{o+IJNAAZ@faiqljh{kTaIn~`KbfFz4)g)5!pmvO+E9&(jhvHsHm*9! zCEzP(xjo#_3-}@kj(w!e8e}z+9P3GP%Ra_js9z2B9cbUw-U717Jp>`w>O)*Q6*h;! zgiEw4$T#-#)lmL6l;`n@K_g&YFPk^AmudW|k7*b=&I=tLsTeJ{?IjYyeOgvPwTE76 zA`-3(_y6!ixv*s5{#o#C?|`x|wfnX@=$*V^jXXlG3LXA)&)#1zv>~>)p*Dj6f~Z{Y zk3Zu6@&Q5Wz<&<%cwWgaV$9$JQ#|X9A{t^cpV>8h8Y`Exi*CG!ivUl>HVwPOc96<$ zHK>(hHvP*1s<+D7Zv3iHM}2?H@OD2Rya4+SIG08&aMgEWhG>-{8esR?y?x7&facfkPC&j&zMR!9o8ZQT6-8-iZ&R*)37_bA|rK|yaCHeys1Eq zJ$$$2iS>=;>ozZ_9}+L=8aLl>(yC_;e%Mb!GU{XYy?cvf_E**Q^;gxd?rVxHU$;BN zufFMvs|&2(>>`S!NVMhZjCf)=A%R>UjWjHYc`jMMqbW~U) zkI1Y}uGv)@9$vb;Hn}=8Off``Z~toV-p&reQR}6!tTg@1CoiqmS2izm$%$jjLMF%PoaVcy zPHjq9{$5vLUelJs_Ge?5ps1?9pM3Ri`j`mi#Gvdn!w_b67VRIHoiv~Bix4y(aJmiV zq=7ve%9(oeLkkOx1hD*AH@nAc@FTXNAw~GO>Xd?2CCOglrMqfiS`l*VUv_kK?%n&9 z;OPB=Q(@y$^{I`UpDb>@@Z?N=T2|O*uJ!KSH!glb`>KRF?S?stc>L*jYXqVJyy2$i z1vqx&!vug-l@ep(L6=PN4DEvp!=ipHdL<2#MJKPlfk zXJVqIYVLLpC*R*PW83K(=kdVlfWt-5>3f39phsY}KpvY-fHmliMulO`gx(M##(RhQ z*CrH~8{;R2G)ZKEUV(A1F^5Nrn8SU2iL*0|K|u{NnOk(&F<#~XzPJ=-8#@Uo&$x%N2aXm;GBfC zy3W{XU7J?vevQ_bf4WZYJVY(bwHNsO49E?4gmfSWAfioIPYKhG4ky$=#+soaul?-> zE2}Nryc26z*QNM^^m%J~Px`7`8?1MTcgf(~^o;I72%^xnS`Ii%5o`uuSq_rxv1pna z=wx}U!OtWju?AhVh{(5X6POLLAzGmey^F0sT}K2?9TehPXv3M(mc0!ctn@Qr2*R79 zlBmT)-E3eVd^pw+5JxZW<2eoznOq~O9SYCLPj?T_*3zaeM!5bnR4a0EaS|>#%tron zy+5TcB}D3@;n$kZ{|L}A*wa)#gINcSgTVV3Up>mLoINyzw;v_-)**scaw`Q#M~3j% ze7!;29ee=l0mTG>`)5+}fl08&!Wg&ew&ilsw(C-t3CnY)2WZ(lc03C!60q`6h z4ujW#t~V)@VMZ!7MZ3z5byn{uWr~IzJVGXz{xmcs(~ODC4e#9Q=`q-}XX`eh#5*J{ z72hO4mtiH$sI7?bMnv8JJZ?cd+Eb`al$vg+(aAFhtlGQYmi@96p}|8Lfp zjXd!i%S%u#@ZojfLpREYa-JpZkvX~$7T%Mh@k~2^*2c{_)Uft)#jMLu^k1&np-pee z8Q+qwwJfWsTxKDf?l-qh9lW>a$&u|lM)sBr^<-r943+e})YbKposN3BLgHkt@vXL)8E;6mV1h=hz4`n9Lo2(1;;ijM&>HLsg%f-uv7jb7i2+8~R`>bJIOImuN-aMGk zi))M3xmqJtm5WWR*|PNjN%n zfj+He+#l^+`zSsLJ@ZilxJiz#QQH(GuGZvH)y4 zh}_AO8AFUE*}{T5Xu9|Ek*63!QO69g`c}Z>?mL` z?qjuHs=Tm?FP@RB_pEz5Bw6Dk^3<|@7&8J#*oOC0h$TjxI_)gKcA%%R7-TTmj@0j8 zw4Rz0FyED6n;x{!1B^Kpgc==0W@?RCVv@iC#59f!2^c&TRU)&G158N;yEEkABNI`@ z+b0q_ou5dGpO=7Tb3q?#vJZU~Fh zNQ7R$n24)O0QW(`?;;zmzXdP5^&u9Q z367qlFm%pN`*~;*xx=0swG~HW?7+2P=h$V5eVx@p*aF9k<_vo{#m+2`?yIUU$(u8E zu}IdMx1^vXaep6HyKL+Zjmup)G2G?mP0h4fdGlxJ3{z&#E-8VtO-tq>7=j^&%vGCO<`vBs#E!kCiVTE`vwbj+779g)J5b#f`& zJr`)Q$KWbTUvhzrsN9nCXHVeo-18Po@9S}yUjB9Y7ZzA73ob;b2YA;{v-`c&FZ334 z!I>unn9B*Xe9iLsvP zyV=Jb$GLqlN8oRCSFi*4061I@tx(Z50$%oF{FXQ871jhcwfZeFDn`vi>riijnTjmjy6q9bX%a2z`dLO{;eD(6<&7 z#^u>vmiXp#^Ri3bPDAZFHNMy47l`z&^@;UjKIl%83LEP%WVC{nocBP6{?*Hz{nd_ zro7OA0~QeR=gEZbNP-mX8apMY%~jA-G$Ub>Uv4ZBMeD+SxDJu~a~&&RfsLw;m1nvz zKC$~BS|^S>u?rrga%^dqS~;~Rp};%VAQEdsJu?D)TQ;6uN>BpPFbcK|;)$Sfz?qhV(xG>T4D_8l*LUt6dOy#WaksgTupidjpLCuOBPBX! zh*@Oc6Q&VdW@xo_E#Zjn>Ut^@ngH7}rU`bCz#>6M;9}t%+;?m-`whfiLg8G@M4%Z! z0+mhtrZp8zFiUEg*`sz;cVVL;z+S*$rXhCYD3V}LObsCbrT#rfxBSMr2^niTvmM=v zTD#2S`Z}|^QtDRqk2`_g4bEG?>YelElOAF`Z1Niup~0e=6TAxw+IG1xGIf}z=f3K7 zX$9olsL-6QEPr#wP)T^;g!$Qq$g-WYYL9dl7(PB07qjVai>97j5ZlIzy_CcO`za^I zCp$^JL){e)!oS}kpYr%fVeXJ!>};w#=x%KanQ7h6&hCRe0%uS+lO(~Ao%ZOQ?N-!- zh5scjKFv5!icXqhzt7#?RJK7bfctv6kli73Go?*Sag&zuCCHp1P5;(HKw z0Mim9fEt((H&ej^F;ql=&(7N$ zIB5MwvH#ZaN#+3Q`t{*m@uA~_19P|#_Fr5~ZtSzh)ckw{esg4~GA={z?GHl)+JN{a z^CQSH5scCk=mR|ED3wVHwt_vn%&H;e?Pt(4(s0(USrV|6tdyRROHb^J$eI`|5cvnC zjR{zPOLknoKB=rMEyCHMqWk%GckQ!x# z@lo`qy0MQUV%NCZ+{6Fz6bCIOqemhgo`c%ycO4yNIM+nHwA1^ z*f6sD>sSXaFjYjO7h`5jjFh{_SzjcR$ptPGpQe}fFnRMWs$2xewp#zQnUig1Z{%2i z;MZavSIq7g+=PDS;>)(p(2qeuaRG4w)=RVm;A7F3xD4Qd{Jib(H2`nN<-dZL06Y!O zrmy^EdEpI!H=wf&od@5J?MF5hRfvEtyc!-h{VMB-5gkz%=gWO+_eYSJ=C$Cy%fnN@Pf=XJK?IMR-+>Y2Vx&fua8+?ay7LIZEh?z0( znX35}zC+`-n%zCkrQOd|Z7>(LOkZD;q*3c9Ti?KMjW33`M2*TjD06@R9oS?#^t-{i z8Byc5ZRBiCodr)H-rSLXF(G&UzWs%rE&HlR${DL+@}j4oDx6onlBdh%FxG8$`oKv% zeZ<=7kB1La{NXddf_L-yJAn5AJ{~D=#}oRF$CDinCkWVPk)4ij@pPQ%M0LqVfHOch zPJA@czCUizF(#>Y6gjZbHD=n-_QtV-aO=qMFzFlTV5wVn`01))d|P5js7zI=Kfq_X zYus5w;FOic$vO%59O&~>+h^=d!9_UP4n6~KsR6EOPz+8<0a?1~{J{GMUt(1D!DctO zKw!Gj6RSqL1Vkis!XkZ)2R>@~;lxwl`8b4T6z3a5%4#M_M4T{h_6+AhUze2XSt$mA z`@I2wjnq{cJpK4GhoFDwbWAoWbl}Eq5sNkK(oHA!uL}+niejrW${syZ9%Rrz$ip8-+t`!n_<~z&ZWcJWC}v-};Z~%Ya}vQrS=jvE)Xf9&kMt9Yq9i zox!h-9Zq#*J5I2D_e;3oikktYNq)3L`8DRt3l2f~3e-b#A1hx5@CHc}SQgY{YH0W)W zI>un(u}#mhft{q1>mZLJA0Ka+<={75+9ZSv=B-i4+$63J zD`S^88B@*vp(=NoXQ+8#gZZ_we%2syblR}paYToLq^cXH7poT(-zLOguho=3;@9M5fYsxmud%h&GXYO?vDh{Qc*^A0rxD^d zPYXi;J{Gy#X|kh?f$lv1?~F&+R&oyUivL#iy-*s zg3$|3Lc75FG?F#C>xKIujJ)f?&KGT;hoAyjwU#7OG?>cF=jce4ayp0@{ABW zD(YjmSFO&P9+V^Tw4CePRgN!)M|G{fxhlZBW9RIh2rH&o*wyj1pR#b2MD#H9%)BFZITAmf=@n#ccW(+FLnmB zjZ_4Ci$wZ=!-MF^b5S z+}gClakCS>U7btPrli#;>s%dOuXk8HWV8OTZ27Fnoo#XMvRUVPmsUkEjP==!NxhpU z6=g*2NXcEWDR){{NC0>06qCNMwOqhJ4q$|I4FE=!45TDV26#`|U#w608S*f%pjd&R~(_A%43 z38*dFfV%L9qhrJ9F!5#%lLN4Fyia(TK~_W#P@mAASNupGIchC;-9Q+km+dTnzW&o?QMdNt%A;4Zi?EN2k z*lehq26ct#jGC6oXER8<;BPSQGdLSQWCl1#0A9f+lcj<$0sack;o+|k3U9MTkqvyC z=aJj}@A)=oK-q;*R*23y-G_Pgq4$_G>=AnAi(0jIQ$j@JqCydP0W|&r4}KXy$MMRI z2F%RSSkk-Q_~N3rMH9=Tit`g^`CJ0e$xesMy7&PdMuaWpMPa#l8)hHn$u z3|N26iokaBg70l<8qQhaA*f|fV>o|XH<~73759v$8B{tNDJR>_rSdnzB8h* zDtOjF2`zjn*$B@D9-@VK8GLjI|5_n7GzR{4qOM2s!=Y3Qd}$B_^}3->CzRI0N!u3G z3~BmoM|IGy79?XhP~WfHs~s&%pZ2ohpkZsFbTicJgqEG)A01@_z_pNLZ#xvy4N#^E zM$-lJq3Jq+(Q>qJdu|7$+9AIRo*>O3rvtngTB?F?trGgE0jL&Qpt(O|rxV__P>W(k z3ulSC0AG#JMisneKwcA+r9FkiyB$I?V8>+*d{Ak}We=3>g8XhMZ6DRoSfbbpr#8gc z_L_>$nrdxjkG8PBqO+>Lrd>O=vAvYfW8OV+ABAdHgpr zi3g-XOHnc#Dl%yiz0D2UEMF4SGpvhlgC0@&qKs(x_HeMSU85R{56%heN|9k89 zaX+X9A8PZ9*)d8$TIh*>mK+WjO=+P(O?w9*Q5z8v86FuC0g#^?e}IJlna|K;3y*-3 z{&%heZ2y`*DDeNG?tiCrX!sYj0PhoFMJao?!zb5WaHlw$3nFw9tZX~aB8?z+e$J@? zH`#}cWGAp$Y!ck#SPynAo64rKv1}C7E8^LNvRU2#&|=Lm+JsM29`^Ss&{zh}hkGYL zr@)=ET{Ta4)&E0_R+4b-*MYz?$T8J!A`x<`7g0s4j?p*PUCyMPFP>vsWw z7)t&B{eAp?v;wUJdu0%Pg;t?;V3$7we`kT9wdhX}@7s(9&^ELhy@ozQhtWRtJ9HUc z1pmGgB;^J`{S`pxCHT7UI(iFTMc2?>K;3)jZS)R$4p90Jh@AWpy^n4H&3=vkhSs3j zfa*Db^hQ{?y+B57pm``^b-^Oc1v&axG!Mbw)<6s3{MJISzn_G^XRsJ8g%dE}pl{J> z%%B(0i|8bJ3B8V4@K;Zu3s``KP8}_^o%KDf^)<~-wGDM$?KRaE9rZ44T`iq8?NpX^ z)KrVQnj4}bA~V@>UG0>r=<0!eMR=f<8!I}S z!P}#G)RwdV?D;;Rc^=QFIe1P@JFP=$ggm%IA`A1;Br_j6T1Hk`Z06YJHS|ZzJxXWy9un%Og9YYq|FnqBSf~MO_;p@|V zkY@(HiUEEG?oqbQFe`s3W3jD=cYj+q|Gp7UZBnbp-zG+Zw$muvc9w_lh1BPKDjj9m z=*f;0=m|sXE8#xPb_GskWI(U^(0YOGCO}R@OGhD=brbR@!b;M69^7X@Sx94Qmt{Ya ztAZ9}@HZg>;T%;s|Mwp(5P6IT9HfBmwf~KWiNK Date: Sat, 3 Oct 2026 12:28:08 +0200 Subject: [PATCH 06/15] Add text shaping, alignment and orientation API --- include/CSFML/Graphics/Text.h | 247 ++++++++++++++++++++++++++++++ src/CSFML/Graphics/Text.cpp | 144 +++++++++++++++++ src/CSFML/Graphics/TextStruct.hpp | 11 +- test/CMakeLists.txt | 1 + test/Graphics/Text.test.cpp | 66 ++++++++ 5 files changed, 465 insertions(+), 4 deletions(-) create mode 100644 test/Graphics/Text.test.cpp diff --git a/include/CSFML/Graphics/Text.h b/include/CSFML/Graphics/Text.h index ba52b190..75a16195 100644 --- a/include/CSFML/Graphics/Text.h +++ b/include/CSFML/Graphics/Text.h @@ -30,12 +30,15 @@ #include #include +#include #include #include #include +#include #include #include +#include //////////////////////////////////////////////////////////// @@ -50,6 +53,102 @@ typedef enum sfTextStrikeThrough = 1 << 3 ///< Strike through characters } sfTextStyle; +//////////////////////////////////////////////////////////// +/// \brief Line alignment of a multi-line text +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfTextLineAlignmentDefault, ///< Automatically align lines by script direction, left-align left-to-right text and right-align right-to-left text + sfTextLineAlignmentLeft, ///< Force align all lines to the left, regardless of script direction + sfTextLineAlignmentCenter, ///< Force align all lines centrally + sfTextLineAlignmentRight ///< Force align lines to the right, regardless of script direction +} sfTextLineAlignment; + +//////////////////////////////////////////////////////////// +/// \brief Cluster grouping algorithm +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfTextClusterGroupingGrapheme, ///< Group clusters by grapheme + sfTextClusterGroupingCharacter, ///< Group clusters by character + sfTextClusterGroupingNone ///< Do not group clusters +} sfTextClusterGrouping; + +//////////////////////////////////////////////////////////// +/// \brief Direction of the text a glyph belongs to +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfTextDirectionUnspecified, ///< Unspecified + sfTextDirectionLeftToRight, ///< Left-to-right + sfTextDirectionRightToLeft, ///< Right-to-left + sfTextDirectionTopToBottom, ///< Top-to-bottom + sfTextDirectionBottomToTop ///< Bottom-to-top +} sfTextDirection; + +//////////////////////////////////////////////////////////// +/// \brief Text orientation +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfTextOrientationDefault, ///< Default (left-to-right or right-to-left depending on detected script) + sfTextOrientationTopToBottom, ///< Top-to-bottom + sfTextOrientationBottomToTop ///< Bottom-to-top +} sfTextOrientation; + +//////////////////////////////////////////////////////////// +/// \brief Glyph that has been positioned by the shaper +/// +//////////////////////////////////////////////////////////// +typedef struct +{ + sfGlyph glyph; ///< The glyph + sfVector2f position; ///< Position of the glyph within a text + uint32_t cluster; ///< Cluster ID + sfTextDirection textDirection; ///< Text direction + float baseline; ///< The baseline position of the line this glyph is a part of + size_t vertexOffset; ///< Starting offset of the vertex data belonging to this glyph + size_t vertexCount; ///< Count of vertices belonging to this glyph +} sfShapedGlyph; + +//////////////////////////////////////////////////////////// +/// \brief Callback that is provided with glyph data for pre-processing +/// +/// The callback is called once per glyph whenever the text +/// geometry is regenerated, in the order in which the glyph +/// geometry is generated. The style, fill color, outline color +/// and outline thickness of the glyph can be modified through +/// the given pointers. +/// +/// To map glyphs back to the input string, use the cluster +/// value of the shaped glyph. See sf::Text::GlyphPreProcessor +/// in the SFML documentation for a detailed explanation. +/// +/// Changing the style or outline thickness might lead to +/// slight inconsistencies of the text bounds, changing the +/// fill or outline color is always safe. It is not safe to +/// query the text bounds from within the callback. +/// +/// \param shapedGlyph The shaped glyph to pre-process +/// \param style Style of the glyph (see sfTextStyle enum) +/// \param fillColor Fill color of the glyph +/// \param outlineColor Outline color of the glyph +/// \param outlineThickness Outline thickness of the glyph +/// \param userData User data passed to sfText_setGlyphPreProcessor +/// +//////////////////////////////////////////////////////////// +typedef void (*sfGlyphPreProcessor)( + const sfShapedGlyph* shapedGlyph, + uint32_t* style, + sfColor* fillColor, + sfColor* outlineColor, + float* outlineThickness, + void* userData); + //////////////////////////////////////////////////////////// /// \brief Create a new text @@ -367,6 +466,31 @@ CSFML_GRAPHICS_API void sfText_setOutlineColor(sfText* text, sfColor color); //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API void sfText_setOutlineThickness(sfText* text, float thickness); +//////////////////////////////////////////////////////////// +/// \brief Set the line alignment for a multi-line text +/// +/// By default, the line alignment is sfTextLineAlignmentDefault. +/// +/// \param text Text object +/// \param lineAlignment New line alignment +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfText_setLineAlignment(sfText* text, sfTextLineAlignment lineAlignment); + +//////////////////////////////////////////////////////////// +/// \brief Set the text orientation +/// +/// By default, the text orientation is sfTextOrientationDefault. +/// +/// Vertical text orientations require the font to provide +/// vertical metrics, see sfFontInfo. +/// +/// \param text Text object +/// \param textOrientation New text orientation +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfText_setTextOrientation(sfText* text, sfTextOrientation textOrientation); + //////////////////////////////////////////////////////////// /// \brief Get the string of a text (returns an ANSI string) /// @@ -475,6 +599,26 @@ CSFML_GRAPHICS_API sfColor sfText_getOutlineColor(const sfText* text); //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API float sfText_getOutlineThickness(const sfText* text); +//////////////////////////////////////////////////////////// +/// \brief Get the line alignment for a multi-line text +/// +/// \param text Text object +/// +/// \return Line alignment +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API sfTextLineAlignment sfText_getLineAlignment(const sfText* text); + +//////////////////////////////////////////////////////////// +/// \brief Get the text orientation +/// +/// \param text Text object +/// +/// \return Text orientation +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API sfTextOrientation sfText_getTextOrientation(const sfText* text); + //////////////////////////////////////////////////////////// /// \brief Return the position of the \a index-th character in a text /// @@ -495,6 +639,109 @@ CSFML_GRAPHICS_API float sfText_getOutlineThickness(const sfText* text); //////////////////////////////////////////////////////////// CSFML_GRAPHICS_API CSFML_DEPRECATED sfVector2f sfText_findCharacterPos(const sfText* text, size_t index); +//////////////////////////////////////////////////////////// +/// \brief Get the shaped glyphs that make up a text +/// +/// The result of shaping, i.e. positioning individual glyphs +/// based on the properties of the font and the input text, +/// is a sequence of shaped glyphs. In addition to the glyph +/// information that is available by looking up a glyph from +/// a font, the glyph position, glyph cluster ID and direction +/// of the text represented by the glyph is provided. +/// +/// When positioning e.g. a cursor within the text, grapheme +/// clusters can be treated as the basic units of which the +/// text is composed. See sfText_setClusterGrouping. +/// +/// The returned glyph positions are in local coordinates +/// (translation, rotation, scale and origin are not applied). +/// +/// The returned array is owned by the text and stays valid +/// until the next call to this function or until the text +/// is destroyed. +/// +/// \param text Text object +/// \param count Pointer to a variable that will be filled with the number of shaped glyphs +/// +/// \return Pointer to the array of shaped glyphs +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API const sfShapedGlyph* sfText_getShapedGlyphs(const sfText* text, size_t* count); + +//////////////////////////////////////////////////////////// +/// \brief Get the cluster grouping algorithm in use +/// +/// \param text Text object +/// +/// \return The cluster grouping algorithm in use +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API sfTextClusterGrouping sfText_getClusterGrouping(const sfText* text); + +//////////////////////////////////////////////////////////// +/// \brief Set the cluster grouping algorithm to use +/// +/// By default, character cluster grouping is used. +/// +/// Character cluster grouping is good enough to be able to +/// position cursors in most scenarios. If more coarse-grained +/// grouping is required, grapheme grouping can be selected. +/// +/// Cluster grouping can also be disabled if necessary. +/// +/// \param text Text object +/// \param clusterGrouping The cluster grouping algorithm to use +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfText_setClusterGrouping(sfText* text, sfTextClusterGrouping clusterGrouping); + +//////////////////////////////////////////////////////////// +/// \brief Set the glyph pre-processor to be called per glyph +/// +/// The glyph pre-processor is called with glyph data to be +/// pre-processed whenever the text geometry is regenerated. +/// +/// \param text Text object +/// \param glyphPreProcessor The glyph pre-processor to be called per glyph, pass NULL to disable pre-processing +/// \param userData User data that will be passed to the glyph pre-processor +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API void sfText_setGlyphPreProcessor(sfText* text, sfGlyphPreProcessor glyphPreProcessor, void* userData); + +//////////////////////////////////////////////////////////// +/// \brief Get the vertex data of a text +/// +/// The vertices form triangles (sfTriangles). +/// +/// The vertex data is regenerated by the text whenever it is +/// necessary. Any changes made to the vertex data will be +/// discarded whenever this happens. +/// +/// \param text Text object +/// \param count Pointer to a variable that will be filled with the number of vertices +/// +/// \return Pointer to the vertex data of the text +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API sfVertex* sfText_getVertexData(const sfText* text, size_t* count); + +//////////////////////////////////////////////////////////// +/// \brief Get the outline vertex data of a text +/// +/// The vertices form triangles (sfTriangles). +/// +/// The outline vertex data is regenerated by the text whenever +/// it is necessary. Any changes made to the outline vertex data +/// will be discarded whenever this happens. +/// +/// \param text Text object +/// \param count Pointer to a variable that will be filled with the number of vertices +/// +/// \return Pointer to the outline vertex data of the text +/// +//////////////////////////////////////////////////////////// +CSFML_GRAPHICS_API sfVertex* sfText_getOutlineVertexData(const sfText* text, size_t* count); + //////////////////////////////////////////////////////////// /// \brief Get the local bounding rectangle of a text /// diff --git a/src/CSFML/Graphics/Text.cpp b/src/CSFML/Graphics/Text.cpp index d838219e..83e488be 100644 --- a/src/CSFML/Graphics/Text.cpp +++ b/src/CSFML/Graphics/Text.cpp @@ -26,6 +26,7 @@ // Headers //////////////////////////////////////////////////////////// #include +#include #include #include #include @@ -33,6 +34,22 @@ #include #include + +namespace +{ +// Helper function for converting a SFML shaped glyph to a CSFML one +[[nodiscard]] sfShapedGlyph convertShapedGlyph(const sf::Text::ShapedGlyph& shapedGlyph) +{ + return {convertGlyph(shapedGlyph.glyph), + convertVector2(shapedGlyph.position), + shapedGlyph.cluster, + static_cast(shapedGlyph.textDirection), + shapedGlyph.baseline, + shapedGlyph.vertexOffset, + shapedGlyph.vertexCount}; +} +} // namespace + #include @@ -250,6 +267,22 @@ void sfText_setOutlineThickness(sfText* text, float thickness) } +//////////////////////////////////////////////////////////// +void sfText_setLineAlignment(sfText* text, sfTextLineAlignment lineAlignment) +{ + assert(text); + text->setLineAlignment(static_cast(lineAlignment)); +} + + +//////////////////////////////////////////////////////////// +void sfText_setTextOrientation(sfText* text, sfTextOrientation textOrientation) +{ + assert(text); + text->setTextOrientation(static_cast(textOrientation)); +} + + //////////////////////////////////////////////////////////// const char* sfText_getString(const sfText* text) { @@ -333,6 +366,22 @@ float sfText_getOutlineThickness(const sfText* text) } +//////////////////////////////////////////////////////////// +sfTextLineAlignment sfText_getLineAlignment(const sfText* text) +{ + assert(text); + return static_cast(text->getLineAlignment()); +} + + +//////////////////////////////////////////////////////////// +sfTextOrientation sfText_getTextOrientation(const sfText* text) +{ + assert(text); + return static_cast(text->getTextOrientation()); +} + + //////////////////////////////////////////////////////////// sfVector2f sfText_findCharacterPos(const sfText* text, size_t index) { @@ -341,6 +390,101 @@ sfVector2f sfText_findCharacterPos(const sfText* text, size_t index) } +//////////////////////////////////////////////////////////// +const sfShapedGlyph* sfText_getShapedGlyphs(const sfText* text, size_t* count) +{ + assert(text); + assert(count); + + const auto& shapedGlyphs = text->getShapedGlyphs(); + text->ShapedGlyphs.clear(); + text->ShapedGlyphs.reserve(shapedGlyphs.size()); + for (const auto& shapedGlyph : shapedGlyphs) + text->ShapedGlyphs.push_back(convertShapedGlyph(shapedGlyph)); + + *count = text->ShapedGlyphs.size(); + return text->ShapedGlyphs.data(); +} + + +//////////////////////////////////////////////////////////// +sfTextClusterGrouping sfText_getClusterGrouping(const sfText* text) +{ + assert(text); + return static_cast(text->getClusterGrouping()); +} + + +//////////////////////////////////////////////////////////// +void sfText_setClusterGrouping(sfText* text, sfTextClusterGrouping clusterGrouping) +{ + assert(text); + text->setClusterGrouping(static_cast(clusterGrouping)); +} + + +//////////////////////////////////////////////////////////// +void sfText_setGlyphPreProcessor(sfText* text, sfGlyphPreProcessor glyphPreProcessor, void* userData) +{ + assert(text); + + if (!glyphPreProcessor) + { + text->setGlyphPreProcessor({}); + return; + } + + text->setGlyphPreProcessor( + [glyphPreProcessor, userData](const sf::Text::ShapedGlyph& shapedGlyph, + std::uint32_t& style, + sf::Color& fillColor, + sf::Color& outlineColor, + float& outlineThickness) + { + const sfShapedGlyph glyph = convertShapedGlyph(shapedGlyph); + sfColor csfmlFillColor = convertColor(fillColor); + sfColor csfmlOutlineColor = convertColor(outlineColor); + glyphPreProcessor(&glyph, &style, &csfmlFillColor, &csfmlOutlineColor, &outlineThickness, userData); + fillColor = convertColor(csfmlFillColor); + outlineColor = convertColor(csfmlOutlineColor); + }); +} + + +//////////////////////////////////////////////////////////// +sfVertex* sfText_getVertexData(const sfText* text, size_t* count) +{ + assert(text); + assert(count); + + // Make sure the geometry is up to date, sf::Text only updates it when needed + [[maybe_unused]] const auto bounds = text->getLocalBounds(); + + sf::VertexArray& vertices = text->getVertexData(); + *count = vertices.getVertexCount(); + + // the cast is safe, sfVertex has to be binary compatible with sf::Vertex + return *count > 0 ? reinterpret_cast(&vertices[0]) : nullptr; +} + + +//////////////////////////////////////////////////////////// +sfVertex* sfText_getOutlineVertexData(const sfText* text, size_t* count) +{ + assert(text); + assert(count); + + // Make sure the geometry is up to date, sf::Text only updates it when needed + [[maybe_unused]] const auto bounds = text->getLocalBounds(); + + sf::VertexArray& vertices = text->getOutlineVertexData(); + *count = vertices.getVertexCount(); + + // the cast is safe, sfVertex has to be binary compatible with sf::Vertex + return *count > 0 ? reinterpret_cast(&vertices[0]) : nullptr; +} + + //////////////////////////////////////////////////////////// sfFloatRect sfText_getLocalBounds(const sfText* text) { diff --git a/src/CSFML/Graphics/TextStruct.hpp b/src/CSFML/Graphics/TextStruct.hpp index 628504a7..7dc7de40 100644 --- a/src/CSFML/Graphics/TextStruct.hpp +++ b/src/CSFML/Graphics/TextStruct.hpp @@ -29,11 +29,13 @@ //////////////////////////////////////////////////////////// #include #include +#include #include #include #include +#include //////////////////////////////////////////////////////////// @@ -42,8 +44,9 @@ struct sfText : sf::Text { using sf::Text::Text; - const sfFont* Font{}; - mutable std::string String; - mutable sfTransform Transform{}; - mutable sfTransform InverseTransform{}; + const sfFont* Font{}; + mutable std::string String; + mutable sfTransform Transform{}; + mutable sfTransform InverseTransform{}; + mutable std::vector ShapedGlyphs; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 24e28902..e613c174 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -56,6 +56,7 @@ add_executable(test-csfml-graphics Graphics/RenderStates.test.cpp Graphics/Shape.test.cpp Graphics/StencilMode.test.cpp + Graphics/Text.test.cpp Graphics/Transform.test.cpp Graphics/VertexArray.test.cpp Graphics/View.test.cpp diff --git a/test/Graphics/Text.test.cpp b/test/Graphics/Text.test.cpp new file mode 100644 index 00000000..d8d3e95d --- /dev/null +++ b/test/Graphics/Text.test.cpp @@ -0,0 +1,66 @@ +#include +#include + +#include + +#include + +TEST_CASE("[Graphics] sfText") +{ + SECTION("sfTextLineAlignment") + { + STATIC_CHECK(sfTextLineAlignmentDefault == static_cast(sf::Text::LineAlignment::Default)); + STATIC_CHECK(sfTextLineAlignmentLeft == static_cast(sf::Text::LineAlignment::Left)); + STATIC_CHECK(sfTextLineAlignmentCenter == static_cast(sf::Text::LineAlignment::Center)); + STATIC_CHECK(sfTextLineAlignmentRight == static_cast(sf::Text::LineAlignment::Right)); + } + + SECTION("sfTextClusterGrouping") + { + STATIC_CHECK(sfTextClusterGroupingGrapheme == static_cast(sf::Text::ClusterGrouping::Grapheme)); + STATIC_CHECK(sfTextClusterGroupingCharacter == static_cast(sf::Text::ClusterGrouping::Character)); + STATIC_CHECK(sfTextClusterGroupingNone == static_cast(sf::Text::ClusterGrouping::None)); + } + + SECTION("sfTextDirection") + { + STATIC_CHECK(sfTextDirectionUnspecified == static_cast(sf::Text::TextDirection::Unspecified)); + STATIC_CHECK(sfTextDirectionLeftToRight == static_cast(sf::Text::TextDirection::LeftToRight)); + STATIC_CHECK(sfTextDirectionRightToLeft == static_cast(sf::Text::TextDirection::RightToLeft)); + STATIC_CHECK(sfTextDirectionTopToBottom == static_cast(sf::Text::TextDirection::TopToBottom)); + STATIC_CHECK(sfTextDirectionBottomToTop == static_cast(sf::Text::TextDirection::BottomToTop)); + } + + SECTION("sfTextOrientation") + { + STATIC_CHECK(sfTextOrientationDefault == static_cast(sf::Text::TextOrientation::Default)); + STATIC_CHECK(sfTextOrientationTopToBottom == static_cast(sf::Text::TextOrientation::TopToBottom)); + STATIC_CHECK(sfTextOrientationBottomToTop == static_cast(sf::Text::TextOrientation::BottomToTop)); + } + + SECTION("Set/get layout properties") + { + sfFont* font = sfFont_createFromFile("Graphics/tuffy.ttf"); + REQUIRE(font != nullptr); + sfText* text = sfText_create(font); + + CHECK(sfText_getLineAlignment(text) == sfTextLineAlignmentDefault); + CHECK(sfText_getTextOrientation(text) == sfTextOrientationDefault); + CHECK(sfText_getClusterGrouping(text) == sfTextClusterGroupingCharacter); + + sfText_setLineAlignment(text, sfTextLineAlignmentCenter); + sfText_setTextOrientation(text, sfTextOrientationTopToBottom); + sfText_setClusterGrouping(text, sfTextClusterGroupingGrapheme); + + CHECK(sfText_getLineAlignment(text) == sfTextLineAlignmentCenter); + CHECK(sfText_getTextOrientation(text) == sfTextOrientationTopToBottom); + CHECK(sfText_getClusterGrouping(text) == sfTextClusterGroupingGrapheme); + + sfText* copy = sfText_copy(text); + CHECK(sfText_getLineAlignment(copy) == sfTextLineAlignmentCenter); + sfText_destroy(copy); + + sfText_destroy(text); + sfFont_destroy(font); + } +} From b4220b39ffb157176309c990ebab675fc1f06e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:28:36 +0200 Subject: [PATCH 07/15] Document QOI support for images --- include/CSFML/Graphics/Image.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/include/CSFML/Graphics/Image.h b/include/CSFML/Graphics/Image.h index d858b946..41b21234 100644 --- a/include/CSFML/Graphics/Image.h +++ b/include/CSFML/Graphics/Image.h @@ -82,8 +82,8 @@ CSFML_GRAPHICS_API sfImage* sfImage_createFromPixels(sfVector2u size, const uint /// \brief Create an image from a file on disk /// /// The supported image formats are bmp, png, tga, jpg, gif, -/// psd, hdr and pic. Some format options are not supported, -/// like progressive jpeg. +/// psd, hdr, pic, pnm and qoi. Some format options are not supported, +/// like jpeg with arithmetic coding or ASCII pnm. /// If this function fails, the image is left unchanged. /// /// \param filename Path of the image file to load @@ -97,8 +97,8 @@ CSFML_GRAPHICS_API sfImage* sfImage_createFromFile(const char* filename); /// \brief Create an image from a file in memory /// /// The supported image formats are bmp, png, tga, jpg, gif, -/// psd, hdr and pic. Some format options are not supported, -/// like progressive jpeg. +/// psd, hdr, pic, pnm and qoi. Some format options are not supported, +/// like jpeg with arithmetic coding or ASCII pnm. /// If this function fails, the image is left unchanged. /// /// \param data Pointer to the file data in memory @@ -113,8 +113,8 @@ CSFML_GRAPHICS_API sfImage* sfImage_createFromMemory(const void* data, size_t si /// \brief Create an image from a custom stream /// /// The supported image formats are bmp, png, tga, jpg, gif, -/// psd, hdr and pic. Some format options are not supported, -/// like progressive jpeg. +/// psd, hdr, pic, pnm and qoi. Some format options are not supported, +/// like jpeg with arithmetic coding or ASCII pnm. /// If this function fails, the image is left unchanged. /// /// \param stream Source stream to read from @@ -147,7 +147,7 @@ CSFML_GRAPHICS_API void sfImage_destroy(const sfImage* image); /// /// The format of the image is automatically deduced from /// the extension. The supported image formats are bmp, png, -/// tga and jpg. The destination file is overwritten +/// tga, jpg and qoi. The destination file is overwritten /// if it already exists. This function fails if the image is empty. /// /// \param image Image object @@ -164,7 +164,7 @@ CSFML_GRAPHICS_API bool sfImage_saveToFile(const sfImage* image, const char* fil /// \brief Save the image to a buffer in memory /// /// The format of the image must be specified. -/// The supported image formats are bmp, png, tga and jpg. +/// The supported image formats are bmp, png, tga, jpg and qoi. /// This function fails if the image is empty, or if /// the format was invalid. /// From 523c48cf988e66ef43255dc1c9203b9e9f0a49e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:30:44 +0200 Subject: [PATCH 08/15] Add sfPlaybackDevice to manage the audio playback device --- .github/workflows/ci.yml | 4 + include/CSFML/Audio.h | 1 + include/CSFML/Audio/PlaybackDevice.h | 205 +++++++++++++++++++++++++++ src/CSFML/Audio/CMakeLists.txt | 2 + src/CSFML/Audio/PlaybackDevice.cpp | 135 ++++++++++++++++++ test/Audio/PlaybackDevice.test.cpp | 26 ++++ test/CMakeLists.txt | 1 + 7 files changed, 374 insertions(+) create mode 100644 include/CSFML/Audio/PlaybackDevice.h create mode 100644 src/CSFML/Audio/PlaybackDevice.cpp create mode 100644 test/Audio/PlaybackDevice.test.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03ffc978..f752e38b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,9 +62,13 @@ jobs: BUILD_TYPE: ${{ matrix.type.name }} CMAKE_FLAGS: ${{ matrix.platform.flags }} ${{ matrix.config.flags }} run: | + # Let the installed SFML libraries find each other, same as tools/nuget/build.linux.sh + RPATH_FLAGS=() + if [ "$RUNNER_OS" = "Linux" ]; then RPATH_FLAGS=('-DCMAKE_INSTALL_RPATH=$ORIGIN'); fi cmake -S SFML -B SFML/build \ -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ -DCMAKE_INSTALL_PREFIX=SFML/install \ + "${RPATH_FLAGS[@]}" \ $CMAKE_FLAGS - name: Build SFML diff --git a/include/CSFML/Audio.h b/include/CSFML/Audio.h index 11a1771f..b7f8a077 100644 --- a/include/CSFML/Audio.h +++ b/include/CSFML/Audio.h @@ -30,6 +30,7 @@ #include #include +#include #include #include #include diff --git a/include/CSFML/Audio/PlaybackDevice.h b/include/CSFML/Audio/PlaybackDevice.h new file mode 100644 index 00000000..58b4ce20 --- /dev/null +++ b/include/CSFML/Audio/PlaybackDevice.h @@ -0,0 +1,205 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include +#include +#include + + +//////////////////////////////////////////////////////////// +/// \brief Enumeration of the playback device notifications +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfPlaybackDeviceStarted, ///< Playback device has been started + sfPlaybackDeviceStopped, ///< Playback device has been stopped + sfPlaybackDeviceRerouted, ///< Playback device has been rerouted (Generated on platforms that support automatic stream routing) + sfPlaybackDeviceInterruptionBegan, ///< Playback device interruption has begun (Generated on Apple mobile platforms) + sfPlaybackDeviceInterruptionEnded, ///< Playback device interruption has ended (Generated on Apple mobile platforms) + sfPlaybackDeviceUnlocked ///< Playback device has been unlocked (Generated by Emscripten/WebAudio) +} sfPlaybackDeviceNotification; + +//////////////////////////////////////////////////////////// +/// \brief Callback that is called to notify of changes to the playback device state +/// +/// \param notification The notification +/// \param userData User data passed to sfPlaybackDevice_setNotificationCallback +/// +//////////////////////////////////////////////////////////// +typedef void (*sfPlaybackDeviceNotificationCallback)(sfPlaybackDeviceNotification notification, void* userData); + + +//////////////////////////////////////////////////////////// +/// \brief Get a list of the names of all available audio playback devices +/// +/// If the operating system reports multiple devices with +/// the same name, a number will be appended to the name +/// of all subsequent devices to distinguish them from each +/// other. This guarantees that every entry returned by this +/// function will represent a unique device. +/// +/// The default device, if one is marked as such, will be +/// placed at the beginning of the list. +/// +/// The returned array and strings stay valid until the next +/// call to this function. +/// +/// \param count Pointer to a variable that will be filled with the number of devices +/// +/// \return An array of strings containing the device names or NULL if no devices are available +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API const char* const* sfPlaybackDevice_getAvailableDevices(size_t* count); + +//////////////////////////////////////////////////////////// +/// \brief Get the name of the default audio playback device +/// +/// Note that depending on when this function is called, the +/// default device reported by the operating system might +/// change e.g. when a USB audio device is plugged into or +/// unplugged from the system. +/// +/// The returned string stays valid until the next call to +/// this function. +/// +/// \return The name of the default audio playback device or NULL if there is none +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API const char* sfPlaybackDevice_getDefaultDevice(void); + +//////////////////////////////////////////////////////////// +/// \brief Set the audio playback device +/// +/// This function sets the audio playback device to the device +/// with the given name. It can be called on the fly (i.e: +/// while sounds are playing). +/// +/// If there are sounds playing when the audio playback +/// device is switched, the sounds will continue playing +/// uninterrupted on the new audio playback device. +/// +/// \param name The name of the audio playback device +/// +/// \return True, if it was able to set the requested device +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API bool sfPlaybackDevice_setDevice(const char* name); + +//////////////////////////////////////////////////////////// +/// \brief Set the audio playback device to the default +/// +/// This function sets the audio playback device to the +/// default device. It can be called on the fly (i.e: +/// while sounds are playing). +/// +/// When certain backends are used, using the default device +/// will enable automatic stream routing. When automatic +/// stream routing is enabled, audio data is automatically +/// sent to whichever physical audio device is currently +/// marked as the default on the system. +/// +/// Automatic stream routing is currently supported when using +/// the WASAPI or DirectSound backend on Windows or the +/// Core Audio backend on macOS and iOS. +/// +/// \return True, if it was able to set the audio playback device to the default device +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API bool sfPlaybackDevice_setDeviceToDefault(void); + +//////////////////////////////////////////////////////////// +/// \brief Set the audio playback device to the null device +/// +/// This function sets the audio playback device to the +/// null device. It can be called on the fly (i.e: +/// while sounds are playing). +/// +/// Audio data routed to the null device will be discarded +/// by the backend. This can be used to keep sounds playing +/// without having them actually output on a physical +/// audio playback device. +/// +/// \return True, if it was able to set the audio playback device to the null device +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API bool sfPlaybackDevice_setDeviceToNull(void); + +//////////////////////////////////////////////////////////// +/// \brief Get the name of the current audio playback device +/// +/// The returned string stays valid until the next call to +/// this function. +/// +/// \return The name of the current audio playback device or NULL if there is none +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API const char* sfPlaybackDevice_getDevice(void); + +//////////////////////////////////////////////////////////// +/// \brief Get the sample rate of the current audio playback device +/// +/// \return The sample rate of the current audio playback device or 0 if there is none +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API uint32_t sfPlaybackDevice_getDeviceSampleRate(void); + +//////////////////////////////////////////////////////////// +/// \brief Check if the current playback device is the default device +/// +/// This function will return false if there is no +/// current playback device. +/// +/// \return True, if the current playback device is the default device +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API bool sfPlaybackDevice_isDefaultDevice(void); + +//////////////////////////////////////////////////////////// +/// \brief Set a callback that should be called to notify of changes to the playback device state +/// +/// Warning: Do not attempt to alter the device state from +/// within this callback. This includes changing the device +/// and even creating/destroying sound objects since that +/// could indirectly cause the playback device to be +/// created/destroyed. Also do not attempt to set the +/// notification callback from within this callback. Doing +/// so will result in a deadlock. +/// +/// When receiving a notification via this callback, store +/// the information somewhere and react on it from another +/// thread e.g. the main thread within the application. +/// +/// \param callback The callback to be called, pass NULL to remove the current callback +/// \param userData User data that will be passed to the callback +/// +//////////////////////////////////////////////////////////// +CSFML_AUDIO_API void sfPlaybackDevice_setNotificationCallback(sfPlaybackDeviceNotificationCallback callback, void* userData); diff --git a/src/CSFML/Audio/CMakeLists.txt b/src/CSFML/Audio/CMakeLists.txt index eb7e4394..d7a6cf17 100644 --- a/src/CSFML/Audio/CMakeLists.txt +++ b/src/CSFML/Audio/CMakeLists.txt @@ -11,6 +11,8 @@ set(SRC ${SRCROOT}/Music.cpp ${SRCROOT}/MusicStruct.hpp ${INCROOT}/Music.h + ${SRCROOT}/PlaybackDevice.cpp + ${INCROOT}/PlaybackDevice.h ${SRCROOT}/Sound.cpp ${SRCROOT}/SoundStruct.hpp ${INCROOT}/Sound.h diff --git a/src/CSFML/Audio/PlaybackDevice.cpp b/src/CSFML/Audio/PlaybackDevice.cpp new file mode 100644 index 00000000..307218c0 --- /dev/null +++ b/src/CSFML/Audio/PlaybackDevice.cpp @@ -0,0 +1,135 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include + +#include +#include +#include +#include + + +namespace +{ +// Helper function for returning optional strings +[[nodiscard]] const char* toCString(std::string& storage, const std::optional& string) +{ + if (!string) + return nullptr; + + storage = *string; + return storage.c_str(); +} +} // namespace + + +//////////////////////////////////////////////////////////// +const char* const* sfPlaybackDevice_getAvailableDevices(size_t* count) +{ + static std::vector stringDevices; + static std::vector devices; + + stringDevices = sf::PlaybackDevice::getAvailableDevices(); + devices.clear(); + devices.reserve(stringDevices.size()); + for (const auto& stringDevice : stringDevices) + devices.push_back(stringDevice.c_str()); + + if (count) + *count = devices.size(); + + return !devices.empty() ? devices.data() : nullptr; +} + + +//////////////////////////////////////////////////////////// +const char* sfPlaybackDevice_getDefaultDevice() +{ + static std::string defaultDevice; + return toCString(defaultDevice, sf::PlaybackDevice::getDefaultDevice()); +} + + +//////////////////////////////////////////////////////////// +bool sfPlaybackDevice_setDevice(const char* name) +{ + assert(name); + return sf::PlaybackDevice::setDevice(name); +} + + +//////////////////////////////////////////////////////////// +bool sfPlaybackDevice_setDeviceToDefault() +{ + return sf::PlaybackDevice::setDeviceToDefault(); +} + + +//////////////////////////////////////////////////////////// +bool sfPlaybackDevice_setDeviceToNull() +{ + return sf::PlaybackDevice::setDeviceToNull(); +} + + +//////////////////////////////////////////////////////////// +const char* sfPlaybackDevice_getDevice() +{ + static std::string device; + return toCString(device, sf::PlaybackDevice::getDevice()); +} + + +//////////////////////////////////////////////////////////// +uint32_t sfPlaybackDevice_getDeviceSampleRate() +{ + return sf::PlaybackDevice::getDeviceSampleRate().value_or(0); +} + + +//////////////////////////////////////////////////////////// +bool sfPlaybackDevice_isDefaultDevice() +{ + return sf::PlaybackDevice::isDefaultDevice(); +} + + +//////////////////////////////////////////////////////////// +void sfPlaybackDevice_setNotificationCallback(sfPlaybackDeviceNotificationCallback callback, void* userData) +{ + if (!callback) + { + sf::PlaybackDevice::setNotificationCallback({}); + return; + } + + sf::PlaybackDevice::setNotificationCallback( + [callback, userData](sf::PlaybackDevice::Notification notification) + { callback(static_cast(notification), userData); }); +} diff --git a/test/Audio/PlaybackDevice.test.cpp b/test/Audio/PlaybackDevice.test.cpp new file mode 100644 index 00000000..15350c17 --- /dev/null +++ b/test/Audio/PlaybackDevice.test.cpp @@ -0,0 +1,26 @@ +#include + +#include + +#include + +TEST_CASE("[Audio] sfPlaybackDevice") +{ + SECTION("sfPlaybackDeviceNotification") + { + STATIC_CHECK(sfPlaybackDeviceStarted == static_cast(sf::PlaybackDevice::Notification::DeviceStarted)); + STATIC_CHECK(sfPlaybackDeviceStopped == static_cast(sf::PlaybackDevice::Notification::DeviceStopped)); + STATIC_CHECK(sfPlaybackDeviceRerouted == static_cast(sf::PlaybackDevice::Notification::DeviceRerouted)); + STATIC_CHECK(sfPlaybackDeviceInterruptionBegan == + static_cast(sf::PlaybackDevice::Notification::DeviceInterruptionBegan)); + STATIC_CHECK(sfPlaybackDeviceInterruptionEnded == + static_cast(sf::PlaybackDevice::Notification::DeviceInterruptionEnded)); + STATIC_CHECK(sfPlaybackDeviceUnlocked == static_cast(sf::PlaybackDevice::Notification::DeviceUnlocked)); + } + + SECTION("sfPlaybackDevice_setNotificationCallback") + { + sfPlaybackDevice_setNotificationCallback([](sfPlaybackDeviceNotification, void*) {}, nullptr); + sfPlaybackDevice_setNotificationCallback(nullptr, nullptr); + } +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e613c174..332962e9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -76,6 +76,7 @@ set_target_warnings(test-csfml-network) catch_discover_tests(test-csfml-network) add_executable(test-csfml-audio + Audio/PlaybackDevice.test.cpp Audio/SoundChannel.test.cpp ) target_link_libraries(test-csfml-audio PRIVATE csfml-audio Catch2::Catch2WithMain SFML::Audio) From 917e811888ac1a0fac4b2d7b915e02e0f746acc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:36:02 +0200 Subject: [PATCH 09/15] Add IPv6 support and sfDns - Grow sfIpAddress to hold IPv6 addresses and add address types - Make sfIpAddress_fromString not resolve network names anymore, matching sf::IpAddress::fromString, and add the deprecated sfIpAddress_resolve - Add sfDns for resolving hostnames and querying NS, MX, SRV and TXT records --- include/CSFML/Network.h | 1 + include/CSFML/Network/Dns.h | 288 +++++++++++++++++++++++++ include/CSFML/Network/IpAddress.h | 184 +++++++++++++++- include/CSFML/Network/Types.h | 4 + src/CSFML/Network/CMakeLists.txt | 4 + src/CSFML/Network/ConvertIpAddress.hpp | 76 +++++++ src/CSFML/Network/Dns.cpp | 285 ++++++++++++++++++++++++ src/CSFML/Network/DnsStruct.hpp | 82 +++++++ src/CSFML/Network/Ftp.cpp | 3 +- src/CSFML/Network/IpAddress.cpp | 127 +++++++++-- src/CSFML/Network/TcpListener.cpp | 3 +- src/CSFML/Network/TcpSocket.cpp | 5 +- src/CSFML/Network/UdpSocket.cpp | 11 +- test/CMakeLists.txt | 3 +- test/Network/Dns.test.cpp | 25 +++ test/Network/IpAddress.test.cpp | 51 ++++- 16 files changed, 1108 insertions(+), 44 deletions(-) create mode 100644 include/CSFML/Network/Dns.h create mode 100644 src/CSFML/Network/ConvertIpAddress.hpp create mode 100644 src/CSFML/Network/Dns.cpp create mode 100644 src/CSFML/Network/DnsStruct.hpp create mode 100644 test/Network/Dns.test.cpp diff --git a/include/CSFML/Network.h b/include/CSFML/Network.h index 31ce8b7c..ddc55e0a 100644 --- a/include/CSFML/Network.h +++ b/include/CSFML/Network.h @@ -28,6 +28,7 @@ // Headers //////////////////////////////////////////////////////////// +#include #include #include #include diff --git a/include/CSFML/Network/Dns.h b/include/CSFML/Network/Dns.h new file mode 100644 index 00000000..fe6d93a1 --- /dev/null +++ b/include/CSFML/Network/Dns.h @@ -0,0 +1,288 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include +#include +#include + +#include +#include + + +//////////////////////////////////////////////////////////// +/// \brief A DNS MX record +/// +//////////////////////////////////////////////////////////// +typedef struct +{ + const char* exchange; ///< Host willing to act as mail exchange + uint16_t preference; ///< Preference of this record among others, lower values are preferred +} sfDnsMxRecord; + +//////////////////////////////////////////////////////////// +/// \brief A DNS SRV record +/// +//////////////////////////////////////////////////////////// +typedef struct +{ + const char* target; ///< The domain name of the target host + uint16_t port; ///< The port on the target host of the service + uint16_t weight; ///< Server selection mechanism, larger weights should be given a proportionately higher probability of being selected + uint16_t priority; ///< The priority of the target host, a client must attempt to contact the target host with the lowest-numbered priority it can reach +} sfDnsSrvRecord; + + +//////////////////////////////////////////////////////////// +/// \brief Resolve a hostname into a list of IP addresses +/// +/// The returned array must be freed with sfFree. +/// +/// \param hostname Hostname to resolve, encoded in UTF-8 +/// \param servers The list of servers to query, NULL to use the default servers +/// \param serverCount Number of servers in \a servers +/// \param timeout Query timeout if using a provided list of servers, use 0 to wait forever +/// \param count Pointer to a variable that will be filled with the number of addresses +/// +/// \return Array of IP addresses the given hostname resolves to, NULL if name resolution fails or no address was found +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfIpAddress* sfDns_resolve( + const char* hostname, + const sfIpAddress* servers, + size_t serverCount, + sfTime timeout, + size_t* count); + +//////////////////////////////////////////////////////////// +/// \brief Query NS records for a hostname +/// +/// \param hostname Hostname to query NS records for, encoded in UTF-8 +/// \param servers The list of servers to query, NULL to use the default servers +/// \param serverCount Number of servers in \a servers +/// \param timeout Query timeout if using a provided list of servers, use 0 to wait forever +/// +/// \return New list of NS records, which can be empty +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfDnsNsRecords* sfDns_queryNs(const char* hostname, const sfIpAddress* servers, size_t serverCount, sfTime timeout); + +//////////////////////////////////////////////////////////// +/// \brief Query MX records for a hostname +/// +/// \param hostname Hostname to query MX records for, encoded in UTF-8 +/// \param servers The list of servers to query, NULL to use the default servers +/// \param serverCount Number of servers in \a servers +/// \param timeout Query timeout if using a provided list of servers, use 0 to wait forever +/// +/// \return New list of MX records, which can be empty +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfDnsMxRecords* sfDns_queryMx(const char* hostname, const sfIpAddress* servers, size_t serverCount, sfTime timeout); + +//////////////////////////////////////////////////////////// +/// \brief Query SRV records for a hostname +/// +/// \param hostname Hostname to query SRV records for, encoded in UTF-8 +/// \param servers The list of servers to query, NULL to use the default servers +/// \param serverCount Number of servers in \a servers +/// \param timeout Query timeout if using a provided list of servers, use 0 to wait forever +/// +/// \return New list of SRV records, which can be empty +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfDnsSrvRecords* sfDns_querySrv(const char* hostname, const sfIpAddress* servers, size_t serverCount, sfTime timeout); + +//////////////////////////////////////////////////////////// +/// \brief Query TXT records for a hostname +/// +/// \param hostname Hostname to query TXT records for, encoded in UTF-8 +/// \param servers The list of servers to query, NULL to use the default servers +/// \param serverCount Number of servers in \a servers +/// \param timeout Query timeout if using a provided list of servers, use 0 to wait forever +/// +/// \return New list of TXT records, which can be empty +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfDnsTxtRecords* sfDns_queryTxt(const char* hostname, const sfIpAddress* servers, size_t serverCount, sfTime timeout); + +//////////////////////////////////////////////////////////// +/// \brief Get the computer's public address via DNS +/// +/// The public address is the address of the computer from the +/// point of view of the internet, i.e. something like 89.54.1.169 +/// or 2600:1901:0:13e0::1 as opposed to a private or local address +/// like 192.168.1.56 or fe80::1234:5678:9abc. +/// +/// This function depends on both your network connection and +/// the server, and may be very slow. You should try to use it +/// as little as possible. +/// +/// \param timeout Maximum time to wait, use 0 to wait forever +/// \param type The type of public address to get +/// +/// \return Public IP address of the computer on success, sfIpAddress_None otherwise +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfIpAddress sfDns_getPublicAddress(sfTime timeout, sfIpAddressType type); + +//////////////////////////////////////////////////////////// +/// \brief Destroy a list of NS records +/// +/// \param records List of NS records to destroy +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API void sfDnsNsRecords_destroy(const sfDnsNsRecords* records); + +//////////////////////////////////////////////////////////// +/// \brief Get the number of NS records in a list +/// +/// \param records List of NS records +/// +/// \return Number of NS records +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API size_t sfDnsNsRecords_getCount(const sfDnsNsRecords* records); + +//////////////////////////////////////////////////////////// +/// \brief Get a NS record of a list +/// +/// \param records List of NS records +/// \param index Index of the NS record to get +/// +/// \return The NS record, encoded in UTF-8 +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const char* sfDnsNsRecords_getRecord(const sfDnsNsRecords* records, size_t index); + +//////////////////////////////////////////////////////////// +/// \brief Destroy a list of MX records +/// +/// \param records List of MX records to destroy +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API void sfDnsMxRecords_destroy(const sfDnsMxRecords* records); + +//////////////////////////////////////////////////////////// +/// \brief Get the number of MX records in a list +/// +/// \param records List of MX records +/// +/// \return Number of MX records +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API size_t sfDnsMxRecords_getCount(const sfDnsMxRecords* records); + +//////////////////////////////////////////////////////////// +/// \brief Get a MX record of a list +/// +/// The strings of the record stay valid until the list +/// is destroyed. +/// +/// \param records List of MX records +/// \param index Index of the MX record to get +/// +/// \return The MX record +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfDnsMxRecord sfDnsMxRecords_getRecord(const sfDnsMxRecords* records, size_t index); + +//////////////////////////////////////////////////////////// +/// \brief Destroy a list of SRV records +/// +/// \param records List of SRV records to destroy +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API void sfDnsSrvRecords_destroy(const sfDnsSrvRecords* records); + +//////////////////////////////////////////////////////////// +/// \brief Get the number of SRV records in a list +/// +/// \param records List of SRV records +/// +/// \return Number of SRV records +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API size_t sfDnsSrvRecords_getCount(const sfDnsSrvRecords* records); + +//////////////////////////////////////////////////////////// +/// \brief Get a SRV record of a list +/// +/// The strings of the record stay valid until the list +/// is destroyed. +/// +/// \param records List of SRV records +/// \param index Index of the SRV record to get +/// +/// \return The SRV record +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfDnsSrvRecord sfDnsSrvRecords_getRecord(const sfDnsSrvRecords* records, size_t index); + +//////////////////////////////////////////////////////////// +/// \brief Destroy a list of TXT records +/// +/// \param records List of TXT records to destroy +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API void sfDnsTxtRecords_destroy(const sfDnsTxtRecords* records); + +//////////////////////////////////////////////////////////// +/// \brief Get the number of TXT records in a list +/// +/// \param records List of TXT records +/// +/// \return Number of TXT records +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API size_t sfDnsTxtRecords_getCount(const sfDnsTxtRecords* records); + +//////////////////////////////////////////////////////////// +/// \brief Get the number of strings in a TXT record +/// +/// \param records List of TXT records +/// \param index Index of the TXT record +/// +/// \return Number of strings in the TXT record +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API size_t sfDnsTxtRecords_getStringCount(const sfDnsTxtRecords* records, size_t index); + +//////////////////////////////////////////////////////////// +/// \brief Get a string of a TXT record +/// +/// \param records List of TXT records +/// \param index Index of the TXT record +/// \param stringIndex Index of the string within the TXT record +/// +/// \return The string, encoded in UTF-8 +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const char* sfDnsTxtRecords_getString(const sfDnsTxtRecords* records, size_t index, size_t stringIndex); diff --git a/include/CSFML/Network/IpAddress.h b/include/CSFML/Network/IpAddress.h index 4b6eefca..1bab7998 100644 --- a/include/CSFML/Network/IpAddress.h +++ b/include/CSFML/Network/IpAddress.h @@ -31,17 +31,34 @@ #include +#include +#include + //////////////////////////////////////////////////////////// -/// \brief Encapsulate an IPv4 network address +/// \brief Encapsulate an IPv4 or IPv6 network address +/// +/// The address is stored as its string representation, +/// which is large enough to hold any IPv6 address. /// //////////////////////////////////////////////////////////// typedef struct { - char address[16]; + char address[46]; } sfIpAddress; +//////////////////////////////////////////////////////////// +/// \brief Type of an IP address +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfIpAddressV4, ///< IPv4 address + sfIpAddressV6 ///< IPv6 address +} sfIpAddressType; + + //////////////////////////////////////////////////////////// /// \brief Empty object that represents invalid addresses /// @@ -49,35 +66,84 @@ typedef struct CSFML_NETWORK_API const sfIpAddress sfIpAddress_None; //////////////////////////////////////////////////////////// -/// \brief Value representing any address (0.0.0.0) +/// \brief The same as sfIpAddress_AnyV4 /// //////////////////////////////////////////////////////////// CSFML_NETWORK_API const sfIpAddress sfIpAddress_Any; //////////////////////////////////////////////////////////// -/// \brief Local host IP address (127.0.0.1, or "localhost") +/// \brief The same as sfIpAddress_LocalHostV4 /// //////////////////////////////////////////////////////////// CSFML_NETWORK_API const sfIpAddress sfIpAddress_LocalHost; //////////////////////////////////////////////////////////// -/// \brief UDP broadcast address (255.255.255.255) +/// \brief The same as sfIpAddress_BroadcastV4 /// //////////////////////////////////////////////////////////// CSFML_NETWORK_API const sfIpAddress sfIpAddress_Broadcast; //////////////////////////////////////////////////////////// -/// \brief Create an address from a string +/// \brief Value representing any IPv4 address (0.0.0.0) +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const sfIpAddress sfIpAddress_AnyV4; + +//////////////////////////////////////////////////////////// +/// \brief Local host IPv4 address (127.0.0.1) +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const sfIpAddress sfIpAddress_LocalHostV4; + +//////////////////////////////////////////////////////////// +/// \brief UDP broadcast IPv4 address (255.255.255.255) +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const sfIpAddress sfIpAddress_BroadcastV4; + +//////////////////////////////////////////////////////////// +/// \brief Value representing any IPv6 address (::) +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const sfIpAddress sfIpAddress_AnyV6; + +//////////////////////////////////////////////////////////// +/// \brief Local host IPv6 address (::1) +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const sfIpAddress sfIpAddress_LocalHostV6; + +//////////////////////////////////////////////////////////// +/// \brief Create an address from a string representation +/// +/// Here \a address can be either an IPv4 address in +/// dotted-decimal notation (ex: "192.168.1.56") or an IPv6 +/// address in standard notation (ex: "2606:4700:4700::1111"). +/// Network names are not resolved, use sfDns_resolve or +/// sfIpAddress_resolve for that. +/// +/// \param address IP address string +/// +/// \return Resulting address or sfIpAddress_None if the string is not a valid address +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfIpAddress sfIpAddress_fromString(const char* address); + +//////////////////////////////////////////////////////////// +/// \brief Create an IPv4 address from a string or by resolving a network name /// /// Here \a address can be either a decimal address /// (ex: "192.168.1.56") or a network name (ex: "localhost"). +/// Only IPv4 addresses are returned. /// /// \param address IP address or network name /// -/// \return Resulting address +/// \return Resulting address or sfIpAddress_None if the address could not be resolved +/// +/// \deprecated Use sfDns_resolve instead /// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API sfIpAddress sfIpAddress_fromString(const char* address); +CSFML_NETWORK_API CSFML_DEPRECATED sfIpAddress sfIpAddress_resolve(const char* address); //////////////////////////////////////////////////////////// /// \brief Create an address from 4 bytes @@ -112,12 +178,24 @@ CSFML_NETWORK_API sfIpAddress sfIpAddress_fromBytes(uint8_t byte0, uint8_t byte1 //////////////////////////////////////////////////////////// CSFML_NETWORK_API sfIpAddress sfIpAddress_fromInteger(uint32_t address); +//////////////////////////////////////////////////////////// +/// \brief Create an IPv6 address from 16 bytes +/// +/// \param bytes Array of 16 bytes containing the address +/// +/// \return Resulting address +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfIpAddress sfIpAddress_fromV6Bytes(const uint8_t bytes[16]); + //////////////////////////////////////////////////////////// /// \brief Get a string representation of an address /// /// The returned string is the decimal representation of the -/// IP address (like "192.168.1.56"), even if it was constructed -/// from a host name. +/// IPv4 address (like "192.168.1.56") or the standard +/// representation of the IPv6 address (like "2606:4700:4700::1111"), +/// even if it was constructed from a host name. +/// The string must be able to hold at least 46 characters. /// /// \param address Address object /// \param string String where the string representation will be stored @@ -134,6 +212,9 @@ CSFML_NETWORK_API void sfIpAddress_toString(sfIpAddress address, char* string); /// The integer produced by this function can then be converted /// back to a sfIpAddress with sfIpAddress_fromInteger. /// +/// Only IPv4 addresses can be converted to an integer, 0 is +/// returned for IPv6 addresses. +/// /// \param address Address object /// /// \return 32-bits unsigned integer representation of the address @@ -141,6 +222,50 @@ CSFML_NETWORK_API void sfIpAddress_toString(sfIpAddress address, char* string); //////////////////////////////////////////////////////////// CSFML_NETWORK_API uint32_t sfIpAddress_toInteger(sfIpAddress address); +//////////////////////////////////////////////////////////// +/// \brief Get a 16-byte representation of an IPv6 address +/// +/// The bytes produced by this function can then be converted +/// back to a sfIpAddress with sfIpAddress_fromV6Bytes. +/// +/// \param address Address object +/// \param bytes Array of 16 bytes that will be filled with the address +/// +/// \return True if the address is a valid IPv6 address and the bytes have been written, false otherwise +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API bool sfIpAddress_toV6Bytes(sfIpAddress address, uint8_t bytes[16]); + +//////////////////////////////////////////////////////////// +/// \brief Get the type of an address +/// +/// \param address Address object +/// +/// \return The type of the address, sfIpAddressV4 is returned for invalid addresses +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfIpAddressType sfIpAddress_getType(sfIpAddress address); + +//////////////////////////////////////////////////////////// +/// \brief Check if an address is a valid IPv4 address +/// +/// \param address Address object +/// +/// \return True if the address is a valid IPv4 address +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API bool sfIpAddress_isV4(sfIpAddress address); + +//////////////////////////////////////////////////////////// +/// \brief Check if an address is a valid IPv6 address +/// +/// \param address Address object +/// +/// \return True if the address is a valid IPv6 address +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API bool sfIpAddress_isV6(sfIpAddress address); + //////////////////////////////////////////////////////////// /// \brief Get the computer's local address /// @@ -150,11 +275,28 @@ CSFML_NETWORK_API uint32_t sfIpAddress_toInteger(sfIpAddress address); /// Unlike sfIpAddress_getPublicAddress, this function is fast /// and may be used safely anywhere. /// +/// This returns the local IPv4 address. +/// /// \return Local IP address of the computer /// //////////////////////////////////////////////////////////// CSFML_NETWORK_API sfIpAddress sfIpAddress_getLocalAddress(void); +//////////////////////////////////////////////////////////// +/// \brief Get the computer's local address of a given type +/// +/// The local address is the address of the computer from the +/// LAN point of view, i.e. something like 192.168.1.56 or +/// fe80::1234:5678:9abc. It is meaningful only for +/// communications over the local network. +/// +/// \param type The type of local address to get +/// +/// \return Local IP address of the computer or sfIpAddress_None if there is none +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfIpAddress sfIpAddress_getLocalAddressOfType(sfIpAddressType type); + //////////////////////////////////////////////////////////// /// \brief Get the computer's public address /// @@ -170,9 +312,31 @@ CSFML_NETWORK_API sfIpAddress sfIpAddress_getLocalAddress(void); /// to be possibly stuck waiting in case there is a problem; use /// 0 to deactivate this limit. /// +/// This returns the public IPv4 address. +/// /// \param timeout Maximum time to wait /// /// \return Public IP address of the computer /// //////////////////////////////////////////////////////////// CSFML_NETWORK_API sfIpAddress sfIpAddress_getPublicAddress(sfTime timeout); + +//////////////////////////////////////////////////////////// +/// \brief Get the computer's public address of a given type +/// +/// The public address is the address of the computer from the +/// internet point of view, i.e. something like 89.54.1.169 or +/// 2600:1901:0:13e0::1. See sfIpAddress_getPublicAddress for +/// details. +/// +/// If tamper resistance is required, setting \a secure to true +/// will make use of verified HTTPS connections to get the address. +/// +/// \param timeout Maximum time to wait, use 0 to deactivate the limit +/// \param type The type of public address to get, NULL to specify no preference +/// \param secure True to retrieve the public address via a secure HTTPS connection, false to retrieve via DNS or an insecure connection +/// +/// \return Public IP address of the computer or sfIpAddress_None on failure +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfIpAddress sfIpAddress_getPublicAddressOfType(sfTime timeout, const sfIpAddressType* type, bool secure); diff --git a/include/CSFML/Network/Types.h b/include/CSFML/Network/Types.h index e607e3f9..0e6636db 100644 --- a/include/CSFML/Network/Types.h +++ b/include/CSFML/Network/Types.h @@ -25,6 +25,10 @@ #pragma once +typedef struct sfDnsMxRecords sfDnsMxRecords; +typedef struct sfDnsNsRecords sfDnsNsRecords; +typedef struct sfDnsSrvRecords sfDnsSrvRecords; +typedef struct sfDnsTxtRecords sfDnsTxtRecords; typedef struct sfFtpDirectoryResponse sfFtpDirectoryResponse; typedef struct sfFtpListingResponse sfFtpListingResponse; typedef struct sfFtpResponse sfFtpResponse; diff --git a/src/CSFML/Network/CMakeLists.txt b/src/CSFML/Network/CMakeLists.txt index c24caf4b..f9fa582d 100644 --- a/src/CSFML/Network/CMakeLists.txt +++ b/src/CSFML/Network/CMakeLists.txt @@ -3,6 +3,10 @@ set(SRCROOT ${PROJECT_SOURCE_DIR}/src/CSFML/Network) # all source files set(SRC + ${SRCROOT}/ConvertIpAddress.hpp + ${SRCROOT}/Dns.cpp + ${SRCROOT}/DnsStruct.hpp + ${INCROOT}/Dns.h ${INCROOT}/Export.h ${SRCROOT}/Ftp.cpp ${SRCROOT}/FtpStruct.hpp diff --git a/src/CSFML/Network/ConvertIpAddress.hpp b/src/CSFML/Network/ConvertIpAddress.hpp new file mode 100644 index 00000000..49987518 --- /dev/null +++ b/src/CSFML/Network/ConvertIpAddress.hpp @@ -0,0 +1,76 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include + +#include +#include + + +//////////////////////////////////////////////////////////// +// Convert sf::IpAddress to sfIpAddress +//////////////////////////////////////////////////////////// +[[nodiscard]] inline sfIpAddress convertIpAddress(const sf::IpAddress& address) +{ + sfIpAddress result{}; + std::strncpy(result.address, address.toString().c_str(), sizeof(result.address) - 1); + return result; +} + + +//////////////////////////////////////////////////////////// +// Convert std::optional to sfIpAddress +//////////////////////////////////////////////////////////// +[[nodiscard]] inline sfIpAddress convertIpAddress(const std::optional& address) +{ + return address ? convertIpAddress(*address) : sfIpAddress_None; +} + + +//////////////////////////////////////////////////////////// +// Convert sfIpAddress to std::optional +//////////////////////////////////////////////////////////// +[[nodiscard]] inline std::optional convertIpAddress(const sfIpAddress& address) +{ + return sf::IpAddress::fromString(address.address); +} + + +//////////////////////////////////////////////////////////// +// Convert an optional sfIpAddressType to std::optional +//////////////////////////////////////////////////////////// +[[nodiscard]] inline std::optional convertIpAddressType(const sfIpAddressType* type) +{ + if (!type) + return std::nullopt; + + return static_cast(*type); +} diff --git a/src/CSFML/Network/Dns.cpp b/src/CSFML/Network/Dns.cpp new file mode 100644 index 00000000..fb834d89 --- /dev/null +++ b/src/CSFML/Network/Dns.cpp @@ -0,0 +1,285 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include +#include +#include + +#include +#include + +#include +#include +#include + + +namespace +{ +//////////////////////////////////////////////////////////// +[[nodiscard]] sf::String fromUtf8(const char* string) +{ + return sf::String::fromUtf8(string, string + std::strlen(string)); +} + + +//////////////////////////////////////////////////////////// +[[nodiscard]] std::string toUtf8(const sf::String& string) +{ + const auto utf8 = string.toUtf8(); + return {utf8.begin(), utf8.end()}; +} + + +//////////////////////////////////////////////////////////// +[[nodiscard]] std::vector convertServers(const sfIpAddress* servers, size_t serverCount) +{ + std::vector result; + + if (!servers) + return result; + + result.reserve(serverCount); + for (size_t i = 0; i < serverCount; ++i) + { + if (const auto address = convertIpAddress(servers[i])) + result.push_back(*address); + } + + return result; +} + + +//////////////////////////////////////////////////////////// +[[nodiscard]] std::optional convertTimeout(sfTime timeout) +{ + if (timeout.microseconds == 0) + return std::nullopt; + + return sf::microseconds(timeout.microseconds); +} +} // namespace + + +//////////////////////////////////////////////////////////// +sfIpAddress* sfDns_resolve(const char* hostname, const sfIpAddress* servers, size_t serverCount, sfTime timeout, size_t* count) +{ + assert(hostname); + assert(count); + + *count = 0; + + const auto addresses = sf::Dns::resolve(fromUtf8(hostname), convertServers(servers, serverCount), convertTimeout(timeout)); + if (!addresses || addresses->empty()) + return nullptr; + + auto* result = static_cast(std::malloc(addresses->size() * sizeof(sfIpAddress))); + if (!result) + return nullptr; + + for (std::size_t i = 0; i < addresses->size(); ++i) + result[i] = convertIpAddress((*addresses)[i]); + + *count = addresses->size(); + return result; +} + + +//////////////////////////////////////////////////////////// +sfDnsNsRecords* sfDns_queryNs(const char* hostname, const sfIpAddress* servers, size_t serverCount, sfTime timeout) +{ + assert(hostname); + + auto* records = new sfDnsNsRecords; + for (const auto& record : + sf::Dns::queryNs(fromUtf8(hostname), convertServers(servers, serverCount), convertTimeout(timeout))) + records->Records.push_back(toUtf8(record)); + + return records; +} + + +//////////////////////////////////////////////////////////// +sfDnsMxRecords* sfDns_queryMx(const char* hostname, const sfIpAddress* servers, size_t serverCount, sfTime timeout) +{ + assert(hostname); + + auto* records = new sfDnsMxRecords; + for (const auto& record : + sf::Dns::queryMx(fromUtf8(hostname), convertServers(servers, serverCount), convertTimeout(timeout))) + records->Records.push_back({toUtf8(record.exchange), record.preference}); + + return records; +} + + +//////////////////////////////////////////////////////////// +sfDnsSrvRecords* sfDns_querySrv(const char* hostname, const sfIpAddress* servers, size_t serverCount, sfTime timeout) +{ + assert(hostname); + + auto* records = new sfDnsSrvRecords; + for (const auto& record : + sf::Dns::querySrv(fromUtf8(hostname), convertServers(servers, serverCount), convertTimeout(timeout))) + records->Records.push_back({toUtf8(record.target), record.port, record.weight, record.priority}); + + return records; +} + + +//////////////////////////////////////////////////////////// +sfDnsTxtRecords* sfDns_queryTxt(const char* hostname, const sfIpAddress* servers, size_t serverCount, sfTime timeout) +{ + assert(hostname); + + auto* records = new sfDnsTxtRecords; + for (const auto& record : + sf::Dns::queryTxt(fromUtf8(hostname), convertServers(servers, serverCount), convertTimeout(timeout))) + { + auto& strings = records->Records.emplace_back(); + for (const auto& string : record) + strings.push_back(toUtf8(string)); + } + + return records; +} + + +//////////////////////////////////////////////////////////// +sfIpAddress sfDns_getPublicAddress(sfTime timeout, sfIpAddressType type) +{ + return convertIpAddress(sf::Dns::getPublicAddress(convertTimeout(timeout), static_cast(type))); +} + + +//////////////////////////////////////////////////////////// +void sfDnsNsRecords_destroy(const sfDnsNsRecords* records) +{ + delete records; +} + + +//////////////////////////////////////////////////////////// +size_t sfDnsNsRecords_getCount(const sfDnsNsRecords* records) +{ + assert(records); + return records->Records.size(); +} + + +//////////////////////////////////////////////////////////// +const char* sfDnsNsRecords_getRecord(const sfDnsNsRecords* records, size_t index) +{ + assert(records); + assert(index < records->Records.size()); + return records->Records[index].c_str(); +} + + +//////////////////////////////////////////////////////////// +void sfDnsMxRecords_destroy(const sfDnsMxRecords* records) +{ + delete records; +} + + +//////////////////////////////////////////////////////////// +size_t sfDnsMxRecords_getCount(const sfDnsMxRecords* records) +{ + assert(records); + return records->Records.size(); +} + + +//////////////////////////////////////////////////////////// +sfDnsMxRecord sfDnsMxRecords_getRecord(const sfDnsMxRecords* records, size_t index) +{ + assert(records); + assert(index < records->Records.size()); + + const auto& record = records->Records[index]; + return {record.exchange.c_str(), record.preference}; +} + + +//////////////////////////////////////////////////////////// +void sfDnsSrvRecords_destroy(const sfDnsSrvRecords* records) +{ + delete records; +} + + +//////////////////////////////////////////////////////////// +size_t sfDnsSrvRecords_getCount(const sfDnsSrvRecords* records) +{ + assert(records); + return records->Records.size(); +} + + +//////////////////////////////////////////////////////////// +sfDnsSrvRecord sfDnsSrvRecords_getRecord(const sfDnsSrvRecords* records, size_t index) +{ + assert(records); + assert(index < records->Records.size()); + + const auto& record = records->Records[index]; + return {record.target.c_str(), record.port, record.weight, record.priority}; +} + + +//////////////////////////////////////////////////////////// +void sfDnsTxtRecords_destroy(const sfDnsTxtRecords* records) +{ + delete records; +} + + +//////////////////////////////////////////////////////////// +size_t sfDnsTxtRecords_getCount(const sfDnsTxtRecords* records) +{ + assert(records); + return records->Records.size(); +} + + +//////////////////////////////////////////////////////////// +size_t sfDnsTxtRecords_getStringCount(const sfDnsTxtRecords* records, size_t index) +{ + assert(records); + assert(index < records->Records.size()); + return records->Records[index].size(); +} + + +//////////////////////////////////////////////////////////// +const char* sfDnsTxtRecords_getString(const sfDnsTxtRecords* records, size_t index, size_t stringIndex) +{ + assert(records); + assert(index < records->Records.size()); + assert(stringIndex < records->Records[index].size()); + return records->Records[index][stringIndex].c_str(); +} diff --git a/src/CSFML/Network/DnsStruct.hpp b/src/CSFML/Network/DnsStruct.hpp new file mode 100644 index 00000000..31977b21 --- /dev/null +++ b/src/CSFML/Network/DnsStruct.hpp @@ -0,0 +1,82 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include +#include +#include + + +//////////////////////////////////////////////////////////// +// Internal structure of sfDnsNsRecords +//////////////////////////////////////////////////////////// +struct sfDnsNsRecords +{ + std::vector Records; +}; + + +//////////////////////////////////////////////////////////// +// Internal structure of sfDnsMxRecords +//////////////////////////////////////////////////////////// +struct sfDnsMxRecords +{ + struct Record + { + std::string exchange; + std::uint16_t preference{}; + }; + + std::vector Records; +}; + + +//////////////////////////////////////////////////////////// +// Internal structure of sfDnsSrvRecords +//////////////////////////////////////////////////////////// +struct sfDnsSrvRecords +{ + struct Record + { + std::string target; + std::uint16_t port{}; + std::uint16_t weight{}; + std::uint16_t priority{}; + }; + + std::vector Records; +}; + + +//////////////////////////////////////////////////////////// +// Internal structure of sfDnsTxtRecords +//////////////////////////////////////////////////////////// +struct sfDnsTxtRecords +{ + std::vector> Records; +}; diff --git a/src/CSFML/Network/Ftp.cpp b/src/CSFML/Network/Ftp.cpp index 0cf64026..c255be5b 100644 --- a/src/CSFML/Network/Ftp.cpp +++ b/src/CSFML/Network/Ftp.cpp @@ -25,6 +25,7 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// +#include #include #include @@ -205,7 +206,7 @@ sfFtpResponse* sfFtp_connect(sfFtp* ftp, sfIpAddress server, unsigned short port { assert(ftp); - std::optional sfmlServer = sf::IpAddress::resolve(server.address); + std::optional sfmlServer = convertIpAddress(server); if (!sfmlServer) return nullptr; diff --git a/src/CSFML/Network/IpAddress.cpp b/src/CSFML/Network/IpAddress.cpp index d48a66dc..f5c80dc3 100644 --- a/src/CSFML/Network/IpAddress.cpp +++ b/src/CSFML/Network/IpAddress.cpp @@ -25,63 +25,90 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// +#include #include #include +#include +#include #include -namespace -{ -// Helper function for converting a SFML address to a CSFML one -[[nodiscard]] sfIpAddress fromSFMLAddress(std::optional address) -{ - if (!address) - return sfIpAddress_None; +//////////////////////////////////////////////////////////// +const sfIpAddress sfIpAddress_None = {{0}}; - sfIpAddress result{}; - std::strncpy(result.address, address->toString().c_str(), 15); - return result; -} -} // namespace + +//////////////////////////////////////////////////////////// +const sfIpAddress sfIpAddress_Any = {"0.0.0.0"}; //////////////////////////////////////////////////////////// -const sfIpAddress sfIpAddress_None = {{0}}; +const sfIpAddress sfIpAddress_LocalHost = {"127.0.0.1"}; //////////////////////////////////////////////////////////// -const sfIpAddress sfIpAddress_Any = sfIpAddress_fromBytes(0, 0, 0, 0); +const sfIpAddress sfIpAddress_Broadcast = {"255.255.255.255"}; //////////////////////////////////////////////////////////// -const sfIpAddress sfIpAddress_LocalHost = sfIpAddress_fromBytes(127, 0, 0, 1); +const sfIpAddress sfIpAddress_AnyV4 = {"0.0.0.0"}; //////////////////////////////////////////////////////////// -const sfIpAddress sfIpAddress_Broadcast = sfIpAddress_fromBytes(255, 255, 255, 255); +const sfIpAddress sfIpAddress_LocalHostV4 = {"127.0.0.1"}; + + +//////////////////////////////////////////////////////////// +const sfIpAddress sfIpAddress_BroadcastV4 = {"255.255.255.255"}; + + +//////////////////////////////////////////////////////////// +const sfIpAddress sfIpAddress_AnyV6 = {"::"}; + + +//////////////////////////////////////////////////////////// +const sfIpAddress sfIpAddress_LocalHostV6 = {"::1"}; //////////////////////////////////////////////////////////// sfIpAddress sfIpAddress_fromString(const char* address) { assert(address); - return fromSFMLAddress(sf::IpAddress::resolve(address)); + return convertIpAddress(sf::IpAddress::fromString(address)); +} + + +//////////////////////////////////////////////////////////// +sfIpAddress sfIpAddress_resolve(const char* address) +{ + assert(address); + return convertIpAddress(sf::IpAddress::resolve(address)); } //////////////////////////////////////////////////////////// sfIpAddress sfIpAddress_fromBytes(uint8_t byte0, uint8_t byte1, uint8_t byte2, uint8_t byte3) { - return fromSFMLAddress(sf::IpAddress(byte0, byte1, byte2, byte3)); + return convertIpAddress(sf::IpAddress(byte0, byte1, byte2, byte3)); } //////////////////////////////////////////////////////////// sfIpAddress sfIpAddress_fromInteger(uint32_t address) { - return fromSFMLAddress(sf::IpAddress(address)); + return convertIpAddress(sf::IpAddress(address)); +} + + +//////////////////////////////////////////////////////////// +sfIpAddress sfIpAddress_fromV6Bytes(const uint8_t bytes[16]) +{ + assert(bytes); + + std::array array{}; + std::copy(bytes, bytes + array.size(), array.begin()); + return convertIpAddress(sf::IpAddress(array)); } @@ -96,20 +123,74 @@ void sfIpAddress_toString(sfIpAddress address, char* string) //////////////////////////////////////////////////////////// uint32_t sfIpAddress_toInteger(sfIpAddress address) { - const auto sfmlAddress = sf::IpAddress::resolve(address.address); - return sfmlAddress ? sfmlAddress->toInteger() : 0; + const auto sfmlAddress = convertIpAddress(address); + return sfmlAddress && sfmlAddress->isV4() ? sfmlAddress->toInteger() : 0; +} + + +//////////////////////////////////////////////////////////// +bool sfIpAddress_toV6Bytes(sfIpAddress address, uint8_t bytes[16]) +{ + assert(bytes); + + const auto sfmlAddress = convertIpAddress(address); + if (!sfmlAddress || !sfmlAddress->isV6()) + return false; + + const auto array = sfmlAddress->toBytes(); + std::copy(array.begin(), array.end(), bytes); + return true; +} + + +//////////////////////////////////////////////////////////// +sfIpAddressType sfIpAddress_getType(sfIpAddress address) +{ + const auto sfmlAddress = convertIpAddress(address); + return sfmlAddress ? static_cast(sfmlAddress->getType()) : sfIpAddressV4; +} + + +//////////////////////////////////////////////////////////// +bool sfIpAddress_isV4(sfIpAddress address) +{ + const auto sfmlAddress = convertIpAddress(address); + return sfmlAddress && sfmlAddress->isV4(); +} + + +//////////////////////////////////////////////////////////// +bool sfIpAddress_isV6(sfIpAddress address) +{ + const auto sfmlAddress = convertIpAddress(address); + return sfmlAddress && sfmlAddress->isV6(); } //////////////////////////////////////////////////////////// sfIpAddress sfIpAddress_getLocalAddress() { - return fromSFMLAddress(sf::IpAddress::getLocalAddress()); + return convertIpAddress(sf::IpAddress::getLocalAddress()); +} + + +//////////////////////////////////////////////////////////// +sfIpAddress sfIpAddress_getLocalAddressOfType(sfIpAddressType type) +{ + return convertIpAddress(sf::IpAddress::getLocalAddress(static_cast(type))); } //////////////////////////////////////////////////////////// sfIpAddress sfIpAddress_getPublicAddress(sfTime timeout) { - return fromSFMLAddress(sf::IpAddress::getPublicAddress(sf::microseconds(timeout.microseconds))); + return convertIpAddress(sf::IpAddress::getPublicAddress(sf::microseconds(timeout.microseconds))); +} + + +//////////////////////////////////////////////////////////// +sfIpAddress sfIpAddress_getPublicAddressOfType(sfTime timeout, const sfIpAddressType* type, bool secure) +{ + return convertIpAddress( + sf::IpAddress::getPublicAddress(sf::microseconds(timeout.microseconds), convertIpAddressType(type), secure)); } diff --git a/src/CSFML/Network/TcpListener.cpp b/src/CSFML/Network/TcpListener.cpp index a1d0318d..195846a4 100644 --- a/src/CSFML/Network/TcpListener.cpp +++ b/src/CSFML/Network/TcpListener.cpp @@ -25,6 +25,7 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// +#include #include #include #include @@ -75,7 +76,7 @@ sfSocketStatus sfTcpListener_listen(sfTcpListener* listener, unsigned short port { assert(listener); - std::optional sfmlAddress = sf::IpAddress::resolve(address.address); + std::optional sfmlAddress = convertIpAddress(address); if (!sfmlAddress) { diff --git a/src/CSFML/Network/TcpSocket.cpp b/src/CSFML/Network/TcpSocket.cpp index 599a83b7..5308533b 100644 --- a/src/CSFML/Network/TcpSocket.cpp +++ b/src/CSFML/Network/TcpSocket.cpp @@ -25,6 +25,7 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// +#include #include #include #include @@ -82,7 +83,7 @@ sfIpAddress sfTcpSocket_getRemoteAddress(const sfTcpSocket* socket) sfIpAddress result = sfIpAddress_None; if (address) { - std::strncpy(result.address, address->toString().c_str(), 15); + result = convertIpAddress(*address); } return result; @@ -100,7 +101,7 @@ unsigned short sfTcpSocket_getRemotePort(const sfTcpSocket* socket) //////////////////////////////////////////////////////////// sfSocketStatus sfTcpSocket_connect(sfTcpSocket* socket, sfIpAddress remoteAddress, unsigned short remotePort, sfTime timeout) { - std::optional address = sf::IpAddress::resolve(remoteAddress.address); + std::optional address = convertIpAddress(remoteAddress); if (!address) { diff --git a/src/CSFML/Network/UdpSocket.cpp b/src/CSFML/Network/UdpSocket.cpp index 05f1778c..e188273c 100644 --- a/src/CSFML/Network/UdpSocket.cpp +++ b/src/CSFML/Network/UdpSocket.cpp @@ -25,6 +25,7 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// +#include #include #include #include @@ -76,7 +77,7 @@ sfSocketStatus sfUdpSocket_bind(sfUdpSocket* socket, unsigned short port, sfIpAd { assert(socket); - std::optional sfmlAddress = sf::IpAddress::resolve(address.address); + std::optional sfmlAddress = convertIpAddress(address); if (!sfmlAddress) { @@ -101,7 +102,7 @@ sfSocketStatus sfUdpSocket_send(sfUdpSocket* socket, const void* data, size_t si assert(socket); // Convert the address - std::optional address = sf::IpAddress::resolve(remoteAddress.address); + std::optional address = convertIpAddress(remoteAddress); if (!address) { @@ -139,7 +140,7 @@ sfSocketStatus sfUdpSocket_receive(sfUdpSocket* socket, if (address) { - std::strncpy(remoteAddress->address, address->toString().c_str(), 15); + *remoteAddress = convertIpAddress(*address); } } @@ -157,7 +158,7 @@ sfSocketStatus sfUdpSocket_sendPacket(sfUdpSocket* socket, sfPacket* packet, sfI assert(packet); // Convert the address - std::optional address = sf::IpAddress::resolve(remoteAddress.address); + std::optional address = convertIpAddress(remoteAddress); if (!address) { @@ -187,7 +188,7 @@ sfSocketStatus sfUdpSocket_receivePacket(sfUdpSocket* socket, sfPacket* packet, if (address) { - std::strncpy(remoteAddress->address, address->toString().c_str(), 15); + *remoteAddress = convertIpAddress(*address); } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 332962e9..c944caba 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -66,12 +66,13 @@ set_target_warnings(test-csfml-graphics) catch_discover_tests(test-csfml-graphics WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) add_executable(test-csfml-network + Network/Dns.test.cpp Network/Ftp.test.cpp Network/Http.test.cpp Network/IpAddress.test.cpp Network/SocketStatus.test.cpp ) -target_link_libraries(test-csfml-network PRIVATE csfml-network Catch2::Catch2WithMain SFML::Network) +target_link_libraries(test-csfml-network PRIVATE csfml-network csfml-system Catch2::Catch2WithMain SFML::Network) set_target_warnings(test-csfml-network) catch_discover_tests(test-csfml-network) diff --git a/test/Network/Dns.test.cpp b/test/Network/Dns.test.cpp new file mode 100644 index 00000000..61bde5c2 --- /dev/null +++ b/test/Network/Dns.test.cpp @@ -0,0 +1,25 @@ +#include +#include + +#include + +#include + +TEST_CASE("[Network] sfDns") +{ + SECTION("sfDns_resolve") + { + size_t count = 0; + sfIpAddress* addresses = sfDns_resolve("127.0.0.1", nullptr, 0, sfSeconds(1), &count); + REQUIRE(addresses != nullptr); + REQUIRE(count == 1); + CHECK(std::strcmp(addresses[0].address, "127.0.0.1") == 0); + sfFree(addresses); + + addresses = sfDns_resolve("::1", nullptr, 0, sfSeconds(1), &count); + REQUIRE(addresses != nullptr); + REQUIRE(count == 1); + CHECK(std::strcmp(addresses[0].address, "::1") == 0); + sfFree(addresses); + } +} diff --git a/test/Network/IpAddress.test.cpp b/test/Network/IpAddress.test.cpp index 1becf666..99134c2a 100644 --- a/test/Network/IpAddress.test.cpp +++ b/test/Network/IpAddress.test.cpp @@ -1,5 +1,7 @@ #include +#include + #include #include @@ -17,15 +19,62 @@ TEST_CASE("[Network] sfIpAddress") CHECK(std::strcmp(sfIpAddress_Any.address, "0.0.0.0") == 0); CHECK(std::strcmp(sfIpAddress_LocalHost.address, "127.0.0.1") == 0); CHECK(std::strcmp(sfIpAddress_Broadcast.address, "255.255.255.255") == 0); + + CHECK(std::strcmp(sfIpAddress_AnyV4.address, "0.0.0.0") == 0); + CHECK(std::strcmp(sfIpAddress_LocalHostV4.address, "127.0.0.1") == 0); + CHECK(std::strcmp(sfIpAddress_BroadcastV4.address, "255.255.255.255") == 0); + CHECK(std::strcmp(sfIpAddress_AnyV6.address, "::") == 0); + CHECK(std::strcmp(sfIpAddress_LocalHostV6.address, "::1") == 0); + } + + SECTION("sfIpAddressType") + { + STATIC_CHECK(sfIpAddressV4 == static_cast(sf::IpAddress::Type::IpV4)); + STATIC_CHECK(sfIpAddressV6 == static_cast(sf::IpAddress::Type::IpV6)); } SECTION("sfIpAddress_fromString") { CHECK(sfIpAddress_toInteger(sfIpAddress_fromString("")) == 0); CHECK(sfIpAddress_toInteger(sfIpAddress_fromString("256.256.256.256")) == 0); - CHECK(sfIpAddress_toInteger(sfIpAddress_fromString("localhost")) == 0x7F000001); + CHECK(sfIpAddress_toInteger(sfIpAddress_fromString("localhost")) == 0); CHECK(sfIpAddress_toInteger(sfIpAddress_fromString("192.168.0.1")) == 0xC0A80001); CHECK(sfIpAddress_toInteger(sfIpAddress_fromString("8.8.8.8")) == 0x08080808); + CHECK(std::strcmp(sfIpAddress_fromString("2606:4700:4700::1111").address, "2606:4700:4700::1111") == 0); + CHECK(std::strcmp(sfIpAddress_fromString("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff").address, + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") == 0); + } + + SECTION("sfIpAddress_resolve") + { + CHECK(sfIpAddress_toInteger(sfIpAddress_resolve("")) == 0); + CHECK(sfIpAddress_toInteger(sfIpAddress_resolve("localhost")) == 0x7F000001); + CHECK(sfIpAddress_toInteger(sfIpAddress_resolve("192.168.0.1")) == 0xC0A80001); + } + + SECTION("Type") + { + CHECK(sfIpAddress_isV4(sfIpAddress_LocalHostV4)); + CHECK(!sfIpAddress_isV6(sfIpAddress_LocalHostV4)); + CHECK(sfIpAddress_getType(sfIpAddress_LocalHostV4) == sfIpAddressV4); + CHECK(!sfIpAddress_isV4(sfIpAddress_LocalHostV6)); + CHECK(sfIpAddress_isV6(sfIpAddress_LocalHostV6)); + CHECK(sfIpAddress_getType(sfIpAddress_LocalHostV6) == sfIpAddressV6); + CHECK(!sfIpAddress_isV4(sfIpAddress_None)); + CHECK(!sfIpAddress_isV6(sfIpAddress_None)); + } + + SECTION("sfIpAddress_fromV6Bytes") + { + const uint8_t bytes[16] = {0x26, 0x06, 0x47, 0x00, 0x47, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0x11, 0x11}; + const sfIpAddress address = sfIpAddress_fromV6Bytes(bytes); + CHECK(std::strcmp(address.address, "2606:4700:4700::1111") == 0); + CHECK(sfIpAddress_toInteger(address) == 0); + + uint8_t result[16] = {}; + CHECK(sfIpAddress_toV6Bytes(address, result)); + CHECK(std::memcmp(bytes, result, sizeof(bytes)) == 0); + CHECK(!sfIpAddress_toV6Bytes(sfIpAddress_LocalHostV4, result)); } SECTION("sfIpAddress_fromBytes") From d95f00f21253786c18ad28d4a33028d399aadf94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:40:16 +0200 Subject: [PATCH 10/15] Add transport layer security to TCP sockets --- include/CSFML/Network/TcpSocket.h | 132 ++++++++++++++++++++++++++ src/CSFML/Network/TcpSocket.cpp | 72 ++++++++++++++ src/CSFML/Network/TcpSocketStruct.hpp | 3 + test/CMakeLists.txt | 1 + test/Network/TcpSocket.test.cpp | 24 +++++ 5 files changed, 232 insertions(+) create mode 100644 test/Network/TcpSocket.test.cpp diff --git a/include/CSFML/Network/TcpSocket.h b/include/CSFML/Network/TcpSocket.h index a5d9501f..5ed4d187 100644 --- a/include/CSFML/Network/TcpSocket.h +++ b/include/CSFML/Network/TcpSocket.h @@ -34,9 +34,23 @@ #include #include +#include #include +//////////////////////////////////////////////////////////// +/// \brief Transport layer security status codes +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfTlsNotConnected, ///< TCP connection not yet connected + sfTlsHandshakeStarted, ///< TLS handshake has been started + sfTlsHandshakeComplete, ///< TLS handshake is complete, stream is encrypted + sfTlsError ///< An unexpected error happened +} sfTlsStatus; + + //////////////////////////////////////////////////////////// /// \brief Create a new TCP socket /// @@ -229,3 +243,121 @@ CSFML_NETWORK_API sfSocketStatus sfTcpSocket_sendPacket(sfTcpSocket* socket, sfP /// //////////////////////////////////////////////////////////// CSFML_NETWORK_API sfSocketStatus sfTcpSocket_receivePacket(sfTcpSocket* socket, sfPacket* packet); + +//////////////////////////////////////////////////////////// +/// \brief Set up transport layer security as a client +/// +/// Once the TCP connection is connected, transport layer +/// security can be set up. +/// +/// If this function is called before the TCP connection is +/// connected, it will return sfTlsNotConnected and must be +/// called again once the TCP connection is connected. +/// +/// If this function started TLS setup but could not finish +/// it within this call e.g. because this socket was set to +/// non-blocking, it will return sfTlsHandshakeStarted and +/// this function will have to be called repeatedly until +/// sfTlsHandshakeComplete is returned. If this socket is +/// blocking, sfTlsHandshakeComplete should be returned +/// within the same function call if TLS setup was successful. +/// +/// If sfTlsError is returned, something went wrong with TLS +/// setup and the connection must be reconnected and TLS setup +/// reattempted after it is connected again. +/// +/// If verification is enabled, this function verifies the peer +/// using the system provided certificate store. If the peer +/// does not have a certificate that was signed by a certificate +/// authority i.e. a self-signed certificate, the entire certificate +/// chain can be provided using sfTcpSocket_setupTlsClientWithCertificate. +/// +/// The hostname is sent to the server via server name indication +/// (SNI) and used to verify the certificate chain returned by the +/// server. +/// +/// \param socket TCP socket object +/// \param hostname Hostname of the remote peer, encoded in UTF-8, used for verification +/// \param verifyPeer True to enable peer verification, false to disable it +/// +/// \return TLS status code +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfTlsStatus sfTcpSocket_setupTlsClient(sfTcpSocket* socket, const char* hostname, bool verifyPeer); + +//////////////////////////////////////////////////////////// +/// \brief Set up transport layer security as a client with a given certificate chain +/// +/// When calling this function, the certificate chain to verify +/// the host with has to be provided. Verification is always +/// enabled when calling this function. +/// +/// The certificate data can be provided in PEM or DER format. +/// +/// See sfTcpSocket_setupTlsClient for details on the returned +/// status codes. +/// +/// \param socket TCP socket object +/// \param hostname Hostname of the remote peer, encoded in UTF-8, used for verification +/// \param certificateChainData Certificate chain data in PEM or DER encoding +/// \param certificateChainSize Size of the certificate chain data +/// +/// \return TLS status code +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfTlsStatus sfTcpSocket_setupTlsClientWithCertificate( + sfTcpSocket* socket, + const char* hostname, + const void* certificateChainData, + size_t certificateChainSize); + +//////////////////////////////////////////////////////////// +/// \brief Set up transport layer security as a server +/// +/// Once the TCP connection is connected, transport layer +/// security can be set up. +/// +/// As a server, a certificate chain as well as a private key +/// must be provided. The certificate and private key data can +/// be provided in PEM or DER format. If the private key is +/// secured by a password, the password must be provided. +/// +/// If sfTlsError is returned, something went wrong with TLS +/// setup and the connection must be disconnected. The client +/// must reconnect and reattempt TLS setup again. +/// +/// See sfTcpSocket_setupTlsClient for details on the other +/// returned status codes. +/// +/// \param socket TCP socket object +/// \param certificateChainData Certificate chain data in PEM or DER encoding +/// \param certificateChainSize Size of the certificate chain data +/// \param privateKeyData Private key data in PEM or DER encoding +/// \param privateKeySize Size of the private key data +/// \param privateKeyPasswordData Private key password data, can be NULL if there is no password +/// \param privateKeyPasswordSize Size of the private key password data +/// +/// \return TLS status code +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfTlsStatus sfTcpSocket_setupTlsServer( + sfTcpSocket* socket, + const void* certificateChainData, + size_t certificateChainSize, + const void* privateKeyData, + size_t privateKeySize, + const void* privateKeyPasswordData, + size_t privateKeyPasswordSize); + +//////////////////////////////////////////////////////////// +/// \brief Get the name of the TLS ciphersuite currently in use +/// +/// The returned string stays valid until the next call to +/// this function or until the socket is destroyed. +/// +/// \param socket TCP socket object +/// +/// \return TLS ciphersuite currently in use or NULL if TLS is not set up +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const char* sfTcpSocket_getCurrentCiphersuiteName(const sfTcpSocket* socket); diff --git a/src/CSFML/Network/TcpSocket.cpp b/src/CSFML/Network/TcpSocket.cpp index 5308533b..70afe8ec 100644 --- a/src/CSFML/Network/TcpSocket.cpp +++ b/src/CSFML/Network/TcpSocket.cpp @@ -31,7 +31,9 @@ #include #include +#include +#include #include @@ -170,3 +172,73 @@ sfSocketStatus sfTcpSocket_receivePacket(sfTcpSocket* socket, sfPacket* packet) assert(packet); return static_cast(socket->receive(*packet)); } + + +namespace +{ +//////////////////////////////////////////////////////////// +[[nodiscard]] sf::String fromUtf8(const char* string) +{ + return sf::String::fromUtf8(string, string + std::strlen(string)); +} +} // namespace + + +//////////////////////////////////////////////////////////// +sfTlsStatus sfTcpSocket_setupTlsClient(sfTcpSocket* socket, const char* hostname, bool verifyPeer) +{ + assert(socket); + assert(hostname); + return static_cast(socket->setupTlsClient(fromUtf8(hostname), verifyPeer)); +} + + +//////////////////////////////////////////////////////////// +sfTlsStatus sfTcpSocket_setupTlsClientWithCertificate(sfTcpSocket* socket, + const char* hostname, + const void* certificateChainData, + size_t certificateChainSize) +{ + assert(socket); + assert(hostname); + assert(certificateChainData); + return static_cast( + socket->setupTlsClient(fromUtf8(hostname), static_cast(certificateChainData), certificateChainSize)); +} + + +//////////////////////////////////////////////////////////// +sfTlsStatus sfTcpSocket_setupTlsServer( + sfTcpSocket* socket, + const void* certificateChainData, + size_t certificateChainSize, + const void* privateKeyData, + size_t privateKeySize, + const void* privateKeyPasswordData, + size_t privateKeyPasswordSize) +{ + assert(socket); + assert(certificateChainData); + assert(privateKeyData); + return static_cast( + socket->setupTlsServer(static_cast(certificateChainData), + certificateChainSize, + static_cast(privateKeyData), + privateKeySize, + static_cast(privateKeyPasswordData), + privateKeyPasswordData ? privateKeyPasswordSize : 0)); +} + + +//////////////////////////////////////////////////////////// +const char* sfTcpSocket_getCurrentCiphersuiteName(const sfTcpSocket* socket) +{ + assert(socket); + + const auto name = socket->getCurrentCiphersuiteName(); + if (!name) + return nullptr; + + socket->CiphersuiteName = *name; + return socket->CiphersuiteName.c_str(); +} diff --git a/src/CSFML/Network/TcpSocketStruct.hpp b/src/CSFML/Network/TcpSocketStruct.hpp index f4c89087..8c7bfe04 100644 --- a/src/CSFML/Network/TcpSocketStruct.hpp +++ b/src/CSFML/Network/TcpSocketStruct.hpp @@ -29,10 +29,13 @@ //////////////////////////////////////////////////////////// #include +#include + //////////////////////////////////////////////////////////// // Internal structure of sfTcpSocket //////////////////////////////////////////////////////////// struct sfTcpSocket : sf::TcpSocket { + mutable std::string CiphersuiteName; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c944caba..c18fb001 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -71,6 +71,7 @@ add_executable(test-csfml-network Network/Http.test.cpp Network/IpAddress.test.cpp Network/SocketStatus.test.cpp + Network/TcpSocket.test.cpp ) target_link_libraries(test-csfml-network PRIVATE csfml-network csfml-system Catch2::Catch2WithMain SFML::Network) set_target_warnings(test-csfml-network) diff --git a/test/Network/TcpSocket.test.cpp b/test/Network/TcpSocket.test.cpp new file mode 100644 index 00000000..2db38492 --- /dev/null +++ b/test/Network/TcpSocket.test.cpp @@ -0,0 +1,24 @@ +#include + +#include + +#include + +TEST_CASE("[Network] sfTcpSocket") +{ + SECTION("sfTlsStatus") + { + STATIC_CHECK(sfTlsNotConnected == static_cast(sf::TcpSocket::TlsStatus::NotConnected)); + STATIC_CHECK(sfTlsHandshakeStarted == static_cast(sf::TcpSocket::TlsStatus::HandshakeStarted)); + STATIC_CHECK(sfTlsHandshakeComplete == static_cast(sf::TcpSocket::TlsStatus::HandshakeComplete)); + STATIC_CHECK(sfTlsError == static_cast(sf::TcpSocket::TlsStatus::Error)); + } + + SECTION("TLS on unconnected socket") + { + sfTcpSocket* socket = sfTcpSocket_create(); + CHECK(sfTcpSocket_setupTlsClient(socket, "localhost", true) == sfTlsNotConnected); + CHECK(sfTcpSocket_getCurrentCiphersuiteName(socket) == nullptr); + sfTcpSocket_destroy(socket); + } +} From af34a2890e141bf76db23f6ae6970fdbbd484cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:40:17 +0200 Subject: [PATCH 11/15] Add readiness types and callbacks to sfSocketSelector --- include/CSFML/Network/SocketSelector.h | 138 +++++++++++++++++++++++-- src/CSFML/Network/SocketSelector.cpp | 117 ++++++++++++++++++--- test/CMakeLists.txt | 1 + test/Network/SocketSelector.test.cpp | 44 ++++++++ 4 files changed, 278 insertions(+), 22 deletions(-) create mode 100644 test/Network/SocketSelector.test.cpp diff --git a/include/CSFML/Network/SocketSelector.h b/include/CSFML/Network/SocketSelector.h index aa0fcf35..85d2b10d 100644 --- a/include/CSFML/Network/SocketSelector.h +++ b/include/CSFML/Network/SocketSelector.h @@ -32,6 +32,31 @@ #include #include +#include +#include + + +//////////////////////////////////////////////////////////// +/// \brief Readiness types a socket selector can check for +/// +/// The values can be combined with a bitwise OR. +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfSocketSelectorReceive = 1 << 0, ///< Check if sockets are ready to be received from + sfSocketSelectorSend = 1 << 1 ///< Check if sockets are ready to be sent to +} sfSocketSelectorReadiness; + +//////////////////////////////////////////////////////////// +/// \brief Callback that is called when a socket is ready +/// +/// \param readiness Readiness of the socket, a combination of sfSocketSelectorReadiness values +/// \param userData User data passed when adding the socket +/// +//////////////////////////////////////////////////////////// +typedef void (*sfSocketSelectorCallback)(uint32_t readiness, void* userData); + //////////////////////////////////////////////////////////// /// \brief Create a new selector @@ -66,13 +91,64 @@ CSFML_NETWORK_API void sfSocketSelector_destroy(const sfSocketSelector* selector /// so you have to make sure that the socket is not destroyed /// while it is stored in the selector. /// +/// The socket is checked for being ready to receive. +/// /// \param selector Socket selector object /// \param socket Pointer to the socket to add /// +/// \return True if the socket was successfully added +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API void sfSocketSelector_addTcpListener(sfSocketSelector* selector, sfTcpListener* socket); -CSFML_NETWORK_API void sfSocketSelector_addTcpSocket(sfSocketSelector* selector, sfTcpSocket* socket); -CSFML_NETWORK_API void sfSocketSelector_addUdpSocket(sfSocketSelector* selector, sfUdpSocket* socket); +CSFML_NETWORK_API bool sfSocketSelector_addTcpListener(sfSocketSelector* selector, sfTcpListener* socket); +CSFML_NETWORK_API bool sfSocketSelector_addTcpSocket(sfSocketSelector* selector, sfTcpSocket* socket); +CSFML_NETWORK_API bool sfSocketSelector_addUdpSocket(sfSocketSelector* selector, sfUdpSocket* socket); + +//////////////////////////////////////////////////////////// +/// \brief Add a new socket to a socket selector with the readiness to check for and an optional callback +/// +/// This function keeps a weak pointer to the socket, +/// so you have to make sure that the socket is not destroyed +/// while it is stored in the selector. +/// +/// The readiness to wait for can be specified as a combination +/// of sfSocketSelectorReceive and sfSocketSelectorSend. +/// +/// If a callback is provided, it will be called with the +/// readiness of the socket when sfSocketSelector_dispatchReadyCallbacks +/// is called after sfSocketSelector_wait returned true. Using +/// callbacks scales better than checking every socket with +/// sfSocketSelector_isXxxReadyFor. +/// +/// Adding a socket that has already been added updates its +/// readiness and callback. +/// +/// \param selector Socket selector object +/// \param socket Pointer to the socket to add +/// \param readiness Readiness to wait for, combination of sfSocketSelectorReadiness values +/// \param callback Callback to call when the socket is ready, can be NULL +/// \param userData User data that will be passed to the callback +/// +/// \return True if the socket was successfully added +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API bool sfSocketSelector_addTcpListenerWithReadiness( + sfSocketSelector* selector, + sfTcpListener* socket, + uint32_t readiness, + sfSocketSelectorCallback callback, + void* userData); +CSFML_NETWORK_API bool sfSocketSelector_addTcpSocketWithReadiness( + sfSocketSelector* selector, + sfTcpSocket* socket, + uint32_t readiness, + sfSocketSelectorCallback callback, + void* userData); +CSFML_NETWORK_API bool sfSocketSelector_addUdpSocketWithReadiness( + sfSocketSelector* selector, + sfUdpSocket* socket, + uint32_t readiness, + sfSocketSelectorCallback callback, + void* userData); //////////////////////////////////////////////////////////// /// \brief Remove a socket from a socket selector @@ -83,10 +159,12 @@ CSFML_NETWORK_API void sfSocketSelector_addUdpSocket(sfSocketSelector* selector, /// \param selector Socket selector object /// \param socket Pointer to the socket to remove /// +/// \return True if the socket was successfully removed +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API void sfSocketSelector_removeTcpListener(sfSocketSelector* selector, sfTcpListener* socket); -CSFML_NETWORK_API void sfSocketSelector_removeTcpSocket(sfSocketSelector* selector, sfTcpSocket* socket); -CSFML_NETWORK_API void sfSocketSelector_removeUdpSocket(sfSocketSelector* selector, sfUdpSocket* socket); +CSFML_NETWORK_API bool sfSocketSelector_removeTcpListener(sfSocketSelector* selector, sfTcpListener* socket); +CSFML_NETWORK_API bool sfSocketSelector_removeTcpSocket(sfSocketSelector* selector, sfTcpSocket* socket); +CSFML_NETWORK_API bool sfSocketSelector_removeUdpSocket(sfSocketSelector* selector, sfUdpSocket* socket); //////////////////////////////////////////////////////////// /// \brief Remove all the sockets stored in a selector @@ -101,11 +179,13 @@ CSFML_NETWORK_API void sfSocketSelector_removeUdpSocket(sfSocketSelector* select CSFML_NETWORK_API void sfSocketSelector_clear(sfSocketSelector* selector); //////////////////////////////////////////////////////////// -/// \brief Wait until one or more sockets are ready to receive +/// \brief Wait until one or more sockets are ready /// -/// This function returns as soon as at least one socket has -/// some data available to be received. To know which sockets are -/// ready, use the sfSocketSelector_isXxxReady functions. +/// This function returns as soon as at least one socket is +/// ready for the readiness it was added with (by default +/// having some data available to be received). To know which +/// sockets are ready, use the sfSocketSelector_isXxxReady +/// functions or sfSocketSelector_dispatchReadyCallbacks. /// If you use a timeout and no socket is ready before the timeout /// is over, the function returns false. /// @@ -136,3 +216,41 @@ CSFML_NETWORK_API bool sfSocketSelector_wait(sfSocketSelector* selector, sfTime CSFML_NETWORK_API bool sfSocketSelector_isTcpListenerReady(const sfSocketSelector* selector, sfTcpListener* socket); CSFML_NETWORK_API bool sfSocketSelector_isTcpSocketReady(const sfSocketSelector* selector, sfTcpSocket* socket); CSFML_NETWORK_API bool sfSocketSelector_isUdpSocketReady(const sfSocketSelector* selector, sfUdpSocket* socket); + +//////////////////////////////////////////////////////////// +/// \brief Test a socket to know if it is ready for the given readiness +/// +/// This function must be used after a call to +/// sfSocketSelector_wait, to know which sockets are ready. +/// +/// \param selector Socket selector object +/// \param socket Socket to test +/// \param readiness Readiness to check for, combination of sfSocketSelectorReadiness values +/// +/// \return true if the socket is ready, false otherwise +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API bool sfSocketSelector_isTcpListenerReadyFor(const sfSocketSelector* selector, + sfTcpListener* socket, + uint32_t readiness); +CSFML_NETWORK_API bool sfSocketSelector_isTcpSocketReadyFor(const sfSocketSelector* selector, + sfTcpSocket* socket, + uint32_t readiness); +CSFML_NETWORK_API bool sfSocketSelector_isUdpSocketReadyFor(const sfSocketSelector* selector, + sfUdpSocket* socket, + uint32_t readiness); + +//////////////////////////////////////////////////////////// +/// \brief Call the callbacks of all sockets that are ready +/// +/// After sfSocketSelector_wait returned true, at least one +/// socket is ready. Calling this function will call the +/// callbacks of all the sockets that became ready during +/// the wait. Calling this function multiple times after a +/// single call to sfSocketSelector_wait will run the callbacks +/// multiple times. +/// +/// \param selector Socket selector object +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API void sfSocketSelector_dispatchReadyCallbacks(sfSocketSelector* selector); diff --git a/src/CSFML/Network/SocketSelector.cpp b/src/CSFML/Network/SocketSelector.cpp index 9d4673df..1abc6851 100644 --- a/src/CSFML/Network/SocketSelector.cpp +++ b/src/CSFML/Network/SocketSelector.cpp @@ -31,6 +31,22 @@ #include #include +#include + + +namespace +{ +//////////////////////////////////////////////////////////// +[[nodiscard]] std::function convertCallback(sfSocketSelectorCallback callback, + void* userData) +{ + if (!callback) + return {}; + + return [callback, userData](sf::SocketSelector::ReadinessType readiness) { callback(readiness, userData); }; +} +} // namespace + //////////////////////////////////////////////////////////// sfSocketSelector* sfSocketSelector_create() @@ -55,44 +71,86 @@ void sfSocketSelector_destroy(const sfSocketSelector* selector) //////////////////////////////////////////////////////////// -void sfSocketSelector_addTcpListener(sfSocketSelector* selector, sfTcpListener* socket) +bool sfSocketSelector_addTcpListener(sfSocketSelector* selector, sfTcpListener* socket) +{ + assert(selector); + assert(socket); + return selector->add(*socket); +} +bool sfSocketSelector_addTcpSocket(sfSocketSelector* selector, sfTcpSocket* socket) +{ + assert(selector); + assert(socket); + return selector->add(*socket); +} +bool sfSocketSelector_addUdpSocket(sfSocketSelector* selector, sfUdpSocket* socket) +{ + assert(selector); + assert(socket); + return selector->add(*socket); +} + + +//////////////////////////////////////////////////////////// +bool sfSocketSelector_addTcpListenerWithReadiness( + sfSocketSelector* selector, + sfTcpListener* socket, + uint32_t readiness, + sfSocketSelectorCallback callback, + void* userData) { assert(selector); assert(socket); - selector->add(*socket); + return selector->add(*socket, readiness, convertCallback(callback, userData)); } -void sfSocketSelector_addTcpSocket(sfSocketSelector* selector, sfTcpSocket* socket) + + +//////////////////////////////////////////////////////////// +bool sfSocketSelector_addTcpSocketWithReadiness( + sfSocketSelector* selector, + sfTcpSocket* socket, + uint32_t readiness, + sfSocketSelectorCallback callback, + void* userData) { assert(selector); assert(socket); - selector->add(*socket); + return selector->add(*socket, readiness, convertCallback(callback, userData)); } -void sfSocketSelector_addUdpSocket(sfSocketSelector* selector, sfUdpSocket* socket) + + +//////////////////////////////////////////////////////////// +bool sfSocketSelector_addUdpSocketWithReadiness( + sfSocketSelector* selector, + sfUdpSocket* socket, + uint32_t readiness, + sfSocketSelectorCallback callback, + void* userData) { assert(selector); assert(socket); - selector->add(*socket); + return selector->add(*socket, readiness, convertCallback(callback, userData)); } //////////////////////////////////////////////////////////// -void sfSocketSelector_removeTcpListener(sfSocketSelector* selector, sfTcpListener* socket) +bool sfSocketSelector_removeTcpListener(sfSocketSelector* selector, sfTcpListener* socket) { assert(selector); assert(socket); - selector->remove(*socket); + return selector->remove(*socket); } -void sfSocketSelector_removeTcpSocket(sfSocketSelector* selector, sfTcpSocket* socket) +bool sfSocketSelector_removeTcpSocket(sfSocketSelector* selector, sfTcpSocket* socket) { assert(selector); assert(socket); - selector->remove(*socket); + return selector->remove(*socket); } -void sfSocketSelector_removeUdpSocket(sfSocketSelector* selector, sfUdpSocket* socket) +bool sfSocketSelector_removeUdpSocket(sfSocketSelector* selector, sfUdpSocket* socket) { assert(selector); assert(socket); - selector->remove(*socket); + return selector->remove(*socket); } @@ -131,3 +189,38 @@ bool sfSocketSelector_isUdpSocketReady(const sfSocketSelector* selector, sfUdpSo assert(socket); return selector->isReady(*socket); } + + +//////////////////////////////////////////////////////////// +bool sfSocketSelector_isTcpListenerReadyFor(const sfSocketSelector* selector, sfTcpListener* socket, uint32_t readiness) +{ + assert(selector); + assert(socket); + return selector->isReady(*socket, readiness); +} + + +//////////////////////////////////////////////////////////// +bool sfSocketSelector_isTcpSocketReadyFor(const sfSocketSelector* selector, sfTcpSocket* socket, uint32_t readiness) +{ + assert(selector); + assert(socket); + return selector->isReady(*socket, readiness); +} + + +//////////////////////////////////////////////////////////// +bool sfSocketSelector_isUdpSocketReadyFor(const sfSocketSelector* selector, sfUdpSocket* socket, uint32_t readiness) +{ + assert(selector); + assert(socket); + return selector->isReady(*socket, readiness); +} + + +//////////////////////////////////////////////////////////// +void sfSocketSelector_dispatchReadyCallbacks(sfSocketSelector* selector) +{ + assert(selector); + selector->dispatchReadyCallbacks(); +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c18fb001..927cda70 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -70,6 +70,7 @@ add_executable(test-csfml-network Network/Ftp.test.cpp Network/Http.test.cpp Network/IpAddress.test.cpp + Network/SocketSelector.test.cpp Network/SocketStatus.test.cpp Network/TcpSocket.test.cpp ) diff --git a/test/Network/SocketSelector.test.cpp b/test/Network/SocketSelector.test.cpp new file mode 100644 index 00000000..03967c2d --- /dev/null +++ b/test/Network/SocketSelector.test.cpp @@ -0,0 +1,44 @@ +#include +#include +#include + +#include + +#include + +TEST_CASE("[Network] sfSocketSelector") +{ + SECTION("sfSocketSelectorReadiness") + { + STATIC_CHECK(sfSocketSelectorReceive == static_cast(sf::SocketSelector::Receive)); + STATIC_CHECK(sfSocketSelectorSend == static_cast(sf::SocketSelector::Send)); + } + + SECTION("Ready callbacks") + { + sfTcpListener* listener = sfTcpListener_create(); + REQUIRE(sfTcpListener_listen(listener, 0, sfIpAddress_LocalHost) == sfSocketDone); + + sfTcpSocket* client = sfTcpSocket_create(); + REQUIRE(sfTcpSocket_connect(client, sfIpAddress_LocalHost, sfTcpListener_getLocalPort(listener), sfSeconds(1)) == + sfSocketDone); + + sfSocketSelector* selector = sfSocketSelector_create(); + + uint32_t readiness = 0; + const auto callback = [](uint32_t ready, void* userData) { *static_cast(userData) |= ready; }; + CHECK(sfSocketSelector_addTcpListenerWithReadiness(selector, listener, sfSocketSelectorReceive, callback, &readiness)); + + REQUIRE(sfSocketSelector_wait(selector, sfSeconds(1))); + CHECK(sfSocketSelector_isTcpListenerReady(selector, listener)); + CHECK(sfSocketSelector_isTcpListenerReadyFor(selector, listener, sfSocketSelectorReceive)); + sfSocketSelector_dispatchReadyCallbacks(selector); + CHECK(readiness == sfSocketSelectorReceive); + + CHECK(sfSocketSelector_removeTcpListener(selector, listener)); + + sfSocketSelector_destroy(selector); + sfTcpSocket_destroy(client); + sfTcpListener_destroy(listener); + } +} From 8208e90599b2d745ff470574c004a1fe2587a111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:40:18 +0200 Subject: [PATCH 12/15] Add HTTPS verification and address type options to sfHttp --- include/CSFML/Network/Http.h | 48 +++++++++++++++++++++++++++++++++++- src/CSFML/Network/Http.cpp | 22 +++++++++++++++-- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/include/CSFML/Network/Http.h b/include/CSFML/Network/Http.h index 35852c4f..b99a61fb 100644 --- a/include/CSFML/Network/Http.h +++ b/include/CSFML/Network/Http.h @@ -29,9 +29,12 @@ //////////////////////////////////////////////////////////// #include +#include #include #include +#include + //////////////////////////////////////////////////////////// /// \brief Enumerate the available HTTP methods for a request @@ -272,12 +275,35 @@ CSFML_NETWORK_API void sfHttp_destroy(const sfHttp* http); /// leave it like this unless you really need a port other /// than the standard one, or use an unknown protocol. /// +/// The host is resolved immediately. To use HTTPS, prefix +/// the host with "https://". +/// /// \param http Http object /// \param host Web server to connect to /// \param port Port to use for connection /// +/// \return True if the host has been resolved and is valid, false otherwise +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API bool sfHttp_setHost(sfHttp* http, const char* host, unsigned short port); + +//////////////////////////////////////////////////////////// +/// \brief Set the target host of a HTTP object with a given address type +/// +/// See sfHttp_setHost for details. +/// +/// \param http Http object +/// \param host Web server to connect to +/// \param port Port to use for connection +/// \param addressType Address type to use for the connection, NULL to specify no preference +/// +/// \return True if the host has been resolved and is valid, false otherwise +/// //////////////////////////////////////////////////////////// -CSFML_NETWORK_API void sfHttp_setHost(sfHttp* http, const char* host, unsigned short port); +CSFML_NETWORK_API bool sfHttp_setHostWithAddressType(sfHttp* http, + const char* host, + unsigned short port, + const sfIpAddressType* addressType); //////////////////////////////////////////////////////////// /// \brief Send a HTTP request and return the server's response. @@ -299,3 +325,23 @@ CSFML_NETWORK_API void sfHttp_setHost(sfHttp* http, const char* host, unsigned s /// //////////////////////////////////////////////////////////// CSFML_NETWORK_API sfHttpResponse* sfHttp_sendRequest(sfHttp* http, const sfHttpRequest* request, sfTime timeout); + +//////////////////////////////////////////////////////////// +/// \brief Send a HTTP request and return the server's response, optionally without verifying the server +/// +/// See sfHttp_sendRequest for details. sfHttp_sendRequest +/// always verifies the server when using HTTPS. +/// +/// \param http Http object +/// \param request Request to send +/// \param timeout Maximum time to wait +/// \param verifyServer Verify the server if using HTTPS +/// +/// \return Server's response +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfHttpResponse* sfHttp_sendRequestWithVerification( + const sfHttp* http, + const sfHttpRequest* request, + sfTime timeout, + bool verifyServer); diff --git a/src/CSFML/Network/Http.cpp b/src/CSFML/Network/Http.cpp index ea5877af..2586c2fa 100644 --- a/src/CSFML/Network/Http.cpp +++ b/src/CSFML/Network/Http.cpp @@ -25,6 +25,7 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// +#include #include #include @@ -149,10 +150,18 @@ void sfHttp_destroy(const sfHttp* http) //////////////////////////////////////////////////////////// -void sfHttp_setHost(sfHttp* http, const char* host, unsigned short port) +bool sfHttp_setHost(sfHttp* http, const char* host, unsigned short port) { assert(http); - http->setHost(host ? host : "", port); + return http->setHost(host ? host : "", port); +} + + +//////////////////////////////////////////////////////////// +bool sfHttp_setHostWithAddressType(sfHttp* http, const char* host, unsigned short port, const sfIpAddressType* addressType) +{ + assert(http); + return http->setHost(host ? host : "", port, convertIpAddressType(addressType)); } @@ -163,3 +172,12 @@ sfHttpResponse* sfHttp_sendRequest(sfHttp* http, const sfHttpRequest* request, s assert(request); return new sfHttpResponse{http->sendRequest(*request, sf::microseconds(timeout.microseconds))}; } + + +//////////////////////////////////////////////////////////// +sfHttpResponse* sfHttp_sendRequestWithVerification(const sfHttp* http, const sfHttpRequest* request, sfTime timeout, bool verifyServer) +{ + assert(http); + assert(request); + return new sfHttpResponse{http->sendRequest(*request, sf::microseconds(timeout.microseconds), verifyServer)}; +} From 63dc503d207c7ce81d1723e29cbf8b409f81abc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:46:08 +0200 Subject: [PATCH 13/15] Add sfTimeoutWithPredicate --- include/CSFML/System.h | 1 + include/CSFML/System/TimeoutWithPredicate.h | 66 +++++++++++++++++++ src/CSFML/System/CMakeLists.txt | 2 + .../System/ConvertTimeoutWithPredicate.hpp | 46 +++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 include/CSFML/System/TimeoutWithPredicate.h create mode 100644 src/CSFML/System/ConvertTimeoutWithPredicate.hpp diff --git a/include/CSFML/System.h b/include/CSFML/System.h index 7050c01e..6da5a17d 100644 --- a/include/CSFML/System.h +++ b/include/CSFML/System.h @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include diff --git a/include/CSFML/System/TimeoutWithPredicate.h b/include/CSFML/System/TimeoutWithPredicate.h new file mode 100644 index 00000000..d555e38f --- /dev/null +++ b/include/CSFML/System/TimeoutWithPredicate.h @@ -0,0 +1,66 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include + + +//////////////////////////////////////////////////////////// +/// \brief Predicate that returns true to continue or false to time out +/// +/// \param userData User data stored in the sfTimeoutWithPredicate +/// +/// \return True to continue, false to time out +/// +//////////////////////////////////////////////////////////// +typedef bool (*sfTimeoutPredicate)(void* userData); + +//////////////////////////////////////////////////////////// +/// \brief Hybrid of a timeout and a continuation predicate +/// +/// Functions taking a timeout parameter that is specified +/// solely by a time cannot be easily interrupted. A predicate +/// allows interrupting the operation at any time, e.g. when +/// the user cancels it. +/// +/// If \a predicate is NULL, the operation times out after +/// \a timeout (0 meaning no timeout). If \a predicate is set, +/// \a timeout is ignored and the operation times out once the +/// predicate returns false. The predicate is checked every +/// \a period, a period of 0 checks it every millisecond. +/// +//////////////////////////////////////////////////////////// +typedef struct +{ + sfTime timeout; ///< Time to time out after, if no predicate is set + sfTimeoutPredicate predicate; ///< Predicate that returns true to continue or false to time out, can be NULL + void* userData; ///< User data passed to the predicate + sfTime period; ///< The period between checks of the predicate +} sfTimeoutWithPredicate; diff --git a/src/CSFML/System/CMakeLists.txt b/src/CSFML/System/CMakeLists.txt index 0178d6c6..4954d33b 100644 --- a/src/CSFML/System/CMakeLists.txt +++ b/src/CSFML/System/CMakeLists.txt @@ -20,6 +20,8 @@ set(SRC ${INCROOT}/Sleep.h ${SRCROOT}/Time.cpp ${INCROOT}/Time.h + ${SRCROOT}/ConvertTimeoutWithPredicate.hpp + ${INCROOT}/TimeoutWithPredicate.h ${INCROOT}/Types.h ${INCROOT}/Vector2.h ${INCROOT}/Vector3.h diff --git a/src/CSFML/System/ConvertTimeoutWithPredicate.hpp b/src/CSFML/System/ConvertTimeoutWithPredicate.hpp new file mode 100644 index 00000000..d0433e77 --- /dev/null +++ b/src/CSFML/System/ConvertTimeoutWithPredicate.hpp @@ -0,0 +1,46 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include + + +//////////////////////////////////////////////////////////// +// Convert sfTimeoutWithPredicate to sf::TimeoutWithPredicate +//////////////////////////////////////////////////////////// +[[nodiscard]] inline sf::TimeoutWithPredicate convertTimeoutWithPredicate(const sfTimeoutWithPredicate& timeout) +{ + if (!timeout.predicate) + return {sf::microseconds(timeout.timeout.microseconds)}; + + const auto period = timeout.period.microseconds > 0 ? sf::microseconds(timeout.period.microseconds) + : sf::milliseconds(1); + return {[predicate = timeout.predicate, userData = timeout.userData] { return predicate(userData); }, period}; +} From fe8c6adb126cbde7cee3920982c7579623ca0961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:46:10 +0200 Subject: [PATCH 14/15] Add sfSftp as a secure replacement for sfFtp --- include/CSFML/Network.h | 1 + include/CSFML/Network/Sftp.h | 778 +++++++++++++++++++++++++++++++ include/CSFML/Network/Types.h | 5 + src/CSFML/Network/CMakeLists.txt | 3 + src/CSFML/Network/Sftp.cpp | 547 ++++++++++++++++++++++ src/CSFML/Network/SftpStruct.hpp | 78 ++++ test/CMakeLists.txt | 1 + test/Network/Sftp.test.cpp | 106 +++++ 8 files changed, 1519 insertions(+) create mode 100644 include/CSFML/Network/Sftp.h create mode 100644 src/CSFML/Network/Sftp.cpp create mode 100644 src/CSFML/Network/SftpStruct.hpp create mode 100644 test/Network/Sftp.test.cpp diff --git a/include/CSFML/Network.h b/include/CSFML/Network.h index ddc55e0a..669bf57c 100644 --- a/include/CSFML/Network.h +++ b/include/CSFML/Network.h @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include diff --git a/include/CSFML/Network/Sftp.h b/include/CSFML/Network/Sftp.h new file mode 100644 index 00000000..9d1c1291 --- /dev/null +++ b/include/CSFML/Network/Sftp.h @@ -0,0 +1,778 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include +#include +#include + +#include +#include +#include + + +//////////////////////////////////////////////////////////// +/// \brief Values of a SFTP result +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + // General result values + sfSftpSuccess, ///< Operation completed successfully + sfSftpDisconnected, ///< The TCP socket has been disconnected + sfSftpTimeout, ///< Operation timed out + sfSftpRefused, ///< Connection refused + sfSftpError, ///< Generic error + + // SSH result values + sfSftpBannerReceive, ///< Error during banner receive + sfSftpBannerSend, ///< Error during banner send + sfSftpInvalidMac, ///< Invalid message authentication code + sfSftpAllocationFailure, ///< Allocation failure + sfSftpSocketSend, ///< Error sending on socket + sfSftpKeyExchangeFailure, ///< Key exchange failed + sfSftpHostKeyInitialization, ///< Host key initialization failed + sfSftpHostKeySign, ///< Host key signing failed + sfSftpDecryptError, ///< Decryption failed + sfSftpProtocolError, ///< SSH protocol error + sfSftpPasswordExpired, ///< Password expired + sfSftpFileError, ///< File error + sfSftpMethodNone, ///< No method found + sfSftpAuthenticationFailed, ///< Authentication failed + sfSftpPublicKeyUnverified, ///< Public key unverified + sfSftpChannelOutOfOrder, ///< Channel out of order + sfSftpChannelFailure, ///< Channel failure + sfSftpChannelRequestDenied, ///< Channel request denied + sfSftpChannelUnknown, ///< Channel unknown + sfSftpChannelWindowExceeded, ///< Channel window exceeded + sfSftpChannelPacketExceeded, ///< Channel packet exceeded + sfSftpChannelClosed, ///< Channel closed + sfSftpChannelEofSent, ///< Channel EOF sent + sfSftpScpProtocol, ///< SCP protocol error + sfSftpZlibError, ///< Zlib error + sfSftpRequestDenied, ///< Request denied + sfSftpMethodNotSupported, ///< Method not supported + sfSftpInvalidData, ///< Invalid data + sfSftpPublicKeyProtocol, ///< Public key protocol error + sfSftpBufferTooSmall, ///< Buffer too small + sfSftpBadUse, ///< Bad usage + sfSftpCompressError, ///< Compression error + sfSftpOutOfBoundary, ///< Out of boundary + sfSftpAgentProtocol, ///< Agent protocol error + sfSftpSocketRecv, ///< Socket receive error + sfSftpEncryptError, ///< Encryption failed + sfSftpBadSocket, ///< Bad socket + sfSftpKnownHosts, ///< Known hosts error + sfSftpChannelWindowFull, ///< Channel window full + sfSftpKeyFileAuthenticationFailed, ///< Key file authentication failed + + // SFTP result values + sfSftpEndOfFile, ///< End of file + sfSftpNoSuchFile, ///< No such file + sfSftpPermissionDenied, ///< Permission denied + sfSftpFailure, ///< Failure + sfSftpBadMessage, ///< Bad message + sfSftpNoConnection, ///< No connection + sfSftpConnectionLost, ///< Connection lost + sfSftpOperationUnsupported, ///< Operation unsupported + sfSftpInvalidHandle, ///< Invalid handle + sfSftpNoSuchPath, ///< No such path + sfSftpFileAlreadyExists, ///< File already exists + sfSftpWriteProtect, ///< Write protect + sfSftpNoMedia, ///< No media + sfSftpNoSpaceOnFileSystem, ///< No space on filesystem + sfSftpQuotaExceeded, ///< Quota exceeded + sfSftpUnknownPrincipal, ///< Unknown principal + sfSftpLockConflict, ///< Lock conflict + sfSftpDirectoryNotEmpty, ///< Directory not empty + sfSftpNotADirectory, ///< Not a directory + sfSftpInvalidFilename, ///< Invalid filename + sfSftpLinkLoop, ///< Link loop + sfSftpSftpError ///< Generic SFTP error +} sfSftpResultValue; + +//////////////////////////////////////////////////////////// +/// \brief Type of a remote file system entry +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfSftpFileNone, ///< No type + sfSftpFileNotFound, ///< The file does not exist + sfSftpFileRegular, ///< Regular file + sfSftpFileDirectory, ///< Directory + sfSftpFileSymlink, ///< Symbolic link + sfSftpFileBlock, ///< Block special file + sfSftpFileCharacter, ///< Character special file + sfSftpFileFifo, ///< FIFO or pipe + sfSftpFileSocket, ///< Socket + sfSftpFileUnknown ///< Unknown type +} sfSftpFileType; + +//////////////////////////////////////////////////////////// +/// \brief Attributes of a remote file system entry +/// +/// Each attribute is only valid if the corresponding +/// has-flag is set. +/// +/// Permissions use the POSIX permission bits, e.g. 0755. +/// Times are given in seconds since the Unix epoch. +/// +//////////////////////////////////////////////////////////// +typedef struct +{ + const char* path; ///< Path to the entry, encoded in UTF-8 + uint64_t size; ///< Size of the entry + uint64_t userId; ///< Owner user ID + uint64_t groupId; ///< Group ID + int64_t accessTime; ///< Last access time + int64_t modificationTime; ///< Last modification time + sfSftpFileType type; ///< Type of the entry + uint32_t permissions; ///< Permissions + bool hasType; ///< Whether the type is available + bool hasSize; ///< Whether the size is available + bool hasPermissions; ///< Whether the permissions are available + bool hasUserId; ///< Whether the owner user ID is available + bool hasGroupId; ///< Whether the group ID is available + bool hasAccessTime; ///< Whether the last access time is available + bool hasModificationTime; ///< Whether the last modification time is available +} sfSftpAttributes; + +//////////////////////////////////////////////////////////// +/// \brief Type of a SSH host key +/// +//////////////////////////////////////////////////////////// +typedef enum +{ + sfSftpHostKeyUnknown, ///< Unknown key type + sfSftpHostKeyRsa, ///< RSA + sfSftpHostKeyDsa, ///< DSA + sfSftpHostKeyEcdsa256, ///< NIST P-256 ECDSA + sfSftpHostKeyEcdsa384, ///< NIST P-384 ECDSA + sfSftpHostKeyEcdsa521, ///< NIST P-521 ECDSA + sfSftpHostKeyEd25519 ///< ED25519 +} sfSftpHostKeyType; + +//////////////////////////////////////////////////////////// +/// \brief SSH session information +/// +/// The algorithm identifiers follow the RFC 4253 specification. +/// +//////////////////////////////////////////////////////////// +typedef struct +{ + sfSftpHostKeyType hostKeyType; ///< Host key type + const uint8_t* hostKeyData; ///< Host key data + size_t hostKeySize; ///< Size of the host key data + uint8_t hostKeySha1[20]; ///< Host key SHA1 hash + uint8_t hostKeySha256[32]; ///< Host key SHA256 hash + const char* keyExchangeAlgorithm; ///< Key exchange algorithm used in the session + const char* hostKeyAlgorithm; ///< Host key algorithm used in the session + const char* clientToServerEncryptionAlgorithm; ///< Client to server encryption algorithm used in the session + const char* serverToClientEncryptionAlgorithm; ///< Server to client encryption algorithm used in the session + const char* clientToServerMacAlgorithm; ///< Client to server message authentication code algorithm used in the session + const char* serverToClientMacAlgorithm; ///< Server to client message authentication code algorithm used in the session + const char* clientToServerCompressionAlgorithm; ///< Client to server compression algorithm used in the session + const char* serverToClientCompressionAlgorithm; ///< Server to client compression algorithm used in the session +} sfSftpSessionInfo; + +//////////////////////////////////////////////////////////// +/// \brief Callback receiving the data of a downloaded file +/// +/// The data is transferred in sequential blocks. The size of +/// the blocks can change over time. +/// +/// \param data Pointer to the received data block +/// \param size Size of the received data block +/// \param userData User data passed to sfSftp_download +/// +/// \return True to continue the transfer, false to abort it +/// +//////////////////////////////////////////////////////////// +typedef bool (*sfSftpDownloadCallback)(const void* data, size_t size, void* userData); + +//////////////////////////////////////////////////////////// +/// \brief Callback providing the data of a file to upload +/// +/// Data to be sent should be copied into the data block and +/// \a size set to the actual number of bytes copied. When +/// called, \a size contains the size of the data block, which +/// can change over time. +/// +/// \param data Pointer to the data block to fill +/// \param size Size of the data block, to be set to the number of bytes copied +/// \param userData User data passed to sfSftp_upload +/// +/// \return True to continue the transfer, false to stop it e.g. because there is no more data to send +/// +//////////////////////////////////////////////////////////// +typedef bool (*sfSftpUploadCallback)(void* data, size_t* size, void* userData); + + +//////////////////////////////////////////////////////////// +/// \brief Destroy a SFTP result +/// +/// \param result SFTP result to destroy +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API void sfSftpResult_destroy(const sfSftpResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Check if a SFTP result is a success +/// +/// \param result SFTP result +/// +/// \return True if the result is sfSftpSuccess +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API bool sfSftpResult_isOk(const sfSftpResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the value of a SFTP result +/// +/// \param result SFTP result +/// +/// \return Result value +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResultValue sfSftpResult_getValue(const sfSftpResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the message of a SFTP result +/// +/// \param result SFTP result +/// +/// \return The result message +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const char* sfSftpResult_getMessage(const sfSftpResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Destroy a SFTP path result +/// +/// \param result SFTP path result to destroy +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API void sfSftpPathResult_destroy(const sfSftpPathResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Check if a SFTP path result is a success +/// +/// \param result SFTP path result +/// +/// \return True if the result is sfSftpSuccess +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API bool sfSftpPathResult_isOk(const sfSftpPathResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the value of a SFTP path result +/// +/// \param result SFTP path result +/// +/// \return Result value +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResultValue sfSftpPathResult_getValue(const sfSftpPathResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the message of a SFTP path result +/// +/// \param result SFTP path result +/// +/// \return The result message +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const char* sfSftpPathResult_getMessage(const sfSftpPathResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the path of a SFTP path result +/// +/// \param result SFTP path result +/// +/// \return The path, encoded in UTF-8 +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const char* sfSftpPathResult_getPath(const sfSftpPathResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Destroy a SFTP attributes result +/// +/// \param result SFTP attributes result to destroy +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API void sfSftpAttributesResult_destroy(const sfSftpAttributesResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Check if a SFTP attributes result is a success +/// +/// \param result SFTP attributes result +/// +/// \return True if the result is sfSftpSuccess +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API bool sfSftpAttributesResult_isOk(const sfSftpAttributesResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the value of a SFTP attributes result +/// +/// \param result SFTP attributes result +/// +/// \return Result value +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResultValue sfSftpAttributesResult_getValue(const sfSftpAttributesResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the message of a SFTP attributes result +/// +/// \param result SFTP attributes result +/// +/// \return The result message +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const char* sfSftpAttributesResult_getMessage(const sfSftpAttributesResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the attributes of a SFTP attributes result +/// +/// The strings of the attributes stay valid until the +/// result is destroyed. +/// +/// \param result SFTP attributes result +/// +/// \return The attributes +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpAttributes sfSftpAttributesResult_getAttributes(const sfSftpAttributesResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Destroy a SFTP listing result +/// +/// \param result SFTP listing result to destroy +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API void sfSftpListingResult_destroy(const sfSftpListingResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Check if a SFTP listing result is a success +/// +/// \param result SFTP listing result +/// +/// \return True if the result is sfSftpSuccess +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API bool sfSftpListingResult_isOk(const sfSftpListingResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the value of a SFTP listing result +/// +/// \param result SFTP listing result +/// +/// \return Result value +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResultValue sfSftpListingResult_getValue(const sfSftpListingResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the message of a SFTP listing result +/// +/// \param result SFTP listing result +/// +/// \return The result message +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API const char* sfSftpListingResult_getMessage(const sfSftpListingResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the number of entries in a SFTP listing result +/// +/// \param result SFTP listing result +/// +/// \return Number of directory entries +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API size_t sfSftpListingResult_getCount(const sfSftpListingResult* result); + +//////////////////////////////////////////////////////////// +/// \brief Get the attributes of an entry in a SFTP listing result +/// +/// The strings of the attributes stay valid until the +/// result is destroyed. +/// +/// \param result SFTP listing result +/// \param index Index of the entry to get +/// +/// \return The attributes of the entry +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpAttributes sfSftpListingResult_getAttributes(const sfSftpListingResult* result, size_t index); + +//////////////////////////////////////////////////////////// +/// \brief Create a new SFTP object +/// +/// \return A new sfSftp object +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftp* sfSftp_create(void); + +//////////////////////////////////////////////////////////// +/// \brief Destroy an existing SFTP object +/// +/// \param sftp SFTP object to destroy +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API void sfSftp_destroy(const sfSftp* sftp); + +//////////////////////////////////////////////////////////// +/// \brief Connect to the specified SFTP server +/// +/// \param sftp SFTP object +/// \param server Address of the server to connect to +/// \param port Port used for the connection, the default SFTP port is 22 +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of the connection attempt +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResult* sfSftp_connect(sfSftp* sftp, sfIpAddress server, unsigned short port, sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Disconnect the connection with the server +/// +/// \param sftp SFTP object +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of disconnecting the connection with the server +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResult* sfSftp_disconnect(sfSftp* sftp, sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Get SSH session information +/// +/// After connecting to the server and before actually +/// logging in the SSH session information of the underlying +/// connection will be available. +/// +/// The session information contains among other things the +/// public key identifying the remote host and the connection +/// parameters such as encryption and compression used. +/// +/// Relying on the user to check the authenticity of the host +/// key is the typical method used to verify the connection +/// to the legitimate host. If connection security is a high +/// priority, examining the parameters and aborting the +/// connection if any weak algorithms are used is also possible. +/// +/// The pointers in the session information stay valid until +/// the next call to this function or until the SFTP object +/// is destroyed. +/// +/// \param sftp SFTP object +/// \param info Session information to fill +/// +/// \return True if the session information is available, false otherwise +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API bool sfSftp_getSessionInfo(const sfSftp* sftp, sfSftpSessionInfo* info); + +//////////////////////////////////////////////////////////// +/// \brief Log in using a username and a password +/// +/// Logging in is mandatory after connecting to the server. +/// Users that are not logged in cannot perform any operation. +/// +/// \param sftp SFTP object +/// \param name User name +/// \param password Password +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of attempting to log in to the server +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResult* sfSftp_login(sfSftp* sftp, const char* name, const char* password, sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Log in using a public/private key pair +/// +/// Logging in is mandatory after connecting to the server. +/// Users that are not logged in cannot perform any operation. +/// +/// The public and private key data should be provided in PEM +/// format. Even though it is technically possible to derive +/// the public key from the private key, due to backend +/// limitations, providing a pre-generated public key as well +/// is necessary for this function to be able to succeed. +/// +/// If the private key is not protected by a passphrase the +/// passphrase should be set to the empty string or NULL. +/// +/// \param sftp SFTP object +/// \param name User name +/// \param publicKeyData Public key data +/// \param publicKeyLength Public key data length +/// \param privateKeyData Private key data +/// \param privateKeyLength Private key data length +/// \param privateKeyPassphrase Private key passphrase, NULL terminated +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of attempting to log in to the server +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResult* sfSftp_loginWithKey( + sfSftp* sftp, + const char* name, + const char* publicKeyData, + size_t publicKeyLength, + const char* privateKeyData, + size_t privateKeyLength, + const char* privateKeyPassphrase, + sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Resolve a remote path into an absolute remote path +/// +/// Paths can contain links and other reserved path identifiers +/// such as . and .. referring to the current directory and +/// parent directory respectively. This function determines +/// the absolute path, which does not contain links or . or .. +/// +/// Resolving "." will return the absolute path to the current +/// working directory of the user after logging in to the SFTP +/// server. +/// +/// \param sftp SFTP object +/// \param path Path to convert into an absolute path, encoded in UTF-8 +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of converting the path into an absolute path +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpPathResult* sfSftp_resolvePath(sfSftp* sftp, const char* path, sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Get the current working directory on the server +/// +/// This is an alias for calling sfSftp_resolvePath with ".". +/// +/// \param sftp SFTP object +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of getting the current working directory +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpPathResult* sfSftp_getWorkingDirectory(sfSftp* sftp, sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Get the attributes of a remote file or directory +/// +/// Depending on whether \a path refers to a file or directory, +/// the attributes can contain e.g. the type of file, the file +/// owner, group, file size, modification and access times. +/// +/// If links are not to be followed, \a followLinks can be set +/// to false. In this case the attributes of the link itself +/// will be returned. +/// +/// \param sftp SFTP object +/// \param path Path to the remote file or directory whose attributes to get, encoded in UTF-8 +/// \param followLinks True to follow links, false to return attributes of the link itself +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of getting the attributes +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpAttributesResult* sfSftp_getAttributes( + sfSftp* sftp, + const char* path, + bool followLinks, + sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Get the contents of the given directory +/// +/// This function retrieves the sub-directories and files +/// contained in the given directory. It is not recursive. +/// +/// \param sftp SFTP object +/// \param path Path of the directory whose contents to list, encoded in UTF-8 +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of getting the contents of the given directory +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpListingResult* sfSftp_getDirectoryListing(sfSftp* sftp, const char* path, sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Create a new directory +/// +/// The new directory is created as a child of the current +/// working directory. +/// +/// A common permissions value is 0755 (rwxr-xr-x). +/// +/// \param sftp SFTP object +/// \param path Path of the directory to create, encoded in UTF-8 +/// \param permissions POSIX permissions of the directory to create +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of creating the directory +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResult* sfSftp_createDirectory(sfSftp* sftp, + const char* path, + uint32_t permissions, + sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Remove an existing directory +/// +/// Use this function with caution, the directory will +/// be removed permanently! +/// +/// \param sftp SFTP object +/// \param path Path of the directory to remove, encoded in UTF-8 +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of removing the directory +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResult* sfSftp_deleteDirectory(sfSftp* sftp, const char* path, sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Rename an existing file or directory +/// +/// In POSIX renaming and moving are synonymous. If you want +/// to move a file or directory from one place to another +/// you rename it from an old to a new path. +/// +/// If a file exists at the specified new path, depending +/// on whether \a overwrite is set to true, the rename operation +/// will overwrite it or not. Non-empty directories cannot be +/// overwritten by this operation. +/// +/// \param sftp SFTP object +/// \param oldPath Old path to the file or directory, encoded in UTF-8 +/// \param newPath New path to the file or directory, encoded in UTF-8 +/// \param overwrite True to allow overwriting a file that exists at \a newPath +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of the operation +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResult* sfSftp_rename( + sfSftp* sftp, + const char* oldPath, + const char* newPath, + bool overwrite, + sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Remove an existing file +/// +/// Use this function with caution, the file will be +/// removed permanently! +/// +/// \param sftp SFTP object +/// \param path Path to the file to remove, encoded in UTF-8 +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of removing the file +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResult* sfSftp_deleteFile(sfSftp* sftp, const char* path, sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Download a file from the server +/// +/// The file data is transferred in sequential blocks. For +/// every block of data transferred, the provided callback +/// is called. +/// +/// The function returns once all the data in the remote file +/// has been transferred or an error occurs or the function +/// times out. +/// +/// \param sftp SFTP object +/// \param remotePath Path of the remote file whose data to download, encoded in UTF-8 +/// \param callback Callback to be called for every available data block +/// \param userData User data that will be passed to the callback +/// \param offset Byte offset into the remote file at which reading should start +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of downloading the file +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResult* sfSftp_download( + sfSftp* sftp, + const char* remotePath, + sfSftpDownloadCallback callback, + void* userData, + uint64_t offset, + sfTimeoutWithPredicate timeout); + +//////////////////////////////////////////////////////////// +/// \brief Upload a file to the server +/// +/// The file data is transferred in sequential blocks. Every +/// time the function wants to send a new block of data the +/// provided callback is called. +/// +/// The function returns once all the data has been sent or +/// an error occurs or the function times out. +/// +/// If a file does not exist at the remote path yet, it will +/// be created with the provided permissions. A common +/// permissions value is 0644 (rw-r--r--). +/// +/// \param sftp SFTP object +/// \param remotePath Path of the remote file in which to upload the data, encoded in UTF-8 +/// \param callback Callback to be called for every data block to send +/// \param userData User data that will be passed to the callback +/// \param permissions POSIX permissions of the remote file if it has to be created +/// \param truncate True to truncate the remote file if it already exists +/// \param append True to append to the remote file if it already exists +/// \param offset Byte offset into the remote file at which writing should start +/// \param timeout Maximum time to wait, optionally a predicate can be provided for more fine-grained control +/// +/// \return Result of uploading the file +/// +//////////////////////////////////////////////////////////// +CSFML_NETWORK_API sfSftpResult* sfSftp_upload( + sfSftp* sftp, + const char* remotePath, + sfSftpUploadCallback callback, + void* userData, + uint32_t permissions, + bool truncate, + bool append, + uint64_t offset, + sfTimeoutWithPredicate timeout); diff --git a/include/CSFML/Network/Types.h b/include/CSFML/Network/Types.h index 0e6636db..fabbdcb0 100644 --- a/include/CSFML/Network/Types.h +++ b/include/CSFML/Network/Types.h @@ -37,6 +37,11 @@ typedef struct sfHttpRequest sfHttpRequest; typedef struct sfHttpResponse sfHttpResponse; typedef struct sfHttp sfHttp; typedef struct sfPacket sfPacket; +typedef struct sfSftpAttributesResult sfSftpAttributesResult; +typedef struct sfSftpListingResult sfSftpListingResult; +typedef struct sfSftpPathResult sfSftpPathResult; +typedef struct sfSftpResult sfSftpResult; +typedef struct sfSftp sfSftp; typedef struct sfSocketSelector sfSocketSelector; typedef struct sfTcpListener sfTcpListener; typedef struct sfTcpSocket sfTcpSocket; diff --git a/src/CSFML/Network/CMakeLists.txt b/src/CSFML/Network/CMakeLists.txt index f9fa582d..0de6ea41 100644 --- a/src/CSFML/Network/CMakeLists.txt +++ b/src/CSFML/Network/CMakeLists.txt @@ -19,6 +19,9 @@ set(SRC ${SRCROOT}/Packet.cpp ${SRCROOT}/PacketStruct.hpp ${INCROOT}/Packet.h + ${SRCROOT}/Sftp.cpp + ${SRCROOT}/SftpStruct.hpp + ${INCROOT}/Sftp.h ${SRCROOT}/SocketSelector.cpp ${SRCROOT}/SocketSelectorStruct.hpp ${INCROOT}/SocketSelector.h diff --git a/src/CSFML/Network/Sftp.cpp b/src/CSFML/Network/Sftp.cpp new file mode 100644 index 00000000..007d40b1 --- /dev/null +++ b/src/CSFML/Network/Sftp.cpp @@ -0,0 +1,547 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include +#include +#include +#include + +#include +#include +#include +#include + + +namespace +{ +//////////////////////////////////////////////////////////// +[[nodiscard]] std::filesystem::path toPath(const char* path) +{ + return std::filesystem::u8path(path ? path : ""); +} + + +//////////////////////////////////////////////////////////// +[[nodiscard]] std::string fromPath(const std::filesystem::path& path) +{ + const auto string = path.u8string(); + return {string.begin(), string.end()}; +} + + +//////////////////////////////////////////////////////////// +[[nodiscard]] sfSftpFileType convertFileType(std::filesystem::file_type type) +{ + switch (type) + { + case std::filesystem::file_type::none: + return sfSftpFileNone; + case std::filesystem::file_type::not_found: + return sfSftpFileNotFound; + case std::filesystem::file_type::regular: + return sfSftpFileRegular; + case std::filesystem::file_type::directory: + return sfSftpFileDirectory; + case std::filesystem::file_type::symlink: + return sfSftpFileSymlink; + case std::filesystem::file_type::block: + return sfSftpFileBlock; + case std::filesystem::file_type::character: + return sfSftpFileCharacter; + case std::filesystem::file_type::fifo: + return sfSftpFileFifo; + case std::filesystem::file_type::socket: + return sfSftpFileSocket; + default: + return sfSftpFileUnknown; + } +} + + +//////////////////////////////////////////////////////////// +// SFML stores the seconds since the Unix epoch in the file time +[[nodiscard]] std::int64_t convertFileTime(std::filesystem::file_time_type time) +{ + return std::chrono::duration_cast(time.time_since_epoch()).count(); +} + + +//////////////////////////////////////////////////////////// +[[nodiscard]] sfSftpAttributes convertAttributes(const sf::Sftp::Attributes& attributes, const std::string& path) +{ + sfSftpAttributes result{}; + result.path = path.c_str(); + + if (attributes.type) + { + result.hasType = true; + result.type = convertFileType(*attributes.type); + } + + if (attributes.size) + { + result.hasSize = true; + result.size = *attributes.size; + } + + if (attributes.permissions) + { + result.hasPermissions = true; + result.permissions = static_cast(*attributes.permissions); + } + + if (attributes.userId) + { + result.hasUserId = true; + result.userId = *attributes.userId; + } + + if (attributes.groupId) + { + result.hasGroupId = true; + result.groupId = *attributes.groupId; + } + + if (attributes.accessTime) + { + result.hasAccessTime = true; + result.accessTime = convertFileTime(*attributes.accessTime); + } + + if (attributes.modificationTime) + { + result.hasModificationTime = true; + result.modificationTime = convertFileTime(*attributes.modificationTime); + } + + return result; +} + + +//////////////////////////////////////////////////////////// +[[nodiscard]] sfSftpResult* createResult(const sf::Sftp::Result& result) +{ + return new sfSftpResult{result}; +} + + +//////////////////////////////////////////////////////////// +[[nodiscard]] sfSftpPathResult* createResult(const sf::Sftp::PathResult& result) +{ + return new sfSftpPathResult{result, fromPath(result.getPath())}; +} + + +//////////////////////////////////////////////////////////// +[[nodiscard]] sfSftpAttributesResult* createResult(const sf::Sftp::AttributesResult& result) +{ + return new sfSftpAttributesResult{result, fromPath(result.getAttributes().path)}; +} + + +//////////////////////////////////////////////////////////// +[[nodiscard]] sfSftpListingResult* createResult(const sf::Sftp::ListingResult& result) +{ + std::vector paths; + paths.reserve(result.getListing().size()); + for (const auto& attributes : result.getListing()) + paths.push_back(fromPath(attributes.path)); + + return new sfSftpListingResult{result, std::move(paths)}; +} +} // namespace + + +//////////////////////////////////////////////////////////// +void sfSftpResult_destroy(const sfSftpResult* result) +{ + delete result; +} + + +//////////////////////////////////////////////////////////// +bool sfSftpResult_isOk(const sfSftpResult* result) +{ + assert(result); + return result->isOk(); +} + + +//////////////////////////////////////////////////////////// +sfSftpResultValue sfSftpResult_getValue(const sfSftpResult* result) +{ + assert(result); + return static_cast(result->getValue()); +} + + +//////////////////////////////////////////////////////////// +const char* sfSftpResult_getMessage(const sfSftpResult* result) +{ + assert(result); + return result->getMessage().c_str(); +} + + +//////////////////////////////////////////////////////////// +void sfSftpPathResult_destroy(const sfSftpPathResult* result) +{ + delete result; +} + + +//////////////////////////////////////////////////////////// +bool sfSftpPathResult_isOk(const sfSftpPathResult* result) +{ + assert(result); + return result->isOk(); +} + + +//////////////////////////////////////////////////////////// +sfSftpResultValue sfSftpPathResult_getValue(const sfSftpPathResult* result) +{ + assert(result); + return static_cast(result->getValue()); +} + + +//////////////////////////////////////////////////////////// +const char* sfSftpPathResult_getMessage(const sfSftpPathResult* result) +{ + assert(result); + return result->getMessage().c_str(); +} + + +//////////////////////////////////////////////////////////// +const char* sfSftpPathResult_getPath(const sfSftpPathResult* result) +{ + assert(result); + return result->Path.c_str(); +} + + +//////////////////////////////////////////////////////////// +void sfSftpAttributesResult_destroy(const sfSftpAttributesResult* result) +{ + delete result; +} + + +//////////////////////////////////////////////////////////// +bool sfSftpAttributesResult_isOk(const sfSftpAttributesResult* result) +{ + assert(result); + return result->isOk(); +} + + +//////////////////////////////////////////////////////////// +sfSftpResultValue sfSftpAttributesResult_getValue(const sfSftpAttributesResult* result) +{ + assert(result); + return static_cast(result->getValue()); +} + + +//////////////////////////////////////////////////////////// +const char* sfSftpAttributesResult_getMessage(const sfSftpAttributesResult* result) +{ + assert(result); + return result->getMessage().c_str(); +} + + +//////////////////////////////////////////////////////////// +sfSftpAttributes sfSftpAttributesResult_getAttributes(const sfSftpAttributesResult* result) +{ + assert(result); + return convertAttributes(result->getAttributes(), result->Path); +} + + +//////////////////////////////////////////////////////////// +void sfSftpListingResult_destroy(const sfSftpListingResult* result) +{ + delete result; +} + + +//////////////////////////////////////////////////////////// +bool sfSftpListingResult_isOk(const sfSftpListingResult* result) +{ + assert(result); + return result->isOk(); +} + + +//////////////////////////////////////////////////////////// +sfSftpResultValue sfSftpListingResult_getValue(const sfSftpListingResult* result) +{ + assert(result); + return static_cast(result->getValue()); +} + + +//////////////////////////////////////////////////////////// +const char* sfSftpListingResult_getMessage(const sfSftpListingResult* result) +{ + assert(result); + return result->getMessage().c_str(); +} + + +//////////////////////////////////////////////////////////// +size_t sfSftpListingResult_getCount(const sfSftpListingResult* result) +{ + assert(result); + return result->getListing().size(); +} + + +//////////////////////////////////////////////////////////// +sfSftpAttributes sfSftpListingResult_getAttributes(const sfSftpListingResult* result, size_t index) +{ + assert(result); + assert(index < result->getListing().size()); + return convertAttributes(result->getListing()[index], result->Paths[index]); +} + + +//////////////////////////////////////////////////////////// +sfSftp* sfSftp_create() +{ + return new sfSftp; +} + + +//////////////////////////////////////////////////////////// +void sfSftp_destroy(const sfSftp* sftp) +{ + delete sftp; +} + + +//////////////////////////////////////////////////////////// +sfSftpResult* sfSftp_connect(sfSftp* sftp, sfIpAddress server, unsigned short port, sfTimeoutWithPredicate timeout) +{ + assert(sftp); + + const auto address = convertIpAddress(server); + if (!address) + return createResult(sf::Sftp::Result(sf::Sftp::Result::Value::Error, "Invalid server address")); + + return createResult(sftp->connect(*address, port, convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpResult* sfSftp_disconnect(sfSftp* sftp, sfTimeoutWithPredicate timeout) +{ + assert(sftp); + return createResult(sftp->disconnect(convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +bool sfSftp_getSessionInfo(const sfSftp* sftp, sfSftpSessionInfo* info) +{ + assert(sftp); + assert(info); + + sftp->SessionInfo = sftp->getSessionInfo(); + if (!sftp->SessionInfo) + return false; + + const auto& sessionInfo = *sftp->SessionInfo; + const auto& hostKey = sessionInfo.hostKey; + + *info = {}; + info->hostKeyType = static_cast(hostKey.type); + info->hostKeyData = reinterpret_cast(hostKey.data.data()); + info->hostKeySize = hostKey.data.size(); + std::transform(hostKey.sha1.begin(), + hostKey.sha1.end(), + info->hostKeySha1, + [](std::byte b) { return std::to_integer(b); }); + std::transform(hostKey.sha256.begin(), + hostKey.sha256.end(), + info->hostKeySha256, + [](std::byte b) { return std::to_integer(b); }); + info->keyExchangeAlgorithm = sessionInfo.keyExchangeAlgorithm.c_str(); + info->hostKeyAlgorithm = sessionInfo.hostKeyAlgorithm.c_str(); + info->clientToServerEncryptionAlgorithm = sessionInfo.clientToServerEncryptionAlgorithm.c_str(); + info->serverToClientEncryptionAlgorithm = sessionInfo.serverToClientEncryptionAlgorithm.c_str(); + info->clientToServerMacAlgorithm = sessionInfo.clientToServerMacAlgorithm.c_str(); + info->serverToClientMacAlgorithm = sessionInfo.serverToClientMacAlgorithm.c_str(); + info->clientToServerCompressionAlgorithm = sessionInfo.clientToServerCompressionAlgorithm.c_str(); + info->serverToClientCompressionAlgorithm = sessionInfo.serverToClientCompressionAlgorithm.c_str(); + return true; +} + + +//////////////////////////////////////////////////////////// +sfSftpResult* sfSftp_login(sfSftp* sftp, const char* name, const char* password, sfTimeoutWithPredicate timeout) +{ + assert(sftp); + return createResult(sftp->login(name ? name : "", password ? password : "", convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpResult* sfSftp_loginWithKey( + sfSftp* sftp, + const char* name, + const char* publicKeyData, + size_t publicKeyLength, + const char* privateKeyData, + size_t privateKeyLength, + const char* privateKeyPassphrase, + sfTimeoutWithPredicate timeout) +{ + assert(sftp); + assert(publicKeyData); + assert(privateKeyData); + return createResult( + sftp->login(name ? name : "", + publicKeyData, + publicKeyLength, + privateKeyData, + privateKeyLength, + privateKeyPassphrase ? privateKeyPassphrase : "", + convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpPathResult* sfSftp_resolvePath(sfSftp* sftp, const char* path, sfTimeoutWithPredicate timeout) +{ + assert(sftp); + return createResult(sftp->resolvePath(toPath(path), convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpPathResult* sfSftp_getWorkingDirectory(sfSftp* sftp, sfTimeoutWithPredicate timeout) +{ + assert(sftp); + return createResult(sftp->getWorkingDirectory(convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpAttributesResult* sfSftp_getAttributes(sfSftp* sftp, const char* path, bool followLinks, sfTimeoutWithPredicate timeout) +{ + assert(sftp); + return createResult(sftp->getAttributes(toPath(path), followLinks, convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpListingResult* sfSftp_getDirectoryListing(sfSftp* sftp, const char* path, sfTimeoutWithPredicate timeout) +{ + assert(sftp); + return createResult(sftp->getDirectoryListing(toPath(path), convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpResult* sfSftp_createDirectory(sfSftp* sftp, const char* path, uint32_t permissions, sfTimeoutWithPredicate timeout) +{ + assert(sftp); + return createResult(sftp->createDirectory(toPath(path), + static_cast(permissions), + convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpResult* sfSftp_deleteDirectory(sfSftp* sftp, const char* path, sfTimeoutWithPredicate timeout) +{ + assert(sftp); + return createResult(sftp->deleteDirectory(toPath(path), convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpResult* sfSftp_rename(sfSftp* sftp, const char* oldPath, const char* newPath, bool overwrite, sfTimeoutWithPredicate timeout) +{ + assert(sftp); + return createResult(sftp->rename(toPath(oldPath), toPath(newPath), overwrite, convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpResult* sfSftp_deleteFile(sfSftp* sftp, const char* path, sfTimeoutWithPredicate timeout) +{ + assert(sftp); + return createResult(sftp->deleteFile(toPath(path), convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpResult* sfSftp_download(sfSftp* sftp, + const char* remotePath, + sfSftpDownloadCallback callback, + void* userData, + uint64_t offset, + sfTimeoutWithPredicate timeout) +{ + assert(sftp); + assert(callback); + return createResult(sftp->download( + toPath(remotePath), + [callback, userData](const void* data, std::size_t size) { return callback(data, size, userData); }, + offset, + convertTimeoutWithPredicate(timeout))); +} + + +//////////////////////////////////////////////////////////// +sfSftpResult* sfSftp_upload( + sfSftp* sftp, + const char* remotePath, + sfSftpUploadCallback callback, + void* userData, + uint32_t permissions, + bool truncate, + bool append, + uint64_t offset, + sfTimeoutWithPredicate timeout) +{ + assert(sftp); + assert(callback); + return createResult(sftp->upload( + toPath(remotePath), + [callback, userData](void* data, std::size_t& size) { return callback(data, &size, userData); }, + static_cast(permissions), + truncate, + append, + offset, + convertTimeoutWithPredicate(timeout))); +} diff --git a/src/CSFML/Network/SftpStruct.hpp b/src/CSFML/Network/SftpStruct.hpp new file mode 100644 index 00000000..3c411543 --- /dev/null +++ b/src/CSFML/Network/SftpStruct.hpp @@ -0,0 +1,78 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include +#include +#include + + +//////////////////////////////////////////////////////////// +// Internal structure of sfSftp +//////////////////////////////////////////////////////////// +struct sfSftp : sf::Sftp +{ + mutable std::optional SessionInfo; +}; + + +//////////////////////////////////////////////////////////// +// Internal structure of sfSftpResult +//////////////////////////////////////////////////////////// +struct sfSftpResult : sf::Sftp::Result +{ +}; + + +//////////////////////////////////////////////////////////// +// Internal structure of sfSftpPathResult +//////////////////////////////////////////////////////////// +struct sfSftpPathResult : sf::Sftp::PathResult +{ + std::string Path; +}; + + +//////////////////////////////////////////////////////////// +// Internal structure of sfSftpAttributesResult +//////////////////////////////////////////////////////////// +struct sfSftpAttributesResult : sf::Sftp::AttributesResult +{ + std::string Path; +}; + + +//////////////////////////////////////////////////////////// +// Internal structure of sfSftpListingResult +//////////////////////////////////////////////////////////// +struct sfSftpListingResult : sf::Sftp::ListingResult +{ + std::vector Paths; +}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 927cda70..d76e689b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -70,6 +70,7 @@ add_executable(test-csfml-network Network/Ftp.test.cpp Network/Http.test.cpp Network/IpAddress.test.cpp + Network/Sftp.test.cpp Network/SocketSelector.test.cpp Network/SocketStatus.test.cpp Network/TcpSocket.test.cpp diff --git a/test/Network/Sftp.test.cpp b/test/Network/Sftp.test.cpp new file mode 100644 index 00000000..f4c5ca94 --- /dev/null +++ b/test/Network/Sftp.test.cpp @@ -0,0 +1,106 @@ +#include + +#include + +#include + +TEST_CASE("[Network] sfSftp") +{ + SECTION("sfSftpResultValue") + { + STATIC_CHECK(sfSftpSuccess == static_cast(sf::Sftp::Result::Value::Success)); + STATIC_CHECK(sfSftpDisconnected == static_cast(sf::Sftp::Result::Value::Disconnected)); + STATIC_CHECK(sfSftpTimeout == static_cast(sf::Sftp::Result::Value::Timeout)); + STATIC_CHECK(sfSftpRefused == static_cast(sf::Sftp::Result::Value::Refused)); + STATIC_CHECK(sfSftpError == static_cast(sf::Sftp::Result::Value::Error)); + STATIC_CHECK(sfSftpBannerReceive == static_cast(sf::Sftp::Result::Value::BannerReceive)); + STATIC_CHECK(sfSftpBannerSend == static_cast(sf::Sftp::Result::Value::BannerSend)); + STATIC_CHECK(sfSftpInvalidMac == static_cast(sf::Sftp::Result::Value::InvalidMac)); + STATIC_CHECK(sfSftpAllocationFailure == static_cast(sf::Sftp::Result::Value::AllocationFailure)); + STATIC_CHECK(sfSftpSocketSend == static_cast(sf::Sftp::Result::Value::SocketSend)); + STATIC_CHECK(sfSftpKeyExchangeFailure == static_cast(sf::Sftp::Result::Value::KeyExchangeFailure)); + STATIC_CHECK(sfSftpHostKeyInitialization == static_cast(sf::Sftp::Result::Value::HostKeyInitialization)); + STATIC_CHECK(sfSftpHostKeySign == static_cast(sf::Sftp::Result::Value::HostKeySign)); + STATIC_CHECK(sfSftpDecryptError == static_cast(sf::Sftp::Result::Value::DecryptError)); + STATIC_CHECK(sfSftpProtocolError == static_cast(sf::Sftp::Result::Value::ProtocolError)); + STATIC_CHECK(sfSftpPasswordExpired == static_cast(sf::Sftp::Result::Value::PasswordExpired)); + STATIC_CHECK(sfSftpFileError == static_cast(sf::Sftp::Result::Value::FileError)); + STATIC_CHECK(sfSftpMethodNone == static_cast(sf::Sftp::Result::Value::MethodNone)); + STATIC_CHECK(sfSftpAuthenticationFailed == static_cast(sf::Sftp::Result::Value::AuthenticationFailed)); + STATIC_CHECK(sfSftpPublicKeyUnverified == static_cast(sf::Sftp::Result::Value::PublicKeyUnverified)); + STATIC_CHECK(sfSftpChannelOutOfOrder == static_cast(sf::Sftp::Result::Value::ChannelOutOfOrder)); + STATIC_CHECK(sfSftpChannelFailure == static_cast(sf::Sftp::Result::Value::ChannelFailure)); + STATIC_CHECK(sfSftpChannelRequestDenied == static_cast(sf::Sftp::Result::Value::ChannelRequestDenied)); + STATIC_CHECK(sfSftpChannelUnknown == static_cast(sf::Sftp::Result::Value::ChannelUnknown)); + STATIC_CHECK(sfSftpChannelWindowExceeded == static_cast(sf::Sftp::Result::Value::ChannelWindowExceeded)); + STATIC_CHECK(sfSftpChannelPacketExceeded == static_cast(sf::Sftp::Result::Value::ChannelPacketExceeded)); + STATIC_CHECK(sfSftpChannelClosed == static_cast(sf::Sftp::Result::Value::ChannelClosed)); + STATIC_CHECK(sfSftpChannelEofSent == static_cast(sf::Sftp::Result::Value::ChannelEofSent)); + STATIC_CHECK(sfSftpScpProtocol == static_cast(sf::Sftp::Result::Value::ScpProtocol)); + STATIC_CHECK(sfSftpZlibError == static_cast(sf::Sftp::Result::Value::ZlibError)); + STATIC_CHECK(sfSftpRequestDenied == static_cast(sf::Sftp::Result::Value::RequestDenied)); + STATIC_CHECK(sfSftpMethodNotSupported == static_cast(sf::Sftp::Result::Value::MethodNotSupported)); + STATIC_CHECK(sfSftpInvalidData == static_cast(sf::Sftp::Result::Value::InvalidData)); + STATIC_CHECK(sfSftpPublicKeyProtocol == static_cast(sf::Sftp::Result::Value::PublicKeyProtocol)); + STATIC_CHECK(sfSftpBufferTooSmall == static_cast(sf::Sftp::Result::Value::BufferTooSmall)); + STATIC_CHECK(sfSftpBadUse == static_cast(sf::Sftp::Result::Value::BadUse)); + STATIC_CHECK(sfSftpCompressError == static_cast(sf::Sftp::Result::Value::CompressError)); + STATIC_CHECK(sfSftpOutOfBoundary == static_cast(sf::Sftp::Result::Value::OutOfBoundary)); + STATIC_CHECK(sfSftpAgentProtocol == static_cast(sf::Sftp::Result::Value::AgentProtocol)); + STATIC_CHECK(sfSftpSocketRecv == static_cast(sf::Sftp::Result::Value::SocketRecv)); + STATIC_CHECK(sfSftpEncryptError == static_cast(sf::Sftp::Result::Value::EncryptError)); + STATIC_CHECK(sfSftpBadSocket == static_cast(sf::Sftp::Result::Value::BadSocket)); + STATIC_CHECK(sfSftpKnownHosts == static_cast(sf::Sftp::Result::Value::KnownHosts)); + STATIC_CHECK(sfSftpChannelWindowFull == static_cast(sf::Sftp::Result::Value::ChannelWindowFull)); + STATIC_CHECK(sfSftpKeyFileAuthenticationFailed == + static_cast(sf::Sftp::Result::Value::KeyFileAuthenticationFailed)); + STATIC_CHECK(sfSftpEndOfFile == static_cast(sf::Sftp::Result::Value::EndOfFile)); + STATIC_CHECK(sfSftpNoSuchFile == static_cast(sf::Sftp::Result::Value::NoSuchFile)); + STATIC_CHECK(sfSftpPermissionDenied == static_cast(sf::Sftp::Result::Value::PermissionDenied)); + STATIC_CHECK(sfSftpFailure == static_cast(sf::Sftp::Result::Value::Failure)); + STATIC_CHECK(sfSftpBadMessage == static_cast(sf::Sftp::Result::Value::BadMessage)); + STATIC_CHECK(sfSftpNoConnection == static_cast(sf::Sftp::Result::Value::NoConnection)); + STATIC_CHECK(sfSftpConnectionLost == static_cast(sf::Sftp::Result::Value::ConnectionLost)); + STATIC_CHECK(sfSftpOperationUnsupported == static_cast(sf::Sftp::Result::Value::OperationUnsupported)); + STATIC_CHECK(sfSftpInvalidHandle == static_cast(sf::Sftp::Result::Value::InvalidHandle)); + STATIC_CHECK(sfSftpNoSuchPath == static_cast(sf::Sftp::Result::Value::NoSuchPath)); + STATIC_CHECK(sfSftpFileAlreadyExists == static_cast(sf::Sftp::Result::Value::FileAlreadyExists)); + STATIC_CHECK(sfSftpWriteProtect == static_cast(sf::Sftp::Result::Value::WriteProtect)); + STATIC_CHECK(sfSftpNoMedia == static_cast(sf::Sftp::Result::Value::NoMedia)); + STATIC_CHECK(sfSftpNoSpaceOnFileSystem == static_cast(sf::Sftp::Result::Value::NoSpaceOnFileSystem)); + STATIC_CHECK(sfSftpQuotaExceeded == static_cast(sf::Sftp::Result::Value::QuotaExceeded)); + STATIC_CHECK(sfSftpUnknownPrincipal == static_cast(sf::Sftp::Result::Value::UnknownPrincipal)); + STATIC_CHECK(sfSftpLockConflict == static_cast(sf::Sftp::Result::Value::LockConflict)); + STATIC_CHECK(sfSftpDirectoryNotEmpty == static_cast(sf::Sftp::Result::Value::DirectoryNotEmpty)); + STATIC_CHECK(sfSftpNotADirectory == static_cast(sf::Sftp::Result::Value::NotADirectory)); + STATIC_CHECK(sfSftpInvalidFilename == static_cast(sf::Sftp::Result::Value::InvalidFilename)); + STATIC_CHECK(sfSftpLinkLoop == static_cast(sf::Sftp::Result::Value::LinkLoop)); + STATIC_CHECK(sfSftpSftpError == static_cast(sf::Sftp::Result::Value::SftpError)); + } + + SECTION("sfSftpHostKeyType") + { + using HostKeyType = sf::Sftp::SessionInfo::HostKey::Type; + STATIC_CHECK(sfSftpHostKeyUnknown == static_cast(HostKeyType::Unknown)); + STATIC_CHECK(sfSftpHostKeyRsa == static_cast(HostKeyType::Rsa)); + STATIC_CHECK(sfSftpHostKeyDsa == static_cast(HostKeyType::Dsa)); + STATIC_CHECK(sfSftpHostKeyEcdsa256 == static_cast(HostKeyType::Ecdsa256)); + STATIC_CHECK(sfSftpHostKeyEcdsa384 == static_cast(HostKeyType::Ecdsa384)); + STATIC_CHECK(sfSftpHostKeyEcdsa521 == static_cast(HostKeyType::Ecdsa521)); + STATIC_CHECK(sfSftpHostKeyEd25519 == static_cast(HostKeyType::Ed25519)); + } + + SECTION("Unconnected") + { + sfSftp* sftp = sfSftp_create(); + sfSftpSessionInfo info{}; + CHECK(!sfSftp_getSessionInfo(sftp, &info)); + + sfSftpResult* result = sfSftp_connect(sftp, sfIpAddress_None, 22, {}); + CHECK(!sfSftpResult_isOk(result)); + CHECK(sfSftpResult_getValue(result) == sfSftpError); + sfSftpResult_destroy(result); + + sfSftp_destroy(sftp); + } +} From 904c98b058ef0d11634badbca3b59133c9d24084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20D=C3=BCrrenberger?= Date: Sat, 3 Oct 2026 12:46:30 +0200 Subject: [PATCH 15/15] Fix text creation in main page example --- doc/mainpage.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/mainpage.hpp b/doc/mainpage.hpp index 5ba955a5..08015b5e 100644 --- a/doc/mainpage.hpp +++ b/doc/mainpage.hpp @@ -42,9 +42,8 @@ /// font = sfFont_createFromFile("arial.ttf"); /// if (!font) /// return EXIT_FAILURE; -/// text = sfText_create(); +/// text = sfText_create(font); /// sfText_setString(text, "Hello SFML"); -/// sfText_setFont(text, font); /// sfText_setCharacterSize(text, 50); /// /// /* Load a music to play */