diff --git a/.github/workflows/ci-debian.yml b/.github/workflows/ci-debian.yml index c705981107f565dd775c389758e444f2da22812f..88b3f2d557c324e2b00aae53bb7d8638cd5e3f0a 100644 --- a/.github/workflows/ci-debian.yml +++ b/.github/workflows/ci-debian.yml @@ -51,7 +51,7 @@ jobs: - uses: xmake-io/github-action-setup-xmake@v1 with: - xmake-version: v2.8.7 + xmake-version: v2.9.9 actions-cache-folder: '.xmake-cache' - name: xmake repo --update diff --git a/.github/workflows/ci-fedora.yml b/.github/workflows/ci-fedora.yml index 8216a0bc0bd2254e19bdbb332bcd371129f63660..fda7c09bce5c5630280d8df7ee1ec0d3b553b1ec 100644 --- a/.github/workflows/ci-fedora.yml +++ b/.github/workflows/ci-fedora.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: | sudo dnf update -y - sudo dnf install -y gcc g++ xmake git unzip curl + sudo dnf install -y gcc g++ xmake git unzip curl perl - uses: actions/checkout@v3 with: fetch-depth: 1 diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index c47c8cce328fbb985260ae5160ce057e053d40e7..61d5ff5f955babb67a31b1a17b1896655e423327 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -43,7 +43,7 @@ jobs: - uses: xmake-io/github-action-setup-xmake@v1 with: - xmake-version: v2.8.7 + xmake-version: v2.9.9 actions-cache-folder: '.xmake-cache' - name: xmake repo --update diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index 23aeeaa8a24907bc905a22bcea90639ddcc1d6db..fb3e8649310e1f36956df366ae913264d0389dfe 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -32,7 +32,7 @@ jobs: steps: - uses: xmake-io/github-action-setup-xmake@v1 with: - xmake-version: v2.8.9 + xmake-version: v2.9.9 - name: update repo run: xmake repo -u - name: git crlf diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000000000000000000000000000000000000..7578bded0c102ac04ba8be8e7a52d8d19336d0b0 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,89 @@ +name: XPack Build + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: true + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Install Dependencies + run: | + sudo add-apt-repository ppa:xmake-io/xmake + sudo apt update + sudo apt install -y git \ + curl 7zip unzip curl \ + gcc g++ \ + devscripts debhelper build-essential \ + xmake # xmake must be installed by apt (dpkg) + + - name: git add safe directory + run: git config --global --add safe.directory '*' + + + - name: set XMAKE_GLOBALDIR + run: echo "XMAKE_GLOBALDIR=${{ runner.workspace }}/xmake-global" >> $GITHUB_ENV + + - name: xmake repo --update + run: xmake repo --update + + - name: cache packages from xrepo + uses: actions/cache@v3 + with: + path: | + ${{ env.XMAKE_GLOBALDIR }}/.xmake/packages + key: ${{ runner.os }}-xrepo-${{ hashFiles('**/xmake.lua') }} + + - name: Config and Build + run: | + sed -i 's|"deb", "rpm", "srpm"|"deb"|g' xmake.lua + sed -i 's|set_default(false) -- repl-anchor|set_default(true)|g' xmake.lua + + xmake config -vyD --policies=build.ccache -o tmp/build -m releasedbg --repl=true + xmake build goldfish + + - name: Package + run: xmake pack -vyD goldfish + + - name: Create summary table + run: | + echo "## 生成的包文件" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| 类型 | 文件名 | 大小 | 版本 | 架构 |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|------|------|------|" >> $GITHUB_STEP_SUMMARY + + DIR="tmp/build/xpack/goldfish" + + # Debian 包 + for deb in "$DIR"/*.deb; do + if [ -f "$deb" ]; then + size=$(ls -lh "$deb" | awk '{print $5}') + info=$(dpkg-deb -f "$deb" 2>/dev/null) + pkg=$(echo "$info" | grep "^Package" | cut -d' ' -f2- || echo "") + ver=$(echo "$info" | grep "^Version" | cut -d' ' -f2- || echo "") + arch=$(echo "$info" | grep "^Architecture" | cut -d' ' -f2- || echo "") + + echo "| Debian | \`$(basename "$deb")\` | $size | $ver | $arch |" >> $GITHUB_STEP_SUMMARY + fi + done + + echo "" >> $GITHUB_STEP_SUMMARY + echo "*目录: \`$DIR\`*" >> $GITHUB_STEP_SUMMARY + + - name: Upload all package artifacts + uses: actions/upload-artifact@v4 + with: + name: goldfish-packages + path: tmp/build/xpack/goldfish/*.deb + retention-days: 30 + if-no-files-found: error diff --git a/3rdparty/cpr/CMakeLists.txt b/3rdparty/cpr/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..30208bcd5caa6c4c0910e6c839f5e6ec8cc9cefc --- /dev/null +++ b/3rdparty/cpr/CMakeLists.txt @@ -0,0 +1,400 @@ +cmake_minimum_required(VERSION 3.15) +project(cpr VERSION 1.11.1 LANGUAGES CXX) + +math(EXPR cpr_VERSION_NUM "${cpr_VERSION_MAJOR} * 0x10000 + ${cpr_VERSION_MINOR} * 0x100 + ${cpr_VERSION_PATCH}" OUTPUT_FORMAT HEXADECIMAL) +configure_file("${cpr_SOURCE_DIR}/cmake/cprver.h.in" "${cpr_BINARY_DIR}/cpr_generated_includes/cpr/cprver.h") + +# Only change the folder behaviour if cpr is not a subproject +if(${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME}) + set_property(GLOBAL PROPERTY USE_FOLDERS ON) + set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "CMake") + set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) + set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/lib) +else() + # Check required c++ standard of parent project + if(CMAKE_CXX_STANDARD) + set(PARENT_CXX_STANDARD ${CMAKE_CXX_STANDARD}) + message(STATUS "CXX standard of parent project: ${PARENT_CXX_STANDARD}") + endif() +endif() + +# Avoid the dll boilerplate code for windows +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) + +if (PARENT_CXX_STANDARD) + # Don't set CMAKE_CXX_STANDARD if it is already set by parent project + if (PARENT_CXX_STANDARD LESS 17) + message(FATAL_ERROR "cpr ${cpr_VERSION} does not support ${PARENT_CXX_STANDARD}. Please use cpr <= 1.9.x") + endif() +else() + # Set standard version if not already set by potential parent project + set(CMAKE_CXX_STANDARD 17) +endif() + +message(STATUS "CXX standard: ${CMAKE_CXX_STANDARD}") +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CPR_LIBRARIES cpr CACHE INTERNAL "") + +macro(cpr_option OPTION_NAME OPTION_TEXT OPTION_DEFAULT) + option(${OPTION_NAME} ${OPTION_TEXT} ${OPTION_DEFAULT}) + if(DEFINED ENV{${OPTION_NAME}}) + # Allow overriding the option through an environment variable + set(${OPTION_NAME} $ENV{${OPTION_NAME}}) + endif() + if(${OPTION_NAME}) + add_definitions(-D${OPTION_NAME}) + endif() + message(STATUS " ${OPTION_NAME}: ${${OPTION_NAME}}") +endmacro() + +message(STATUS "C++ Requests CMake Options") +message(STATUS "=======================================================") +cpr_option(CPR_GENERATE_COVERAGE "Set to ON to generate coverage reports." OFF) +cpr_option(CPR_CURL_NOSIGNAL "Set to ON to disable use of signals in libcurl." OFF) +cpr_option(CURL_VERBOSE_LOGGING "Curl verbose logging during building curl" OFF) +cpr_option(CPR_USE_SYSTEM_GTEST "If ON, this project will look in the system paths for an installed gtest library. If none is found it will use the built-in one." OFF) +cpr_option(CPR_USE_SYSTEM_CURL "If enabled we will use the curl lib already installed on this system." OFF) +cpr_option(CPR_ENABLE_CURL_HTTP_ONLY "If enabled we will only use the HTTP/HTTPS protocols from CURL. If disabled, all the CURL protocols are enabled. This is useful if your project uses libcurl and you need support for other CURL features e.g. sending emails." ON) +cpr_option(CPR_ENABLE_SSL "Enables or disables the SSL backend. Required to perform HTTPS requests." ON) +cpr_option(CPR_FORCE_OPENSSL_BACKEND "Force to use the OpenSSL backend. If CPR_FORCE_OPENSSL_BACKEND, CPR_FORCE_DARWINSSL_BACKEND, CPR_FORCE_MBEDTLS_BACKEND, and CPR_FORCE_WINSSL_BACKEND are set to to OFF, cpr will try to automatically detect the best available SSL backend (WinSSL - Windows, OpenSSL - Linux, DarwinSSL - Mac ...)." OFF) +cpr_option(CPR_FORCE_WINSSL_BACKEND "Force to use the WinSSL backend. If CPR_FORCE_OPENSSL_BACKEND, CPR_FORCE_DARWINSSL_BACKEND, CPR_FORCE_MBEDTLS_BACKEND, and CPR_FORCE_WINSSL_BACKEND are set to to OFF, cpr will try to automatically detect the best available SSL backend (WinSSL - Windows, OpenSSL - Linux, DarwinSSL - Mac ...)." OFF) +cpr_option(CPR_FORCE_DARWINSSL_BACKEND "Force to use the DarwinSSL backend. If CPR_FORCE_OPENSSL_BACKEND, CPR_FORCE_DARWINSSL_BACKEND, CPR_FORCE_MBEDTLS_BACKEND, and CPR_FORCE_WINSSL_BACKEND are set to to OFF, cpr will try to automatically detect the best available SSL backend (WinSSL - Windows, OpenSSL - Linux, DarwinSSL - Mac ...)." OFF) +cpr_option(CPR_FORCE_MBEDTLS_BACKEND "Force to use the Mbed TLS backend. If CPR_FORCE_OPENSSL_BACKEND, CPR_FORCE_DARWINSSL_BACKEND, CPR_FORCE_MBEDTLS_BACKEND, and CPR_FORCE_WINSSL_BACKEND are set to to OFF, cpr will try to automatically detect the best available SSL backend (WinSSL - Windows, OpenSSL - Linux, DarwinSSL - Mac ...)." OFF) +cpr_option(CPR_ENABLE_LINTING "Set to ON to enable clang linting." OFF) +cpr_option(CPR_ENABLE_CPPCHECK "Set to ON to enable Cppcheck static analysis. Requires CPR_BUILD_TESTS and CPR_BUILD_TESTS_SSL to be OFF to prevent checking google tests source code." OFF) +cpr_option(CPR_BUILD_TESTS "Set to ON to build cpr tests." OFF) +cpr_option(CPR_BUILD_TESTS_SSL "Set to ON to build cpr ssl tests" ${CPR_BUILD_TESTS}) +cpr_option(CPR_BUILD_TESTS_PROXY "Set to ON to build proxy tests. They fail in case there is no valid proxy server available in proxy_tests.cpp" OFF) +cpr_option(CPR_BUILD_VERSION_OUTPUT_ONLY "Set to ON to only export the version into 'build/version.txt' and exit" OFF) +cpr_option(CPR_SKIP_CA_BUNDLE_SEARCH "Skip searching for Certificate Authority certs. Turn ON for systems like iOS where file access is restricted and prevents https from working." OFF) +cpr_option(CPR_USE_BOOST_FILESYSTEM "Set to ON to use the Boost.Filesystem library. This is useful, on, e.g., Apple platforms, where std::filesystem may not always be available when targeting older OS versions." OFF) +cpr_option(CPR_DEBUG_SANITIZER_FLAG_THREAD "Enables the ThreadSanitizer for debug builds." OFF) +cpr_option(CPR_DEBUG_SANITIZER_FLAG_ADDR "Enables the AddressSanitizer for debug builds." OFF) +cpr_option(CPR_DEBUG_SANITIZER_FLAG_LEAK "Enables the LeakSanitizer for debug builds." OFF) +cpr_option(CPR_DEBUG_SANITIZER_FLAG_UB "Enables the UndefinedBehaviorSanitizer for debug builds." OFF) +cpr_option(CPR_DEBUG_SANITIZER_FLAG_ALL "Enables all sanitizers for debug builds except the ThreadSanitizer since it is incompatible with the other sanitizers." OFF) +message(STATUS "=======================================================") + +# Save the project version as txt file for deb and NuGet builds +if(CPR_BUILD_VERSION_OUTPUT_ONLY) + message(STATUS "Printing version and exiting...") + file(WRITE "${CMAKE_BINARY_DIR}/version.txt" "${PROJECT_VERSION}") + return() +endif() + +if (CPR_FORCE_USE_SYSTEM_CURL) + message(WARNING "The variable CPR_FORCE_USE_SYSTEM_CURL is deprecated, please use CPR_USE_SYSTEM_CURL instead") + set(CPR_USE_SYSTEM_CURL ${CPR_FORCE_USE_SYSTEM_CURL}) +endif() + +include(GNUInstallDirs) +include(FetchContent) +include(cmake/code_coverage.cmake) +include(cmake/sanitizer.cmake) +include(cmake/clear_variable.cmake) + +# So CMake can find FindMbedTLS.cmake +set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_MODULE_PATH}") + +# Linting +if(CPR_ENABLE_LINTING) + include(cmake/clang-tidy.cmake) +endif() + +# Cppcheck +if(CPR_ENABLE_CPPCHECK) + if(CPR_BUILD_TESTS OR CPR_BUILD_TESTS_SSL) + message(FATAL_ERROR "Cppcheck is incompatible with building tests. Make sure to disable CPR_ENABLE_CPPCHECK or disable tests by setting CPR_BUILD_TESTS and CPR_BUILD_TESTS_SSL to OFF. This is because Cppcheck would try to check the google tests source code and then fail. ") + endif() + include(cmake/cppcheck.cmake) +endif() + +# SSL +if(CPR_ENABLE_SSL) + if(CPR_FORCE_OPENSSL_BACKEND OR CPR_FORCE_WINSSL_BACKEND OR CPR_FORCE_DARWINSSL_BACKEND OR CPR_FORCE_MBEDTLS_BACKEND) + message(STATUS "Disabled SSL backend auto detect since either CPR_FORCE_OPENSSL_BACKEND, CPR_FORCE_DARWINSSL_BACKEND, CPR_FORCE_MBEDTLS_BACKEND, or CPR_FORCE_WINSSL_BACKEND is enabled.") + set(DETECT_SSL_BACKEND OFF CACHE INTERNAL "" FORCE) + else() + message(STATUS "Automatically detecting SSL backend.") + set(DETECT_SSL_BACKEND ON CACHE INTERNAL "" FORCE) + endif() + + if(CPR_FORCE_WINSSL_BACKEND AND (NOT WIN32)) + message(FATAL_ERROR "WinSSL is only available on Windows! Use either OpenSSL (CPR_FORCE_OPENSSL_BACKEND) or DarwinSSL (CPR_FORCE_DARWINSSL_BACKEND) instead.") + endif() + + if(DETECT_SSL_BACKEND) + message(STATUS "Detecting SSL backend...") + if(WIN32) + message(STATUS "SSL auto detect: Using WinSSL.") + set(SSL_BACKEND_USED "WinSSL") + elseif(APPLE) + message(STATUS "SSL auto detect: Using DarwinSSL.") + set(CPR_BUILD_TESTS_SSL OFF) + set(SSL_BACKEND_USED "DarwinSSL") + else() + find_package(OpenSSL) + if(OPENSSL_FOUND) + message(STATUS "SSL auto detect: Using OpenSSL.") + set(SSL_BACKEND_USED "OpenSSL") + else() + find_package(MbedTLS) + if(MBEDTLS_FOUND) + set(SSL_BACKEND_USED "MbedTLS") + else() + message(FATAL_ERROR "No valid SSL backend found! Please install OpenSSL, Mbed TLS or disable SSL by setting CPR_ENABLE_SSL to OFF.") + endif() + endif() + endif() + else() + if(CPR_FORCE_OPENSSL_BACKEND) + find_package(OpenSSL) + if(OPENSSL_FOUND) + message(STATUS "Using OpenSSL.") + set(SSL_BACKEND_USED "OpenSSL") + else() + message(FATAL_ERROR "CPR_FORCE_OPENSSL_BACKEND enabled but we were not able to find OpenSSL!") + endif() + elseif(CPR_FORCE_WINSSL_BACKEND) + message(STATUS "Using WinSSL.") + set(SSL_BACKEND_USED "WinSSL") + elseif(CPR_FORCE_DARWINSSL_BACKEND) + message(STATUS "Using DarwinSSL.") + set(CPR_BUILD_TESTS_SSL OFF) + set(SSL_BACKEND_USED "DarwinSSL") + elseif(CPR_FORCE_MBEDTLS_BACKEND) + message(STATUS "Using Mbed TLS.") + set(CPR_BUILD_TESTS_SSL OFF) + set(SSL_BACKEND_USED "MbedTLS") + endif() + endif() +endif() + +if(SSL_BACKEND_USED STREQUAL "OpenSSL") +# Fix missing OpenSSL includes for Windows since in 'ssl_ctx.cpp' we include OpenSSL directly +find_package(OpenSSL REQUIRED) + add_compile_definitions(OPENSSL_BACKEND_USED) +endif() + +# Curl configuration +if(CPR_USE_SYSTEM_CURL) + if(CPR_ENABLE_SSL) + find_package(CURL COMPONENTS HTTP HTTPS) + if(CURL_FOUND) + message(STATUS "Curl ${CURL_VERSION_STRING} found on this system.") + + # To be able to load certificates under Windows when using OpenSSL: + if(CMAKE_USE_OPENSSL AND WIN32 AND (NOT (CURL_VERSION_STRING VERSION_GREATER_EQUAL "7.71.0"))) + message(FATAL_ERROR "Your system curl version (${CURL_VERSION_STRING}) is too old to support OpenSSL on Windows which requires curl >= 7.71.0. Update your curl version, use WinSSL, disable SSL or use the built-in version of curl.") + endif() + else() + find_package(CURL COMPONENTS HTTP) + if(CURL_FOUND) + message(FATAL_ERROR "Curl found on this system but WITHOUT HTTPS/SSL support. Either disable SSL by setting CPR_ENABLE_SSL to OFF or use the built-in version of curl by setting CPR_USE_SYSTEM_CURL to OFF.") + else() + message(FATAL_ERROR "Curl not found on this system. To use the built-in version set CPR_USE_SYSTEM_CURL to OFF.") + endif() + endif() + else() + find_package(CURL COMPONENTS HTTP) + if(CURL_FOUND) + message(STATUS "Curl found on this system.") + else() + message(FATAL_ERROR "Curl not found on this system. To use the built-in version set CPR_USE_SYSTEM_CURL to OFF.") + endif() + endif() + + # Check for the minimum supported curl version + if(NOT (CURL_VERSION_STRING VERSION_GREATER_EQUAL "7.64.0")) + message(FATAL_ERROR "Your system curl version (${CURL_VERSION_STRING}) is too old! curl >= 7.64.0 is required. Update your curl version, or use the build in curl version e.g. via `cmake .. -DCPR_USE_SYSTEM_CURL=OFF` during CMake configure.") + endif() +else() + message(STATUS "Configuring built-in curl...") + + # ZLIB is optional for curl + # to disable it: + # * from command line: + # -DCURL_ZLIB=OFF + # * from CMake script: + if (CURL_ZLIB OR CURL_ZLIB STREQUAL AUTO OR NOT DEFINED CACHE{CURL_ZLIB}) + include(cmake/zlib_external.cmake) + endif() + + if (CPR_ENABLE_CURL_HTTP_ONLY) + # We only need HTTP (and HTTPS) support: + set(HTTP_ONLY ON CACHE INTERNAL "" FORCE) + endif() + set(BUILD_CURL_EXE OFF CACHE INTERNAL "" FORCE) + set(BUILD_TESTING OFF) + + if (CURL_VERBOSE_LOGGING) + message(STATUS "Enabled curl debug features") + set(ENABLE_DEBUG ON CACHE INTERNAL "" FORCE) + endif() + + if (CPR_ENABLE_SSL) + set(CURL_ENABLE_SSL ON CACHE INTERNAL "" FORCE) + if(ANDROID) + set(CURL_CA_PATH "/system/etc/security/cacerts" CACHE INTERNAL "") + elseif(CPR_SKIP_CA_BUNDLE_SEARCH) + set(CURL_CA_PATH "none" CACHE INTERNAL "") + else() + set(CURL_CA_PATH "auto" CACHE INTERNAL "") + endif() + + if(CPR_SKIP_CA_BUNDLE_SEARCH) + set(CURL_CA_BUNDLE "none" CACHE INTERNAL "") + elseif(NOT DEFINED CURL_CA_BUNDLE) + set(CURL_CA_BUNDLE "auto" CACHE INTERNAL "") + endif() + + if(SSL_BACKEND_USED STREQUAL "WinSSL") + set(CURL_USE_SCHANNEL ON CACHE INTERNAL "" FORCE) + set(CURL_WINDOWS_SSPI ON CACHE INTERNAL "" FORCE) + endif() + + if(SSL_BACKEND_USED STREQUAL "OpenSSL") + set(CURL_USE_OPENSSL ON CACHE INTERNAL "" FORCE) + endif() + + if(SSL_BACKEND_USED STREQUAL "DarwinSSL") + set(CURL_USE_SECTRANSP ON CACHE INTERNAL "" FORCE) + endif() + + if(SSL_BACKEND_USED STREQUAL "MbedTLS") + set(CURL_USE_MBEDTLS ON CACHE INTERNAL "" FORCE) + endif() + + message(STATUS "Enabled curl SSL") + else() + set(CURL_ENABLE_SSL OFF CACHE INTERNAL "" FORCE) + + set(CURL_CA_PATH "none" CACHE INTERNAL "" FORCE) + set(CURL_USE_SCHANNEL OFF CACHE INTERNAL "" FORCE) + set(CURL_WINDOWS_SSPI OFF CACHE INTERNAL "" FORCE) + set(CURL_USE_OPENSSL OFF CACHE INTERNAL "" FORCE) + set(CURL_USE_SECTRANSP OFF CACHE INTERNAL "" FORCE) + set(CURL_USE_MBEDTLS OFF CACHE INTERNAL "" FORCE) + message(STATUS "Disabled curl SSL") + endif() + # Disable linting for curl + clear_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) + + if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") + cmake_policy(SET CMP0135 NEW) + endif() + FetchContent_Declare(curl + URL https://github.com/curl/curl/releases/download/curl-8_10_1/curl-8.10.1.tar.xz + URL_HASH SHA256=73a4b0e99596a09fa5924a4fb7e4b995a85fda0d18a2c02ab9cf134bebce04ee # the file hash for curl-8.10.1.tar.xz + USES_TERMINAL_DOWNLOAD TRUE) # <---- This is needed only for Ninja to show download progress + FetchContent_MakeAvailable(curl) + + restore_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) +endif() + +# Depending on which version of libcurl we are using the CMake target is called differently +if(TARGET libcurl) + # Old curl CMake target name + set(CURL_LIB libcurl) +else() + # New curl CMake target name + set(CURL_LIB CURL::libcurl) +endif() + +# GTest configuration +if(CPR_BUILD_TESTS) + if(CPR_USE_SYSTEM_GTEST) + find_package(GTest) + endif() + if(NOT CPR_USE_SYSTEM_GTEST OR NOT GTEST_FOUND) + message(STATUS "Not using system gtest, using built-in googletest project instead.") + if(MSVC) + # By default, GTest compiles on Windows in CRT static linkage mode. We use this + # variable to force it into using the CRT in dynamic linkage (DLL), just as CPR + # does. + set(gtest_force_shared_crt ON CACHE BOOL "Force gtest to use the shared c runtime") + endif() + + # Disable linting for google test + clear_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) + + FetchContent_Declare(googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz + URL_HASH SHA256=8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7 # the file hash for release-1.14.0.tar.gz + USES_TERMINAL_DOWNLOAD TRUE) # <---- This is needed only for Ninja to show download progress + FetchContent_MakeAvailable(googletest) + + restore_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) + + add_library(gtest_int INTERFACE) + target_link_libraries(gtest_int INTERFACE gtest) + target_include_directories(gtest_int INTERFACE ${googletest_SOURCE_DIR}/include) + + add_library(GTest::GTest ALIAS gtest_int) + + # Group under the "tests/gtest" project folder in IDEs such as Visual Studio. + set_property(TARGET gtest PROPERTY FOLDER "tests/gtest") + set_property(TARGET gtest_main PROPERTY FOLDER "tests/gtest") + endif() +endif() + + +# Mongoose configuration +if(CPR_BUILD_TESTS) + message(STATUS "Building mongoose project for test support.") + + if(CPR_BUILD_TESTS_SSL) + if(NOT CPR_ENABLE_SSL) + message(FATAL_ERROR "OpenSSL is required to build SSL test but CPR_ENABLE_SSL is disabled. Either set CPR_ENABLE_SSL to ON or disable CPR_BUILD_TESTS_SSL.") + endif() + + if(NOT(SSL_BACKEND_USED STREQUAL "OpenSSL")) + message(FATAL_ERROR "OpenSSL is required for SSL test, but it seams like OpenSSL is not being used as SSL backend. Either set CPR_BUILD_TESTS_SSL to OFF or set CPR_FORCE_OPENSSL_BACKEND to ON and try again.") + endif() + + set(ENABLE_SSL_TESTS ON CACHE INTERNAL "") + else() + set(ENABLE_SSL_TESTS OFF CACHE INTERNAL "") + endif() + + # Disable linting for mongoose + clear_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) + + FetchContent_Declare(mongoose + URL https://github.com/cesanta/mongoose/archive/7.7.tar.gz + URL_HASH SHA256=4e5733dae31c3a81156af63ca9aa3a6b9b736547f21f23c3ab2f8e3f1ecc16c0 # the hash for 7.7.tar.gz + USES_TERMINAL_DOWNLOAD TRUE) # <---- This is needed only for Ninja to show download progress + # We can not use FetchContent_MakeAvailable, since we need to patch mongoose to use CMake + if (NOT mongoose_POPULATED) + FetchContent_POPULATE(mongoose) + + file(INSTALL cmake/mongoose.CMakeLists.txt DESTINATION ${mongoose_SOURCE_DIR}) + file(RENAME ${mongoose_SOURCE_DIR}/mongoose.CMakeLists.txt ${mongoose_SOURCE_DIR}/CMakeLists.txt) + add_subdirectory(${mongoose_SOURCE_DIR} ${mongoose_BINARY_DIR}) + + endif() + # Group under the "external" project folder in IDEs such as Visual Studio. + set_property(TARGET mongoose PROPERTY FOLDER "external") + restore_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) +endif() + +if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") +else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic -Werror") + if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + # Disable C++98 compatibility support in clang: https://github.com/libcpr/cpr/issues/927 + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-nonportable-system-include-path -Wno-exit-time-destructors -Wno-undef -Wno-global-constructors -Wno-switch-enum -Wno-old-style-cast -Wno-covered-switch-default -Wno-undefined-func-template") + endif() +endif() + +add_subdirectory(cpr) +add_subdirectory(include) + +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND CPR_BUILD_TESTS) + # Disable linting for tests since they are currently not up to the standard + clear_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) + enable_testing() + add_subdirectory(test) + restore_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) +endif() diff --git a/3rdparty/cpr/CODE_OF_CONDUCT.md b/3rdparty/cpr/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000000000000000000000000000000..1c9ee05c9ad36c93bf09dfd510a794802b89edcc --- /dev/null +++ b/3rdparty/cpr/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +cc@libcpr.org. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/3rdparty/cpr/CONTRIBUTING.md b/3rdparty/cpr/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..abfb97c346e737b78590e7f9bf64fc5660d8e523 --- /dev/null +++ b/3rdparty/cpr/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing to C++ Requests + +Please fork this repository and contribute back using [pull requests](https://github.com/whoshuu/cpr/pulls). Features can be requested using [issues](https://github.com/whoshuu/cpr/issues). All code, comments, and critiques are greatly appreciated. + +## Formatting + +To avoid unproductive debates on formatting, this project uses `clang-format` to ensure a consistent style across all source files. Currently, `clang-format` 3.8 is the version of `clang-format` we use. The format file can be found [here](https://github.com/whoshuu/cpr/blob/master/.clang-format). To install `clang-format` on Ubuntu, run this: + +``` +apt-get install clang-format-3.8 +``` + +To install `clang-format` on OS X, run this: + +``` +brew install clang-format +``` + +Note that `brew` might install a later version of `clang-format`, but it should be mostly compatible with what's run on the Travis servers. + +To run `clang-format` on every source file, run this in the root directory: + +``` +./format-check.sh +``` + +This should indicate which files need formatting and also show a diff of the requested changes. More specific usage instructions can be found on the official [LLVM website](http://releases.llvm.org/3.8.0/tools/clang/docs/ClangFormat.html). diff --git a/3rdparty/cpr/CppCheckSuppressions.txt b/3rdparty/cpr/CppCheckSuppressions.txt new file mode 100644 index 0000000000000000000000000000000000000000..fec131b1195235c1013eeff27ef0dc372dddf827 --- /dev/null +++ b/3rdparty/cpr/CppCheckSuppressions.txt @@ -0,0 +1,3 @@ +noExplicitConstructor +ConfigurationNotChecked +passedByValue diff --git a/3rdparty/cpr/LICENSE b/3rdparty/cpr/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..c63edeb39817d114d10d4b63d36e808bd4490630 --- /dev/null +++ b/3rdparty/cpr/LICENSE @@ -0,0 +1,25 @@ +This license applies to everything except the contents of the "test" +directory and its subdirectories. + +MIT License + +Copyright (c) 2017-2021 Huu Nguyen +Copyright (c) 2022 libcpr and many other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/3rdparty/cpr/README.md b/3rdparty/cpr/README.md new file mode 100644 index 0000000000000000000000000000000000000000..dd21f1ab218351ab032577eea69b89718b190189 --- /dev/null +++ b/3rdparty/cpr/README.md @@ -0,0 +1,204 @@ +# C++ Requests: Curl for People + +[![Documentation](https://img.shields.io/badge/docs-online-informational?style=flat&link=https://docs.libcpr.org/)](https://docs.libcpr.org/) +![CI](https://github.com/libcpr/cpr/workflows/CI/badge.svg) +[![Gitter](https://badges.gitter.im/libcpr/community.svg)](https://gitter.im/libcpr/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +## Announcements + +* This project is being maintained by [Fabian Sauter](https://github.com/com8) and [Kilian Traub](https://github.com/KingKili). +* For quick help, and discussion libcpr also offers a [gitter](https://gitter.im/libcpr/community?utm_source=share-link&utm_medium=link&utm_campaign=share-link) chat. + +## Supported Releases +| Release | Min. C++ Standard | Status | Notes | +|----------|-------------------|--------|-------| +| master | `cpp17` | ![alt text][preview] | | +| 1.11.x | `cpp17` | ![alt text][supported] | | +| 1.10.x | `cpp17` | ![alt text][unsupported] | | +| 1.9.x | `cpp11` | ![alt text][supported] | Supported until 01.01.2025 | +| <= 1.8.x | `cpp11` | ![alt text][unsupported] | | + +[unsupported]: https://img.shields.io/badge/-unsupported-red "unsupported" +[supported]: https://img.shields.io/badge/-supported-green "supported" +[preview]: https://img.shields.io/badge/-preview-orange "preview" + +## TLDR + +C++ Requests is a simple wrapper around [libcurl](http://curl.haxx.se/libcurl) inspired by the excellent [Python Requests](https://github.com/kennethreitz/requests) project. + +Despite its name, libcurl's easy interface is anything but, and making mistakes, misusing it is a common source of error and frustration. Using the more expressive language facilities of `C++17` (or `C++11` in case you use cpr < 1.10.0), this library captures the essence of making network calls into a few concise idioms. + +Here's a quick GET request: + +```c++ +#include + +int main(int argc, char** argv) { + cpr::Response r = cpr::Get(cpr::Url{"https://api.github.com/repos/whoshuu/cpr/contributors"}, + cpr::Authentication{"user", "pass", cpr::AuthMode::BASIC}, + cpr::Parameters{{"anon", "true"}, {"key", "value"}}); + r.status_code; // 200 + r.header["content-type"]; // application/json; charset=utf-8 + r.text; // JSON text string + return 0; +} +``` + +And here's [less functional, more complicated code, without cpr](https://gist.github.com/whoshuu/2dc858b8730079602044). + +## Documentation + +[![Documentation](https://img.shields.io/badge/docs-online-informational?style=for-the-badge&link=https://docs.libcpr.org/)](https://docs.libcpr.org/) +You can find the latest documentation [here](https://docs.libcpr.org/). It's a work in progress, but it should give you a better idea of how to use the library than the [tests](https://github.com/libcpr/cpr/tree/master/test) currently do. + +## Features + +C++ Requests currently supports: + +* Custom headers +* URL-encoded parameters +* URL-encoded POST values +* Multipart form POST upload +* File POST upload +* Basic authentication +* Bearer authentication +* Digest authentication +* NTLM authentication +* Connection and request timeout specification +* Timeout for low speed connection +* Asynchronous requests +* :cookie: support! +* Proxy support +* Callback interfaces +* PUT methods +* DELETE methods +* HEAD methods +* OPTIONS methods +* PATCH methods +* Thread Safe access to [libCurl](https://curl.haxx.se/libcurl/c/threadsafe.html) +* OpenSSL and WinSSL support for HTTPS requests + +## Planned + +For a quick overview about the planned features, have a look at the next [Milestones](https://github.com/libcpr/cpr/milestones). + +## Usage + +### CMake + +#### fetch_content: +If you already have a CMake project you need to integrate C++ Requests with, the primary way is to use `fetch_content`. +Add the following to your `CMakeLists.txt`. + + +```cmake +include(FetchContent) +FetchContent_Declare(cpr GIT_REPOSITORY https://github.com/libcpr/cpr.git + GIT_TAG bb01c8db702fb41e5497aee9c0559ddf4bf13749) # Replace with your desired git commit from: https://github.com/libcpr/cpr/releases +FetchContent_MakeAvailable(cpr) +``` + +This will produce the target `cpr::cpr` which you can link against the typical way: + +```cmake +target_link_libraries(your_target_name PRIVATE cpr::cpr) +``` + +That should do it! +There's no need to handle `libcurl` yourself. All dependencies are taken care of for you. +All of this can be found in an example [**here**](https://github.com/libcpr/example-cmake-fetch-content). + +#### find_package(): +If you prefer not to use `fetch_content`, you can download, build, and install the library and then use CMake `find_package()` function to integrate it into a project. + +**Note:** this feature is feasible only if CPR_USE_SYSTEM_CURL is set. (see [#645](https://github.com/libcpr/cpr/pull/645)) +```Bash +git clone https://github.com/libcpr/cpr.git +cd cpr && mkdir build && cd build +cmake .. -DCPR_USE_SYSTEM_CURL=ON +cmake --build . --parallel +sudo cmake --install . +``` +#### Build Static Library +As an alternative if you want to switch between a static or shared version of cpr use ['-DBUILD_SHARED_LIBS=ON/OFF'](https://cmake.org/cmake/help/latest/variable/BUILD_SHARED_LIBS.html). +```Bash +git clone https://github.com/libcpr/cpr.git +cd cpr && mkdir build && cd build +cmake .. -DCPR_USE_SYSTEM_CURL=ON -DBUILD_SHARED_LIBS=OFF +cmake --build . --parallel +sudo cmake --install . +``` + +In your `CMakeLists.txt`: +```cmake +find_package(cpr REQUIRED) +add_executable(your_target_name your_target_name.cpp) +target_link_libraries(your_target_name PRIVATE cpr::cpr) +``` + +#### Tests +`cpr` provides a bunch of tests that can be executed via the following commands. +```Bash +git clone https://github.com/libcpr/cpr.git +cd cpr && mkdir build && cd build +cmake .. -DCPR_BUILD_TESTS=ON # There are other test related options like 'CPR_BUILD_TESTS_SSL' and 'CPR_BUILD_TESTS_PROXY' +cmake --build . --parallel +ctest -VV # -VV is optional since it enables verbose output +``` + +### Bazel + +Please refer to [hedronvision/bazel-make-cc-https-easy](https://github.com/hedronvision/bazel-make-cc-https-easy). + +### Packages for Linux Distributions + +Alternatively, you may install a package specific to your Linux distribution. Since so few distributions currently have a package for cpr, most users will not be able to run your program with this approach. + +Currently, we are aware of packages for the following distributions: + +* [Arch Linux (AUR)](https://aur.archlinux.org/packages/cpr) +* [Fedora Linux](https://src.fedoraproject.org/rpms/cpr) + +If there's no package for your distribution, try making one! If you do, and it is added to your distribution's repositories, please submit a pull request to add it to the list above. However, please only do this if you plan to actively maintain the package. + +### NuGet Package + +For Windows, there is also a libcpr NuGet package available. Currently, x86 and x64 builds are supported with release and debug configuration. + +The package can be found here: [NuGet.org](https://www.nuget.org/packages/libcpr/) + +### Port for macOS + +On macOS you may install cpr via [MacPorts.org](https://ports.macports.org/port/cpr) (arm64, x86_64, powerpc) + +### FreeBSD Port + +On FreeBSD, you can issue `pkg install cpr` or use the Ports tree to install it. + +## Requirements + +The only explicit requirements are: + +* a `C++17` compatible compiler such as Clang or GCC. The minimum required version of GCC is unknown, so if anyone has trouble building this library with a specific version of GCC, do let us know +* in case you only have a `C++11` compatible compiler available, all versions below cpr 1.9.x are for you. The 1.10.0 release of cpr switches to `C++17` as a requirement. +* If you would like to perform https requests `OpenSSL` and its development libraries are required. +* If you do not use the built-in version of [curl](https://github.com/curl/curl) but instead use your systems version, make sure you use a version `>= 7.64.0`. Lower versions are not supported. This means you need Debian `>= 10` or Ubuntu `>= 20.04 LTS`. + +## Building cpr - Using vcpkg + +You can download and install cpr using the [vcpkg](https://github.com/Microsoft/vcpkg) dependency manager: +```Bash +git clone https://github.com/Microsoft/vcpkg.git +cd vcpkg +./bootstrap-vcpkg.sh +./vcpkg integrate install +./vcpkg install cpr +``` +The `cpr` port in vcpkg is kept up to date by Microsoft team members and community contributors. If the version is out of date, please [create an issue or pull request](https://github.com/Microsoft/vcpkg) on the vcpkg repository. + +## Building cpr - Using Conan + +You can download and install `cpr` using the [Conan](https://conan.io/) package manager. Setup your CMakeLists.txt (see [Conan documentation](https://docs.conan.io/en/latest/integrations/build_system.html) on how to use MSBuild, Meson and others). +An example can be found [**here**](https://github.com/libcpr/example-cmake-conan). + +The `cpr` package in Conan is kept up to date by Conan contributors. If the version is out of date, please [create an issue or pull request](https://github.com/conan-io/conan-center-index) on the `conan-center-index` repository. diff --git a/3rdparty/cpr/cmake/FindMbedTLS.cmake b/3rdparty/cpr/cmake/FindMbedTLS.cmake new file mode 100644 index 0000000000000000000000000000000000000000..61ec46414f01844277a1f377ce7cf6443e408b4f --- /dev/null +++ b/3rdparty/cpr/cmake/FindMbedTLS.cmake @@ -0,0 +1,14 @@ +# Source: https://github.com/curl/curl/blob/curl-7_82_0/CMake/FindMbedTLS.cmake +find_path(MBEDTLS_INCLUDE_DIRS mbedtls/ssl.h) + +find_library(MBEDTLS_LIBRARY mbedtls) +find_library(MBEDX509_LIBRARY mbedx509) +find_library(MBEDCRYPTO_LIBRARY mbedcrypto) + +set(MBEDTLS_LIBRARIES "${MBEDTLS_LIBRARY}" "${MBEDX509_LIBRARY}" "${MBEDCRYPTO_LIBRARY}") + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(MbedTLS DEFAULT_MSG + MBEDTLS_INCLUDE_DIRS MBEDTLS_LIBRARY MBEDX509_LIBRARY MBEDCRYPTO_LIBRARY) + +mark_as_advanced(MBEDTLS_INCLUDE_DIRS MBEDTLS_LIBRARY MBEDX509_LIBRARY MBEDCRYPTO_LIBRARY) diff --git a/3rdparty/cpr/cmake/clang-tidy.cmake b/3rdparty/cpr/cmake/clang-tidy.cmake new file mode 100644 index 0000000000000000000000000000000000000000..26defad2eaca2a1f12adb3dec3ee6c0159ab799c --- /dev/null +++ b/3rdparty/cpr/cmake/clang-tidy.cmake @@ -0,0 +1,13 @@ +if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + find_program(CLANG_TIDY_EXECUTABLE NAMES clang-tidy) + mark_as_advanced(CLANG_TIDY_EXECUTABLE) + + if (${CLANG_TIDY_EXECUTABLE}) + message(FATAL_ERROR "Clang-tidy not found") + else() + message(STATUS "Enabling clang-tidy") + set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXECUTABLE};-warnings-as-errors=*") + endif() +else() + message(FATAL_ERROR "Clang-tidy is not supported when building for windows") +endif() diff --git a/3rdparty/cpr/cmake/clear_variable.cmake b/3rdparty/cpr/cmake/clear_variable.cmake new file mode 100644 index 0000000000000000000000000000000000000000..469ac827e8b5bd437f484ce96265fe1a6bcce115 --- /dev/null +++ b/3rdparty/cpr/cmake/clear_variable.cmake @@ -0,0 +1,11 @@ +macro(clear_variable) + cmake_parse_arguments(CLEAR_VAR "" "DESTINATION;BACKUP;REPLACE" "" ${ARGN}) + set(${CLEAR_VAR_BACKUP} ${${CLEAR_VAR_DESTINATION}}) + set(${CLEAR_VAR_DESTINATION} ${CLEAR_VAR_REPLACE}) +endmacro() + +macro(restore_variable) + cmake_parse_arguments(CLEAR_VAR "" "DESTINATION;BACKUP" "" ${ARGN}) + set(${CLEAR_VAR_DESTINATION} ${${CLEAR_VAR_BACKUP}}) + unset(${CLEAR_VAR_BACKUP}) +endmacro() diff --git a/3rdparty/cpr/cmake/code_coverage.cmake b/3rdparty/cpr/cmake/code_coverage.cmake new file mode 100644 index 0000000000000000000000000000000000000000..eefc4fb399702fe0248c9ef81b63fad883b32220 --- /dev/null +++ b/3rdparty/cpr/cmake/code_coverage.cmake @@ -0,0 +1,29 @@ +# Code coverage +if(CPR_BUILD_TESTS AND CPR_GENERATE_COVERAGE) + set(CMAKE_BUILD_TYPE COVERAGE CACHE INTERNAL "Coverage enabled build") + message(STATUS "Enabling gcov support") + if(NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + set(COVERAGE_FLAG "--coverage") + endif() + set(CMAKE_CXX_FLAGS_COVERAGE + "-g -O0 ${COVERAGE_FLAG} -fprofile-arcs -ftest-coverage" + CACHE STRING "Flags used by the C++ compiler during coverage builds." + FORCE) + set(CMAKE_C_FLAGS_COVERAGE + "-g -O0 ${COVERAGE_FLAG} -fprofile-arcs -ftest-coverage" + CACHE STRING "Flags used by the C compiler during coverage builds." + FORCE) + set(CMAKE_EXE_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used for linking binaries during coverage builds." + FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE + "" + CACHE STRING "Flags used by the shared libraries linker during coverage builds." + FORCE) + mark_as_advanced( + CMAKE_CXX_FLAGS_COVERAGE + CMAKE_C_FLAGS_COVERAGE + CMAKE_EXE_LINKER_FLAGS_COVERAGE + CMAKE_SHARED_LINKER_FLAGS_COVERAGE) +endif() diff --git a/3rdparty/cpr/cmake/cppcheck.cmake b/3rdparty/cpr/cmake/cppcheck.cmake new file mode 100644 index 0000000000000000000000000000000000000000..8ef46880650b52298cb915152fdd1346c5fa28b0 --- /dev/null +++ b/3rdparty/cpr/cmake/cppcheck.cmake @@ -0,0 +1,10 @@ +find_program(CMAKE_CXX_CPPCHECK NAMES cppcheck) +if(CMAKE_CXX_CPPCHECK) + list(APPEND CMAKE_CXX_CPPCHECK + "--error-exitcode=1" + "--enable=warning,style" + "--force" + "--inline-suppr" + "--std=c++${CMAKE_CXX_STANDARD}" + "--suppressions-list=${CMAKE_SOURCE_DIR}/CppCheckSuppressions.txt") +endif() diff --git a/3rdparty/cpr/cmake/cprConfig.cmake.in b/3rdparty/cpr/cmake/cprConfig.cmake.in new file mode 100644 index 0000000000000000000000000000000000000000..9c0bda5fad2a08297cd182bd2b1e0ccd2071540b --- /dev/null +++ b/3rdparty/cpr/cmake/cprConfig.cmake.in @@ -0,0 +1,8 @@ +include(CMakeFindDependencyMacro) +@PACKAGE_INIT@ + +find_dependency(CURL REQUIRED) + +include(${CMAKE_CURRENT_LIST_DIR}/cprTargets.cmake) + +check_required_components(cpr) \ No newline at end of file diff --git a/3rdparty/cpr/cmake/cprver.h.in b/3rdparty/cpr/cmake/cprver.h.in new file mode 100644 index 0000000000000000000000000000000000000000..e353324976ee2626af0bb12684b0cfc9cdda3dfc --- /dev/null +++ b/3rdparty/cpr/cmake/cprver.h.in @@ -0,0 +1,30 @@ +#ifndef CPR_CPRVER_H +#define CPR_CPRVER_H + +/** + * CPR version as a string. + **/ +#define CPR_VERSION "${cpr_VERSION}" + +/** + * CPR version split up into parts. + **/ +#define CPR_VERSION_MAJOR ${cpr_VERSION_MAJOR} +#define CPR_VERSION_MINOR ${cpr_VERSION_MINOR} +#define CPR_VERSION_PATCH ${cpr_VERSION_PATCH} + +/** + * CPR version as a single hex digit. + * it can be split up into three parts: + * 0xAABBCC + * AA: The current CPR major version number in a hex format. + * BB: The current CPR minor version number in a hex format. + * CC: The current CPR patch version number in a hex format. + * + * Examples: + * '0x010702' -> 01.07.02 -> CPR_VERSION: 1.7.2 + * '0xA13722' -> A1.37.22 -> CPR_VERSION: 161.55.34 + **/ +#define CPR_VERSION_NUM ${cpr_VERSION_NUM} + +#endif diff --git a/3rdparty/cpr/cmake/mongoose.CMakeLists.txt b/3rdparty/cpr/cmake/mongoose.CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..397e0c12660a69895c641de920c381f59b40d07b --- /dev/null +++ b/3rdparty/cpr/cmake/mongoose.CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.15) +project(mongoose C) + + +add_library(mongoose STATIC mongoose.c) +target_include_directories(mongoose PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +if(ENABLE_SSL_TESTS) + # Enable mongoose SSL + target_compile_definitions(mongoose PUBLIC MG_ENABLE_OPENSSL) + target_link_libraries(mongoose PUBLIC OpenSSL::SSL) + + # Fix macOS and Windows invalid OpenSSL include path + target_include_directories(mongoose PUBLIC "${OPENSSL_INCLUDE_DIR}") +endif() diff --git a/3rdparty/cpr/cmake/sanitizer.cmake b/3rdparty/cpr/cmake/sanitizer.cmake new file mode 100644 index 0000000000000000000000000000000000000000..10cdc46244498ccae96fd6e96232a288457ebab4 --- /dev/null +++ b/3rdparty/cpr/cmake/sanitizer.cmake @@ -0,0 +1,69 @@ +include(CheckCXXCompilerFlag) +include(CheckCXXSourceRuns) + +set(ALL_SAN_FLAGS "") + + # No sanitizers when cross compiling to prevent stuff like this: https://github.com/whoshuu/cpr/issues/582 +if(NOT CMAKE_CROSSCOMPILING) + # Thread sanitizer + set(THREAD_SAN_FLAGS "-fsanitize=thread") + set(PREV_FLAG ${CMAKE_REQUIRED_FLAGS}) + set(CMAKE_REQUIRED_FLAGS "${THREAD_SAN_FLAGS}") + check_cxx_source_runs("int main() { return 0; }" THREAD_SANITIZER_AVAILABLE) + set(CMAKE_REQUIRED_FLAGS ${PREV_FLAG}) + # Do not add the ThreadSanitizer for builds with all sanitizers enabled because it is incompatible with other sanitizers. + + # Address sanitizer + set(ADDR_SAN_FLAGS "-fsanitize=address") + set(PREV_FLAG ${CMAKE_REQUIRED_FLAGS}) + set(CMAKE_REQUIRED_FLAGS "${ADDR_SAN_FLAGS}") + check_cxx_source_runs("int main() { return 0; }" ADDRESS_SANITIZER_AVAILABLE) + set(CMAKE_REQUIRED_FLAGS ${PREV_FLAG}) + if(ADDRESS_SANITIZER_AVAILABLE) + set(ALL_SAN_FLAGS "${ALL_SAN_FLAGS} ${ADDR_SAN_FLAGS}") + endif() + + # Leak sanitizer + set(LEAK_SAN_FLAGS "-fsanitize=leak") + check_cxx_compiler_flag(${LEAK_SAN_FLAGS} LEAK_SANITIZER_AVAILABLE) + if(LEAK_SANITIZER_AVAILABLE) + set(ALL_SAN_FLAGS "${ALL_SAN_FLAGS} ${LEAK_SAN_FLAGS}") + endif() + + # Undefined behavior sanitizer + set(UDEF_SAN_FLAGS "-fsanitize=undefined") + check_cxx_compiler_flag(${UDEF_SAN_FLAGS} UNDEFINED_BEHAVIOUR_SANITIZER_AVAILABLE) + if(UNDEFINED_BEHAVIOUR_SANITIZER_AVAILABLE) + set(ALL_SAN_FLAGS "${ALL_SAN_FLAGS} ${UDEF_SAN_FLAGS}") + endif() + + # All sanitizer (without thread sanitizer) + if(NOT ALL_SAN_FLAGS STREQUAL "") + set(PREV_FLAG ${CMAKE_REQUIRED_FLAGS}) + set(CMAKE_REQUIRED_FLAGS "${ALL_SAN_FLAGS}") + check_cxx_source_runs("int main() { return 0; }" ALL_SANITIZERS_AVAILABLE) + set(CMAKE_REQUIRED_FLAGS ${PREV_FLAG}) + endif() + + if(CPR_DEBUG_SANITIZER_FLAG_THREAD AND THREAD_SANITIZER_AVAILABLE) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${THREAD_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C compiler during thread sanitizer builds." FORCE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${THREAD_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C++ compiler during thread sanitizer builds." FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG}" CACHE INTERNAL "Flags used for the linker during thread sanitizer builds" FORCE) + elseif(CPR_DEBUG_SANITIZER_FLAG_ADDR AND ADDRESS_SANITIZER_AVAILABLE) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${ADDR_SAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C compiler during address sanitizer builds." FORCE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${ADDR_SAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C++ compiler during address sanitizer builds." FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG}" CACHE INTERNAL "Flags used for the linker during address sanitizer builds" FORCE) + elseif(CPR_DEBUG_SANITIZER_FLAG_LEAK AND LEAK_SANITIZER_AVAILABLE) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${LEAK_SAN_FLAGS} -fno-omit-frame-pointer" CACHE INTERNAL "Flags used by the C compiler during leak sanitizer builds." FORCE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${LEAK_SAN_FLAGS} -fno-omit-frame-pointer" CACHE INTERNAL "Flags used by the C++ compiler during leak sanitizer builds." FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG}" CACHE INTERNAL "Flags used for the linker during leak sanitizer builds" FORCE) + elseif(CPR_DEBUG_SANITIZER_FLAG_UB AND UNDEFINED_BEHAVIOUR_SANITIZER_AVAILABLE) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${UDEF_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C compiler during undefined behaviour sanitizer builds." FORCE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${UDEF_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C++ compiler during undefined behaviour sanitizer builds." FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG}" CACHE INTERNAL "Flags used for the linker during undefined behaviour sanitizer builds" FORCE) + elseif(CPR_DEBUG_SANITIZER_FLAG_ALL AND ALL_SANITIZERS_AVAILABLE) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} ${ALL_SAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C compiler during most possible sanitizer builds." FORCE) + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${ALL_SAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C++ compiler during most possible sanitizer builds." FORCE) + set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG}" CACHE INTERNAL "Flags used for the linker during most possible sanitizer builds" FORCE) + endif() +endif() diff --git a/3rdparty/cpr/cmake/std_fs_support_test.cpp b/3rdparty/cpr/cmake/std_fs_support_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..44bac7770d52f901beedaf244edfa6cf6de31a22 --- /dev/null +++ b/3rdparty/cpr/cmake/std_fs_support_test.cpp @@ -0,0 +1,11 @@ +#if __has_include() +#include +namespace fs = std::filesystem; +#else +#include +namespace fs = std::experimental::filesystem; +#endif + +int main() { + auto cwd = fs::current_path(); +} diff --git a/3rdparty/cpr/cmake/zlib_external.cmake b/3rdparty/cpr/cmake/zlib_external.cmake new file mode 100644 index 0000000000000000000000000000000000000000..1a8eea066383872eebb4a75fda8fbe408c1545d5 --- /dev/null +++ b/3rdparty/cpr/cmake/zlib_external.cmake @@ -0,0 +1,22 @@ +# ZLIB + +# Fix Windows missing "zlib.dll": +if(WIN32 AND (${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME})) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${EXECUTABLE_OUTPUT_PATH}/$ CACHE INTERNAL "" FORCE) +endif() + +set(ZLIB_COMPAT ON CACHE INTERNAL "" FORCE) +set(ZLIB_ENABLE_TESTS OFF CACHE INTERNAL "" FORCE) + +FetchContent_Declare(zlib + GIT_REPOSITORY https://github.com/zlib-ng/zlib-ng + GIT_TAG 2.1.3 + USES_TERMINAL_DOWNLOAD TRUE) +FetchContent_MakeAvailable(zlib) + +# Fix Windows zlib dll names from "zlibd1.dll" to "zlib.dll": +if(WIN32 AND BUILD_SHARED_LIBS) + set_target_properties(zlib PROPERTIES OUTPUT_NAME "zlib") + set_target_properties(zlib PROPERTIES DEBUG_POSTFIX "") + set_target_properties(zlib PROPERTIES SUFFIX ".dll") +endif() diff --git a/3rdparty/cpr/cpr-config.cmake b/3rdparty/cpr/cpr-config.cmake new file mode 100644 index 0000000000000000000000000000000000000000..58ab48320bb8f4d7c01a936b585a08327a13bbd6 --- /dev/null +++ b/3rdparty/cpr/cpr-config.cmake @@ -0,0 +1,26 @@ +# - C++ Requests, Curl for People +# This module is a libcurl wrapper written in modern C++. +# It provides an easy, intuitive, and efficient interface to +# a host of networking methods. +# +# Finding this module will define the following variables: +# CPR_FOUND - True if the core library has been found +# CPR_LIBRARIES - Path to the core library archive +# CPR_INCLUDE_DIRS - Path to the include directories. Gives access +# to cpr.h, which must be included in every +# file that uses this interface + +find_path(CPR_INCLUDE_DIR + NAMES cpr.h) + +find_library(CPR_LIBRARY + NAMES cpr + HINTS ${CPR_LIBRARY_ROOT}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(CPR REQUIRED_VARS CPR_LIBRARY CPR_INCLUDE_DIR) + +if(CPR_FOUND) + set(CPR_LIBRARIES ${CPR_LIBRARY}) + set(CPR_INCLUDE_DIRS ${CPR_INCLUDE_DIR}) +endif() diff --git a/3rdparty/cpr/cpr/CMakeLists.txt b/3rdparty/cpr/cpr/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..0c7083c4e9286db2e5ef8dfb0131bb52eaf70957 --- /dev/null +++ b/3rdparty/cpr/cpr/CMakeLists.txt @@ -0,0 +1,79 @@ +cmake_minimum_required(VERSION 3.15) + +add_library(cpr + accept_encoding.cpp + async.cpp + auth.cpp + bearer.cpp + callback.cpp + cert_info.cpp + cookies.cpp + cprtypes.cpp + curl_container.cpp + curlholder.cpp + error.cpp + file.cpp + multipart.cpp + parameters.cpp + payload.cpp + proxies.cpp + proxyauth.cpp + session.cpp + threadpool.cpp + timeout.cpp + unix_socket.cpp + util.cpp + response.cpp + redirect.cpp + interceptor.cpp + ssl_ctx.cpp + curlmultiholder.cpp + multiperform.cpp) + +add_library(cpr::cpr ALIAS cpr) + +target_link_libraries(cpr PUBLIC ${CURL_LIB}) # todo should be private, but first dependencies in ssl_options need to be removed + +# Fix missing OpenSSL includes for Windows since in 'ssl_ctx.cpp' we include OpenSSL directly +if(SSL_BACKEND_USED STREQUAL "OpenSSL") + target_link_libraries(cpr PRIVATE OpenSSL::SSL) + target_include_directories(cpr PRIVATE ${OPENSSL_INCLUDE_DIR}) +endif() + +# Set version for shared libraries. +set_target_properties(cpr + PROPERTIES + VERSION ${${PROJECT_NAME}_VERSION} + SOVERSION ${${PROJECT_NAME}_VERSION_MAJOR}) + +# Import GNU common install directory variables +include(GNUInstallDirs) + +install(TARGETS cpr + EXPORT cprTargets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +if(CPR_USE_SYSTEM_CURL) + # Include CMake helpers for package config files + # Follow this installation guideline: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html + include(CMakePackageConfigHelpers) + + write_basic_package_version_file( + "${PROJECT_BINARY_DIR}/cpr/cprConfigVersion.cmake" + VERSION ${${PROJECT_NAME}_VERSION} + COMPATIBILITY ExactVersion) + + configure_package_config_file(${PROJECT_SOURCE_DIR}/cmake/cprConfig.cmake.in + "${PROJECT_BINARY_DIR}/cpr/cprConfig.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cpr) + + install(FILES ${PROJECT_BINARY_DIR}/cpr/cprConfig.cmake + ${PROJECT_BINARY_DIR}/cpr/cprConfigVersion.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cpr) +endif() + +install(EXPORT cprTargets + FILE cprTargets.cmake + NAMESPACE cpr:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cpr) diff --git a/3rdparty/cpr/cpr/accept_encoding.cpp b/3rdparty/cpr/cpr/accept_encoding.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0427b31f50e9d6a80b822f3970d47ee7b548672c --- /dev/null +++ b/3rdparty/cpr/cpr/accept_encoding.cpp @@ -0,0 +1,38 @@ +#include "cpr/accept_encoding.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace cpr { + +AcceptEncoding::AcceptEncoding(const std::initializer_list& methods) { + methods_.clear(); + std::transform(methods.begin(), methods.end(), std::inserter(methods_, methods_.begin()), [&](cpr::AcceptEncodingMethods method) { return cpr::AcceptEncodingMethodsStringMap.at(method); }); +} + +AcceptEncoding::AcceptEncoding(const std::initializer_list& string_methods) : methods_{string_methods} {} + +bool AcceptEncoding::empty() const noexcept { + return methods_.empty(); +} + +const std::string AcceptEncoding::getString() const { + return std::accumulate(std::next(methods_.begin()), methods_.end(), *methods_.begin(), [](std::string a, std::string b) { return std::move(a) + ", " + std::move(b); }); +} + +[[nodiscard]] bool AcceptEncoding::disabled() const { + if (methods_.find(cpr::AcceptEncodingMethodsStringMap.at(AcceptEncodingMethods::disabled)) != methods_.end()) { + if (methods_.size() != 1) { + throw std::invalid_argument("AcceptEncoding does not accept any other values if 'disabled' is present. You set the following encodings: " + getString()); + } + return true; + } + return false; +} + +} // namespace cpr diff --git a/3rdparty/cpr/cpr/async.cpp b/3rdparty/cpr/cpr/async.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e10d09e1fda4521ae442ed4750183077bac297a1 --- /dev/null +++ b/3rdparty/cpr/cpr/async.cpp @@ -0,0 +1,8 @@ +#include "cpr/async.h" + +namespace cpr { + +// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables) +CPR_SINGLETON_IMPL(GlobalThreadPool) + +} // namespace cpr diff --git a/3rdparty/cpr/cpr/auth.cpp b/3rdparty/cpr/cpr/auth.cpp new file mode 100644 index 0000000000000000000000000000000000000000..dfda86263c1d3e126a85b7c9be1cd5f8a53c8125 --- /dev/null +++ b/3rdparty/cpr/cpr/auth.cpp @@ -0,0 +1,26 @@ +#include "cpr/auth.h" +#include "cpr/util.h" + +#include + +namespace cpr { + +Authentication::Authentication(std::string_view username, std::string_view password, AuthMode auth_mode) : auth_mode_{auth_mode} { + auth_string_.reserve(username.size() + 1 + password.size()); + auth_string_ += username; + auth_string_ += ':'; + auth_string_ += password; +} + +Authentication::~Authentication() noexcept { + util::secureStringClear(auth_string_); +} + +const char* Authentication::GetAuthString() const noexcept { + return auth_string_.c_str(); +} + +AuthMode Authentication::GetAuthMode() const noexcept { + return auth_mode_; +} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/bearer.cpp b/3rdparty/cpr/cpr/bearer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..041b6c18786def194b3efe3c2dab4ab23bd38c74 --- /dev/null +++ b/3rdparty/cpr/cpr/bearer.cpp @@ -0,0 +1,17 @@ +#include "cpr/bearer.h" +#include "cpr/util.h" +#include + +namespace cpr { +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 +Bearer::~Bearer() noexcept { + util::secureStringClear(token_string_); +} + +const char* Bearer::GetToken() const noexcept { + return token_string_.c_str(); +} +#endif +} // namespace cpr diff --git a/3rdparty/cpr/cpr/callback.cpp b/3rdparty/cpr/cpr/callback.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f2d257a2c55589410476423ff901b9a6ba2c437e --- /dev/null +++ b/3rdparty/cpr/cpr/callback.cpp @@ -0,0 +1,14 @@ +#include "cpr/callback.h" +#include "cpr/cprtypes.h" +#include + +namespace cpr { + +void CancellationCallback::SetProgressCallback(ProgressCallback& u_cb) { + user_cb.emplace(std::reference_wrapper{u_cb}); +} +bool CancellationCallback::operator()(cpr_pf_arg_t dltotal, cpr_pf_arg_t dlnow, cpr_pf_arg_t ultotal, cpr_pf_arg_t ulnow) const { + const bool cont_operation{!cancellation_state->load()}; + return user_cb ? (cont_operation && (*user_cb)(dltotal, dlnow, ultotal, ulnow)) : cont_operation; +} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/cert_info.cpp b/3rdparty/cpr/cpr/cert_info.cpp new file mode 100644 index 0000000000000000000000000000000000000000..410648c04ded85094f4bc6104f6b0a51889f4db9 --- /dev/null +++ b/3rdparty/cpr/cpr/cert_info.cpp @@ -0,0 +1,45 @@ +#include "cpr/cert_info.h" +#include +#include + +namespace cpr { + +std::string& CertInfo::operator[](const size_t& pos) { + return cert_info_[pos]; +} + +CertInfo::iterator CertInfo::begin() { + return cert_info_.begin(); +} +CertInfo::iterator CertInfo::end() { + return cert_info_.end(); +} + +CertInfo::const_iterator CertInfo::begin() const { + return cert_info_.begin(); +} + +CertInfo::const_iterator CertInfo::end() const { + return cert_info_.end(); +} + +CertInfo::const_iterator CertInfo::cbegin() const { + return cert_info_.cbegin(); +} + +CertInfo::const_iterator CertInfo::cend() const { + return cert_info_.cend(); +} + +void CertInfo::emplace_back(const std::string& str) { + cert_info_.emplace_back(str); +} + +void CertInfo::push_back(const std::string& str) { + cert_info_.push_back(str); +} + +void CertInfo::pop_back() { + cert_info_.pop_back(); +} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/cookies.cpp b/3rdparty/cpr/cpr/cookies.cpp new file mode 100644 index 0000000000000000000000000000000000000000..37c088b9801448b41e2f750fbdc411394fc65ea3 --- /dev/null +++ b/3rdparty/cpr/cpr/cookies.cpp @@ -0,0 +1,115 @@ +#include "cpr/cookies.h" +#include "cpr/curlholder.h" +#include +#include +#include +#include +#include + +namespace cpr { +const std::string Cookie::GetDomain() const { + return domain_; +} + +bool Cookie::IsIncludingSubdomains() const { + return includeSubdomains_; +} + +const std::string Cookie::GetPath() const { + return path_; +} + +bool Cookie::IsHttpsOnly() const { + return httpsOnly_; +} + +const std::chrono::system_clock::time_point Cookie::GetExpires() const { + return expires_; +} + +const std::string Cookie::GetExpiresString() const { + std::stringstream ss; + std::tm tm{}; + const std::time_t tt = std::chrono::system_clock::to_time_t(expires_); +#ifdef _WIN32 + gmtime_s(&tm, &tt); +#else + // NOLINTNEXTLINE(misc-include-cleaner,cert-err33-c) False positive since is included. Also ignore the ret value here. + gmtime_r(&tt, &tm); +#endif + ss << std::put_time(&tm, "%a, %d %b %Y %H:%M:%S GMT"); + return ss.str(); +} + +const std::string Cookie::GetName() const { + return name_; +} + +const std::string Cookie::GetValue() const { + return value_; +} + +const std::string Cookies::GetEncoded(const CurlHolder& holder) const { + std::stringstream stream; + for (const cpr::Cookie& item : cookies_) { + // Depending on if encoding is set to "true", we will URL-encode cookies + stream << (encode ? holder.urlEncode(item.GetName()) : item.GetName()) << "="; + + // special case version 1 cookies, which can be distinguished by + // beginning and trailing quotes + if (!item.GetValue().empty() && item.GetValue().front() == '"' && item.GetValue().back() == '"') { + stream << item.GetValue(); + } else { + // Depending on if encoding is set to "true", we will URL-encode cookies + stream << (encode ? holder.urlEncode(item.GetValue()) : item.GetValue()); + } + stream << "; "; + } + return stream.str(); +} + +cpr::Cookie& Cookies::operator[](size_t pos) { + return cookies_[pos]; +} + +Cookies::iterator Cookies::begin() { + return cookies_.begin(); +} + +Cookies::iterator Cookies::end() { + return cookies_.end(); +} + +Cookies::const_iterator Cookies::begin() const { + return cookies_.begin(); +} + +Cookies::const_iterator Cookies::end() const { + return cookies_.end(); +} + +Cookies::const_iterator Cookies::cbegin() const { + return cookies_.cbegin(); +} + +Cookies::const_iterator Cookies::cend() const { + return cookies_.cend(); +} + +void Cookies::emplace_back(const Cookie& str) { + cookies_.emplace_back(str); +} + +bool Cookies::empty() const { + return cookies_.empty(); +} + +void Cookies::push_back(const Cookie& str) { + cookies_.push_back(str); +} + +void Cookies::pop_back() { + cookies_.pop_back(); +} + +} // namespace cpr diff --git a/3rdparty/cpr/cpr/cprtypes.cpp b/3rdparty/cpr/cpr/cprtypes.cpp new file mode 100644 index 0000000000000000000000000000000000000000..43df134911f30ac6145a8604aeb1d3e18a95512b --- /dev/null +++ b/3rdparty/cpr/cpr/cprtypes.cpp @@ -0,0 +1,11 @@ +#include "cpr/cprtypes.h" + +#include +#include +#include + +namespace cpr { +bool CaseInsensitiveCompare::operator()(const std::string& a, const std::string& b) const noexcept { + return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), [](unsigned char ac, unsigned char bc) { return std::tolower(ac) < std::tolower(bc); }); +} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/curl_container.cpp b/3rdparty/cpr/cpr/curl_container.cpp new file mode 100644 index 0000000000000000000000000000000000000000..de0c9c3b5c84ce8a28d61056d5b6158acc943f4e --- /dev/null +++ b/3rdparty/cpr/cpr/curl_container.cpp @@ -0,0 +1,60 @@ +#include "cpr/curl_container.h" +#include "cpr/curlholder.h" +#include +#include +#include +#include + +namespace cpr { +template +CurlContainer::CurlContainer(const std::initializer_list& containerList) : containerList_(containerList) {} + +template +void CurlContainer::Add(const std::initializer_list& containerList) { + std::transform(containerList.begin(), containerList.end(), std::back_inserter(containerList_), [](const T& elem) { return std::move(elem); }); +} + +template +void CurlContainer::Add(const T& element) { + containerList_.push_back(std::move(element)); +} + +template <> +const std::string CurlContainer::GetContent(const CurlHolder& holder) const { + std::string content{}; + for (const Parameter& parameter : containerList_) { + if (!content.empty()) { + content += "&"; + } + + const std::string escapedKey = encode ? holder.urlEncode(parameter.key) : parameter.key; + if (parameter.value.empty()) { + content += escapedKey; + } else { + const std::string escapedValue = encode ? holder.urlEncode(parameter.value) : parameter.value; + content += escapedKey + "="; + content += escapedValue; + } + } + + return content; +} + +template <> +const std::string CurlContainer::GetContent(const CurlHolder& holder) const { + std::string content{}; + for (const cpr::Pair& element : containerList_) { + if (!content.empty()) { + content += "&"; + } + const std::string escaped = encode ? holder.urlEncode(element.value) : element.value; + content += element.key + "=" + escaped; + } + + return content; +} + +template class CurlContainer; +template class CurlContainer; + +} // namespace cpr diff --git a/3rdparty/cpr/cpr/curlholder.cpp b/3rdparty/cpr/cpr/curlholder.cpp new file mode 100644 index 0000000000000000000000000000000000000000..cee8897c07b68e1a1ca61b0a78b78d63cb5c003f --- /dev/null +++ b/3rdparty/cpr/cpr/curlholder.cpp @@ -0,0 +1,52 @@ +#include "cpr/curlholder.h" +#include +#include +#include +#include + +namespace cpr { +CurlHolder::CurlHolder() { + /** + * Allow multithreaded access to CPR by locking curl_easy_init(). + * curl_easy_init() is not thread safe. + * References: + * https://curl.haxx.se/libcurl/c/curl_easy_init.html + * https://curl.haxx.se/libcurl/c/threadsafe.html + **/ + curl_easy_init_mutex_().lock(); + // NOLINTNEXTLINE (cppcoreguidelines-prefer-member-initializer) since we need it to happen inside the lock + handle = curl_easy_init(); + curl_easy_init_mutex_().unlock(); + + assert(handle); +} // namespace cpr + +CurlHolder::~CurlHolder() { + curl_slist_free_all(chunk); + curl_slist_free_all(resolveCurlList); + curl_mime_free(multipart); + curl_easy_cleanup(handle); +} + +std::string CurlHolder::urlEncode(const std::string& s) const { + assert(handle); + char* output = curl_easy_escape(handle, s.c_str(), static_cast(s.length())); + if (output) { + std::string result = output; + curl_free(output); + return result; + } + return ""; +} + +std::string CurlHolder::urlDecode(const std::string& s) const { + assert(handle); + char* output = curl_easy_unescape(handle, s.c_str(), static_cast(s.length()), nullptr); + if (output) { + std::string result = output; + curl_free(output); + return result; + } + return ""; +} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/curlmultiholder.cpp b/3rdparty/cpr/cpr/curlmultiholder.cpp new file mode 100644 index 0000000000000000000000000000000000000000..972fefe91c1af681af73f441a2b7aced37e9b274 --- /dev/null +++ b/3rdparty/cpr/cpr/curlmultiholder.cpp @@ -0,0 +1,15 @@ +#include "cpr/curlmultiholder.h" +#include +#include + +namespace cpr { + +CurlMultiHolder::CurlMultiHolder() : handle{curl_multi_init()} { + assert(handle); +} + +CurlMultiHolder::~CurlMultiHolder() { + curl_multi_cleanup(handle); +} + +} // namespace cpr diff --git a/3rdparty/cpr/cpr/error.cpp b/3rdparty/cpr/cpr/error.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3e5f355631f2c7a372aba0d5c56d27e61b7e8ef4 --- /dev/null +++ b/3rdparty/cpr/cpr/error.cpp @@ -0,0 +1,167 @@ +#include "cpr/error.h" +#include +#include +#include +#include + +namespace cpr { +static const std::unordered_map curl_error_map = { // NOLINT - (needed because of static init) + {CURLE_OK, ErrorCode::OK}, + {CURLE_UNSUPPORTED_PROTOCOL, ErrorCode::UNSUPPORTED_PROTOCOL}, + {CURLE_FAILED_INIT, ErrorCode::FAILED_INIT}, + {CURLE_URL_MALFORMAT, ErrorCode::URL_MALFORMAT}, + {CURLE_NOT_BUILT_IN, ErrorCode::NOT_BUILT_IN}, + {CURLE_COULDNT_RESOLVE_PROXY, ErrorCode::COULDNT_RESOLVE_PROXY}, + {CURLE_COULDNT_RESOLVE_HOST, ErrorCode::COULDNT_RESOLVE_HOST}, + {CURLE_COULDNT_CONNECT, ErrorCode::COULDNT_CONNECT}, + +// Name changed in curl >= 7.51.0. +#if LIBCURL_VERSION_NUM >= 0x073300 + {CURLE_WEIRD_SERVER_REPLY, ErrorCode::WEIRD_SERVER_REPLY}, +#else + {CURLE_FTP_WEIRD_SERVER_REPLY, ErrorCode::WEIRD_SERVER_REPLY}, +#endif + + {CURLE_REMOTE_ACCESS_DENIED, ErrorCode::REMOTE_ACCESS_DENIED}, + {CURLE_HTTP2, ErrorCode::HTTP2}, + {CURLE_QUOTE_ERROR, ErrorCode::QUOTE_ERROR}, + {CURLE_HTTP_RETURNED_ERROR, ErrorCode::HTTP_RETURNED_ERROR}, + {CURLE_WRITE_ERROR, ErrorCode::WRITE_ERROR}, + {CURLE_UPLOAD_FAILED, ErrorCode::UPLOAD_FAILED}, + {CURLE_READ_ERROR, ErrorCode::READ_ERROR}, + {CURLE_OUT_OF_MEMORY, ErrorCode::OUT_OF_MEMORY}, + {CURLE_OPERATION_TIMEDOUT, ErrorCode::OPERATION_TIMEDOUT}, + {CURLE_RANGE_ERROR, ErrorCode::RANGE_ERROR}, + {CURLE_HTTP_POST_ERROR, ErrorCode::HTTP_POST_ERROR}, + {CURLE_SSL_CONNECT_ERROR, ErrorCode::SSL_CONNECT_ERROR}, + {CURLE_BAD_DOWNLOAD_RESUME, ErrorCode::BAD_DOWNLOAD_RESUME}, + {CURLE_FILE_COULDNT_READ_FILE, ErrorCode::FILE_COULDNT_READ_FILE}, + {CURLE_FUNCTION_NOT_FOUND, ErrorCode::FUNCTION_NOT_FOUND}, + {CURLE_ABORTED_BY_CALLBACK, ErrorCode::ABORTED_BY_CALLBACK}, + {CURLE_BAD_FUNCTION_ARGUMENT, ErrorCode::BAD_FUNCTION_ARGUMENT}, + {CURLE_INTERFACE_FAILED, ErrorCode::INTERFACE_FAILED}, + {CURLE_TOO_MANY_REDIRECTS, ErrorCode::TOO_MANY_REDIRECTS}, + {CURLE_UNKNOWN_OPTION, ErrorCode::UNKNOWN_OPTION}, + +// Added in curl 7.78.0. +#if LIBCURL_VERSION_NUM >= 0x074E00 + {CURLE_SETOPT_OPTION_SYNTAX, ErrorCode::SETOPT_OPTION_SYNTAX}, +#endif + + {CURLE_GOT_NOTHING, ErrorCode::GOT_NOTHING}, + {CURLE_SSL_ENGINE_NOTFOUND, ErrorCode::SSL_ENGINE_NOTFOUND}, + {CURLE_SSL_ENGINE_SETFAILED, ErrorCode::SSL_ENGINE_SETFAILED}, + {CURLE_SEND_ERROR, ErrorCode::SEND_ERROR}, + {CURLE_RECV_ERROR, ErrorCode::RECV_ERROR}, + {CURLE_SSL_CERTPROBLEM, ErrorCode::SSL_CERTPROBLEM}, + {CURLE_SSL_CIPHER, ErrorCode::SSL_CIPHER}, + {CURLE_PEER_FAILED_VERIFICATION, ErrorCode::PEER_FAILED_VERIFICATION}, + {CURLE_BAD_CONTENT_ENCODING, ErrorCode::BAD_CONTENT_ENCODING}, + {CURLE_FILESIZE_EXCEEDED, ErrorCode::FILESIZE_EXCEEDED}, + {CURLE_USE_SSL_FAILED, ErrorCode::USE_SSL_FAILED}, + {CURLE_SEND_FAIL_REWIND, ErrorCode::SEND_FAIL_REWIND}, + {CURLE_SSL_ENGINE_INITFAILED, ErrorCode::SSL_ENGINE_INITFAILED}, + +// Added in curl 7.13.1. +#if LIBCURL_VERSION_NUM >= 0x070D01 + {CURLE_LOGIN_DENIED, ErrorCode::LOGIN_DENIED}, +#endif + +// Added in curl 7.16.0. +#if LIBCURL_VERSION_NUM >= 0x071000 + {CURLE_SSL_CACERT_BADFILE, ErrorCode::SSL_CACERT_BADFILE}, +#endif + +// Added in curl 7.16.1. +#if LIBCURL_VERSION_NUM >= 0x071001 + {CURLE_SSL_SHUTDOWN_FAILED, ErrorCode::SSL_SHUTDOWN_FAILED}, +#endif + +// Added in curl 7.18.2. +#if LIBCURL_VERSION_NUM >= 0x071202 + {CURLE_AGAIN, ErrorCode::AGAIN}, +#endif + +// Added in curl 7.19.0. +#if LIBCURL_VERSION_NUM >= 0x071300 + {CURLE_SSL_CRL_BADFILE, ErrorCode::SSL_CRL_BADFILE}, + {CURLE_SSL_ISSUER_ERROR, ErrorCode::SSL_ISSUER_ERROR}, +#endif + +// Added in curl 7.21.0. +#if LIBCURL_VERSION_NUM >= 0x071500 + {CURLE_CHUNK_FAILED, ErrorCode::CHUNK_FAILED}, +#endif + +// Added in curl 7.30.0. +#if LIBCURL_VERSION_NUM >= 0x071E00 + {CURLE_NO_CONNECTION_AVAILABLE, ErrorCode::NO_CONNECTION_AVAILABLE}, +#endif + +// Added in curl 7.39.0. +#if LIBCURL_VERSION_NUM >= 0x072700 + {CURLE_SSL_PINNEDPUBKEYNOTMATCH, ErrorCode::SSL_PINNEDPUBKEYNOTMATCH}, +#endif + +// Added in curl 7.41.0. +#if LIBCURL_VERSION_NUM >= 0x072900 + {CURLE_SSL_INVALIDCERTSTATUS, ErrorCode::SSL_INVALIDCERTSTATUS}, +#endif + +// Added in curl 7.49.0. +#if LIBCURL_VERSION_NUM >= 0x073100 + {CURLE_HTTP2_STREAM, ErrorCode::HTTP2_STREAM}, +#endif + + {CURLE_PARTIAL_FILE, ErrorCode::PARTIAL_FILE}, + +// Added in curl 7.59.0. +#if LIBCURL_VERSION_NUM >= 0x073B00 + {CURLE_RECURSIVE_API_CALL, ErrorCode::RECURSIVE_API_CALL}, +#endif + +// Added in curl 7.66.0. +#if LIBCURL_VERSION_NUM >= 0x074200 + {CURLE_AUTH_ERROR, ErrorCode::AUTH_ERROR}, +#endif + +// Added in curl 7.68.0. +#if LIBCURL_VERSION_NUM >= 0x074400 + {CURLE_HTTP3, ErrorCode::HTTP3}, +#endif + +// Added in curl 7.69.0. +#if LIBCURL_VERSION_NUM >= 0x074500 + {CURLE_QUIC_CONNECT_ERROR, ErrorCode::QUIC_CONNECT_ERROR}, +#endif + +// Added in curl 7.73.0. +#if LIBCURL_VERSION_NUM >= 0x074900 + {CURLE_PROXY, ErrorCode::PROXY}, +#endif + +// Added in curl 7.77.0. +#if LIBCURL_VERSION_NUM >= 0x074D00 + {CURLE_SSL_CLIENTCERT, ErrorCode::SSL_CLIENTCERT}, +#endif + +// Added in curl 7.84.0. +#if LIBCURL_VERSION_NUM >= 0x075400 + {CURLE_UNRECOVERABLE_POLL, ErrorCode::UNRECOVERABLE_POLL}, +#endif + +// Added in curl 7.6.0. +#if LIBCURL_VERSION_NUM >= 0x080600 + {CURLE_TOO_LARGE, ErrorCode::TOO_LARGE}, +#endif +}; + +ErrorCode Error::getErrorCodeForCurlError(std::int32_t curl_code) { + auto it = curl_error_map.find(curl_code); + if (it == curl_error_map.end()) { + // Default return value when the CURL error code is not recognized + return ErrorCode::UNKNOWN_ERROR; + } + return it->second; +} +} // namespace cpr \ No newline at end of file diff --git a/3rdparty/cpr/cpr/file.cpp b/3rdparty/cpr/cpr/file.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0690198bb190161a4c39f44d8b74178ce220fcc3 --- /dev/null +++ b/3rdparty/cpr/cpr/file.cpp @@ -0,0 +1,63 @@ +#include "cpr/file.h" +#include +#include +#include + +namespace cpr { + +Files::Files(const std::initializer_list& p_filepaths) { + for (const std::string& filepath : p_filepaths) { + files.emplace_back(filepath); + } +} + +Files::iterator Files::begin() { + return files.begin(); +} + +Files::iterator Files::end() { + return files.end(); +} + +Files::const_iterator Files::begin() const { + return files.begin(); +} + +Files::const_iterator Files::end() const { + return files.end(); +} + +Files::const_iterator Files::cbegin() const { + return files.cbegin(); +} + +Files::const_iterator Files::cend() const { + return files.cend(); +} + +void Files::emplace_back(const File& file) { + files.emplace_back(file); +} + +void Files::push_back(const File& file) { + files.push_back(file); +} + +void Files::pop_back() { + files.pop_back(); +} + +Files& Files::operator=(const Files& other) { + if (&other != this) { + files = other.files; + } + return *this; +} + +Files& Files::operator=(Files&& old) noexcept { + if (&old != this) { + files = std::move(old.files); + } + return *this; +} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/interceptor.cpp b/3rdparty/cpr/cpr/interceptor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6af915aa01dec417a146d8fd26904300556f1cab --- /dev/null +++ b/3rdparty/cpr/cpr/interceptor.cpp @@ -0,0 +1,59 @@ +#include "cpr/interceptor.h" +#include "cpr/callback.h" +#include "cpr/multiperform.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include +#include +#include +#include + +namespace cpr { + +Response Interceptor::proceed(Session& session) { + return session.proceed(); +} + +Response Interceptor::proceed(Session& session, ProceedHttpMethod httpMethod) { + switch (httpMethod) { + case ProceedHttpMethod::DELETE_REQUEST: + return session.Delete(); + case ProceedHttpMethod::GET_REQUEST: + return session.Get(); + case ProceedHttpMethod::HEAD_REQUEST: + return session.Head(); + case ProceedHttpMethod::OPTIONS_REQUEST: + return session.Options(); + case ProceedHttpMethod::PATCH_REQUEST: + return session.Patch(); + case ProceedHttpMethod::POST_REQUEST: + return session.Post(); + case ProceedHttpMethod::PUT_REQUEST: + return session.Put(); + default: + throw std::invalid_argument{"Can't proceed the session with the provided http method!"}; + } +} + +Response Interceptor::proceed(Session& session, ProceedHttpMethod httpMethod, std::ofstream& file) { + if (httpMethod == ProceedHttpMethod::DOWNLOAD_FILE_REQUEST) { + return session.Download(file); + } + throw std::invalid_argument{"std::ofstream argument is only valid for ProceedHttpMethod::DOWNLOAD_FILE!"}; +} + +Response Interceptor::proceed(Session& session, ProceedHttpMethod httpMethod, const WriteCallback& write) { + if (httpMethod == ProceedHttpMethod::DOWNLOAD_CALLBACK_REQUEST) { + return session.Download(write); + } + throw std::invalid_argument{"WriteCallback argument is only valid for ProceedHttpMethod::DOWNLOAD_CALLBACK!"}; +} + +std::vector InterceptorMulti::proceed(MultiPerform& multi) { + return multi.proceed(); +} + +void InterceptorMulti::PrepareDownloadSession(MultiPerform& multi, size_t sessions_index, const WriteCallback& write) { + multi.PrepareDownloadSessions(sessions_index, write); +} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/multipart.cpp b/3rdparty/cpr/cpr/multipart.cpp new file mode 100644 index 0000000000000000000000000000000000000000..21251d53110c7ae5c05539a0f2d906cbf4909922 --- /dev/null +++ b/3rdparty/cpr/cpr/multipart.cpp @@ -0,0 +1,9 @@ +#include "cpr/multipart.h" +#include +#include + +namespace cpr { +Multipart::Multipart(const std::initializer_list& p_parts) : parts{p_parts} {} +Multipart::Multipart(const std::vector& p_parts) : parts{p_parts} {} +Multipart::Multipart(const std::vector&& p_parts) : parts{p_parts} {} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/multiperform.cpp b/3rdparty/cpr/cpr/multiperform.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6c6ea0c80bbd2b38d741394856ee446f13f964a9 --- /dev/null +++ b/3rdparty/cpr/cpr/multiperform.cpp @@ -0,0 +1,377 @@ +#include "cpr/multiperform.h" + +#include "cpr/callback.h" +#include "cpr/curlmultiholder.h" +#include "cpr/interceptor.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cpr { + +MultiPerform::MultiPerform() : multicurl_(new CurlMultiHolder()) { + current_interceptor_ = interceptors_.end(); + first_interceptor_ = interceptors_.end(); +} + +MultiPerform::~MultiPerform() { + // Unlock all sessions + for (const std::pair, HttpMethod>& pair : sessions_) { + pair.first->isUsedInMultiPerform = false; + + // Remove easy handle from multi handle + const CURLMcode error_code = curl_multi_remove_handle(multicurl_->handle, pair.first->curl_->handle); + if (error_code) { + std::cerr << "curl_multi_remove_handle() failed, code " << static_cast(error_code) << '\n'; + return; + } + } +} + +void MultiPerform::AddSession(std::shared_ptr& session, HttpMethod method) { + // Check if this multiperform is download only + if (((method != HttpMethod::DOWNLOAD_REQUEST && is_download_multi_perform) && method != HttpMethod::UNDEFINED) || (method == HttpMethod::DOWNLOAD_REQUEST && !is_download_multi_perform && !sessions_.empty())) { + // Currently it is not possible to mix download and non-download methods, as download needs additional parameters + throw std::invalid_argument("Failed to add session: Cannot mix download and non-download methods!"); + } + + // Set download only if neccessary + if (method == HttpMethod::DOWNLOAD_REQUEST) { + is_download_multi_perform = true; + } + + // Add easy handle to multi handle + const CURLMcode error_code = curl_multi_add_handle(multicurl_->handle, session->curl_->handle); + if (error_code) { + std::cerr << "curl_multi_add_handle() failed, code " << static_cast(error_code) << '\n'; + return; + } + + // Lock session to the multihandle + session->isUsedInMultiPerform = true; + + // Add session to sessions_ + sessions_.emplace_back(session, method); +} + +void MultiPerform::RemoveSession(const std::shared_ptr& session) { + // Has to be handled before calling curl_multi_remove_handle to avoid it returning something != CURLM_OK. + if (sessions_.empty()) { + throw std::invalid_argument("Failed to find session!"); + } + + // Remove easy handle from multihandle + const CURLMcode error_code = curl_multi_remove_handle(multicurl_->handle, session->curl_->handle); + if (error_code != CURLM_OK) { + std::cerr << "curl_multi_remove_handle() failed, code " << static_cast(error_code) << '\n'; + return; + } + + // Unlock session + session->isUsedInMultiPerform = false; + + // Remove session from sessions_ + auto it = std::find_if(sessions_.begin(), sessions_.end(), [&session](const std::pair, HttpMethod>& pair) { return session->curl_->handle == pair.first->curl_->handle; }); + if (it == sessions_.end()) { + throw std::invalid_argument("Failed to find session!"); + } + sessions_.erase(it); + + // Reset download only if empty + if (sessions_.empty()) { + is_download_multi_perform = false; + } +} + +std::vector, MultiPerform::HttpMethod>>& MultiPerform::GetSessions() { + return sessions_; +} + +const std::vector, MultiPerform::HttpMethod>>& MultiPerform::GetSessions() const { + return sessions_; +} + +void MultiPerform::DoMultiPerform() { + // Do multi perform until every handle has finished + int still_running{0}; + do { + CURLMcode error_code = curl_multi_perform(multicurl_->handle, &still_running); + if (error_code) { + std::cerr << "curl_multi_perform() failed, code " << static_cast(error_code) << '\n'; + break; + } + + if (still_running) { + const int timeout_ms{250}; +#if LIBCURL_VERSION_NUM >= 0x074200 // 7.66.0 + error_code = curl_multi_poll(multicurl_->handle, nullptr, 0, timeout_ms, nullptr); + if (error_code) { + std::cerr << "curl_multi_poll() failed, code " << static_cast(error_code) << '\n'; +#else + error_code = curl_multi_wait(multicurl_->handle, nullptr, 0, timeout_ms, nullptr); + if (error_code) { + std::cerr << "curl_multi_wait() failed, code " << static_cast(error_code) << '\n'; + +#endif + break; + } + } + } while (still_running); +} + +std::vector MultiPerform::ReadMultiInfo(const std::function& complete_function) { + // Get infos and create Response objects + std::vector responses; + struct CURLMsg* info{nullptr}; + do { + int msgq = 0; + + // Read info from multihandle + info = curl_multi_info_read(multicurl_->handle, &msgq); + + if (info) { + // Find current session + auto it = std::find_if(sessions_.begin(), sessions_.end(), [&info](const std::pair, HttpMethod>& pair) { return pair.first->curl_->handle == info->easy_handle; }); + if (it == sessions_.end()) { + std::cerr << "Failed to find current session!" << '\n'; + break; + } + const std::shared_ptr current_session = (*it).first; + + // Add response object + // NOLINTNEXTLINE (cppcoreguidelines-pro-type-union-access) + responses.push_back(complete_function(*current_session, info->data.result)); + } + } while (info); + + // Sort response objects to match order of added sessions + std::vector sorted_responses; + for (const std::pair, HttpMethod>& pair : sessions_) { + Session& current_session = *(pair.first); + auto it = std::find_if(responses.begin(), responses.end(), [¤t_session](const Response& response) { return current_session.curl_->handle == response.curl_->handle; }); + const Response current_response = *it; // NOLINT (performance-unnecessary-copy-initialization) False possible + // Erase response from original vector to increase future search speed + responses.erase(it); + sorted_responses.push_back(current_response); + } + + return sorted_responses; +} + +std::vector MultiPerform::MakeRequest() { + const std::optional> r = intercept(); + if (r.has_value()) { + return r.value(); + } + + DoMultiPerform(); + return ReadMultiInfo([](Session& session, CURLcode curl_error) -> Response { return session.Complete(curl_error); }); +} + +std::vector MultiPerform::MakeDownloadRequest() { + const std::optional> r = intercept(); + if (r.has_value()) { + return r.value(); + } + + DoMultiPerform(); + return ReadMultiInfo([](Session& session, CURLcode curl_error) -> Response { return session.CompleteDownload(curl_error); }); +} + +void MultiPerform::PrepareSessions() { + for (const std::pair, HttpMethod>& pair : sessions_) { + switch (pair.second) { + case HttpMethod::GET_REQUEST: + pair.first->PrepareGet(); + break; + case HttpMethod::POST_REQUEST: + pair.first->PreparePost(); + break; + case HttpMethod::PUT_REQUEST: + pair.first->PreparePut(); + break; + case HttpMethod::DELETE_REQUEST: + pair.first->PrepareDelete(); + break; + case HttpMethod::PATCH_REQUEST: + pair.first->PreparePatch(); + break; + case HttpMethod::HEAD_REQUEST: + pair.first->PrepareHead(); + break; + case HttpMethod::OPTIONS_REQUEST: + pair.first->PrepareOptions(); + break; + default: + std::cerr << "PrepareSessions failed: Undefined HttpMethod or download without arguments!" << '\n'; + return; + } + } +} + +void MultiPerform::PrepareDownloadSession(size_t sessions_index, const WriteCallback& write) { + const std::pair, HttpMethod>& pair = sessions_[sessions_index]; + switch (pair.second) { + case HttpMethod::DOWNLOAD_REQUEST: + pair.first->PrepareDownload(write); + break; + default: + std::cerr << "PrepareSessions failed: Undefined HttpMethod or non download method with arguments!" << '\n'; + return; + } +} + +void MultiPerform::PrepareDownloadSession(size_t sessions_index, std::ofstream& file) { + const std::pair, HttpMethod>& pair = sessions_[sessions_index]; + switch (pair.second) { + case HttpMethod::DOWNLOAD_REQUEST: + pair.first->PrepareDownload(file); + break; + default: + std::cerr << "PrepareSessions failed: Undefined HttpMethod or non download method with arguments!" << '\n'; + return; + } +} + +void MultiPerform::SetHttpMethod(HttpMethod method) { + for (std::pair, HttpMethod>& pair : sessions_) { + pair.second = method; + } +} + +void MultiPerform::PrepareGet() { + SetHttpMethod(HttpMethod::GET_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PrepareDelete() { + SetHttpMethod(HttpMethod::DELETE_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PreparePut() { + SetHttpMethod(HttpMethod::PUT_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PreparePatch() { + SetHttpMethod(HttpMethod::PATCH_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PrepareHead() { + SetHttpMethod(HttpMethod::HEAD_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PrepareOptions() { + SetHttpMethod(HttpMethod::OPTIONS_REQUEST); + PrepareSessions(); +} + +void MultiPerform::PreparePost() { + SetHttpMethod(HttpMethod::POST_REQUEST); + PrepareSessions(); +} + +std::vector MultiPerform::Get() { + PrepareGet(); + return MakeRequest(); +} + +std::vector MultiPerform::Delete() { + PrepareDelete(); + return MakeRequest(); +} + +std::vector MultiPerform::Put() { + PreparePut(); + return MakeRequest(); +} + +std::vector MultiPerform::Head() { + PrepareHead(); + return MakeRequest(); +} + +std::vector MultiPerform::Options() { + PrepareOptions(); + return MakeRequest(); +} + +std::vector MultiPerform::Patch() { + PreparePatch(); + return MakeRequest(); +} + +std::vector MultiPerform::Post() { + PreparePost(); + return MakeRequest(); +} + +std::vector MultiPerform::Perform() { + PrepareSessions(); + return MakeRequest(); +} + +std::vector MultiPerform::proceed() { + // Check if this multiperform mixes download and non download requests + if (!sessions_.empty()) { + const bool new_is_download_multi_perform = sessions_.front().second == HttpMethod::DOWNLOAD_REQUEST; + + for (const std::pair, HttpMethod>& s : sessions_) { + const HttpMethod method = s.second; + if ((new_is_download_multi_perform && method != HttpMethod::DOWNLOAD_REQUEST) || (!new_is_download_multi_perform && method == HttpMethod::DOWNLOAD_REQUEST)) { + throw std::invalid_argument("Failed to proceed with session: Cannot mix download and non-download methods!"); + } + } + is_download_multi_perform = new_is_download_multi_perform; + } + + PrepareSessions(); + return MakeRequest(); +} + +const std::optional> MultiPerform::intercept() { + if (current_interceptor_ == interceptors_.end()) { + current_interceptor_ = first_interceptor_; + } else { + current_interceptor_++; + } + + if (current_interceptor_ != interceptors_.end()) { + auto icpt = current_interceptor_; + // Nested makeRequest() start at first_interceptor_, thus excluding previous interceptors. + first_interceptor_ = current_interceptor_; + ++first_interceptor_; + + const std::optional> r = (*current_interceptor_)->intercept(*this); + + first_interceptor_ = icpt; + + return r; + } + return std::nullopt; +} + +void MultiPerform::AddInterceptor(const std::shared_ptr& pinterceptor) { + // Shall only add before first interceptor run + assert(current_interceptor_ == interceptors_.end()); + interceptors_.push_back(pinterceptor); + first_interceptor_ = interceptors_.begin(); +} + +} // namespace cpr diff --git a/3rdparty/cpr/cpr/parameters.cpp b/3rdparty/cpr/cpr/parameters.cpp new file mode 100644 index 0000000000000000000000000000000000000000..51cdd183070aa8c55ae294fd00e1035ecb3498df --- /dev/null +++ b/3rdparty/cpr/cpr/parameters.cpp @@ -0,0 +1,4 @@ +// NOLINTNEXTLINE(misc-include-cleaner) Ignored since it's for the future +#include "cpr/parameters.h" + +namespace cpr {} // namespace cpr diff --git a/3rdparty/cpr/cpr/payload.cpp b/3rdparty/cpr/cpr/payload.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2a23b0dd30836b0486c537c4e5a741209d7f0d0f --- /dev/null +++ b/3rdparty/cpr/cpr/payload.cpp @@ -0,0 +1,4 @@ +// NOLINTNEXTLINE(misc-include-cleaner) Ignored since it's for the future +#include "cpr/payload.h" + +namespace cpr {} // namespace cpr diff --git a/3rdparty/cpr/cpr/proxies.cpp b/3rdparty/cpr/cpr/proxies.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0d3fe98935d14e8ebed86608c7a175c0f1decf4e --- /dev/null +++ b/3rdparty/cpr/cpr/proxies.cpp @@ -0,0 +1,21 @@ +#include "cpr/proxies.h" + +#include +#include +#include +#include + +namespace cpr { + +Proxies::Proxies(const std::initializer_list>& hosts) : hosts_{hosts} {} +Proxies::Proxies(const std::map& hosts) : hosts_{hosts} {} + +bool Proxies::has(const std::string& protocol) const { + return hosts_.count(protocol) > 0; +} + +const std::string& Proxies::operator[](const std::string& protocol) { + return hosts_[protocol]; +} + +} // namespace cpr diff --git a/3rdparty/cpr/cpr/proxyauth.cpp b/3rdparty/cpr/cpr/proxyauth.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b9ed0e92ba97cc4b00414aac59e3a50ea3f6b195 --- /dev/null +++ b/3rdparty/cpr/cpr/proxyauth.cpp @@ -0,0 +1,31 @@ +#include "cpr/proxyauth.h" +#include "cpr/util.h" +#include + +namespace cpr { +EncodedAuthentication::~EncodedAuthentication() noexcept { + util::secureStringClear(username); + util::secureStringClear(password); +} + +const std::string& EncodedAuthentication::GetUsername() const { + return username; +} + +const std::string& EncodedAuthentication::GetPassword() const { + return password; +} + +bool ProxyAuthentication::has(const std::string& protocol) const { + return proxyAuth_.count(protocol) > 0; +} + +const char* ProxyAuthentication::GetUsername(const std::string& protocol) { + return proxyAuth_[protocol].username.c_str(); +} + +const char* ProxyAuthentication::GetPassword(const std::string& protocol) { + return proxyAuth_[protocol].password.c_str(); +} + +} // namespace cpr diff --git a/3rdparty/cpr/cpr/redirect.cpp b/3rdparty/cpr/cpr/redirect.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3be96960b311987ee4d85a2bffc178c665bcc00a --- /dev/null +++ b/3rdparty/cpr/cpr/redirect.cpp @@ -0,0 +1,41 @@ +#include "cpr/redirect.h" +#include + +namespace cpr { +PostRedirectFlags operator|(PostRedirectFlags lhs, PostRedirectFlags rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +PostRedirectFlags operator&(PostRedirectFlags lhs, PostRedirectFlags rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} + +PostRedirectFlags operator^(PostRedirectFlags lhs, PostRedirectFlags rhs) { + return static_cast(static_cast(lhs) ^ static_cast(rhs)); +} + +PostRedirectFlags operator~(PostRedirectFlags flag) { + return static_cast(~static_cast(flag)); +} + +PostRedirectFlags& operator|=(PostRedirectFlags& lhs, PostRedirectFlags rhs) { + lhs = static_cast(static_cast(lhs) | static_cast(rhs)); + const uint8_t tmp = static_cast(lhs); + lhs = static_cast(tmp); + return lhs; +} + +PostRedirectFlags& operator&=(PostRedirectFlags& lhs, PostRedirectFlags rhs) { + lhs = static_cast(static_cast(lhs) & static_cast(rhs)); + return lhs; +} + +PostRedirectFlags& operator^=(PostRedirectFlags& lhs, PostRedirectFlags rhs) { + lhs = static_cast(static_cast(lhs) ^ static_cast(rhs)); + return lhs; +} + +bool any(PostRedirectFlags flag) { + return flag != PostRedirectFlags::NONE; +} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/response.cpp b/3rdparty/cpr/cpr/response.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a711ef27a9237d4e8c4e3845f31b824f9db9eca6 --- /dev/null +++ b/3rdparty/cpr/cpr/response.cpp @@ -0,0 +1,57 @@ +#include "cpr/response.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cpr { + +Response::Response(std::shared_ptr curl, std::string&& p_text, std::string&& p_header_string, Cookies&& p_cookies = Cookies{}, Error&& p_error = Error{}) : curl_(std::move(curl)), text(std::move(p_text)), cookies(std::move(p_cookies)), error(std::move(p_error)), raw_header(std::move(p_header_string)) { + header = cpr::util::parseHeader(raw_header, &status_line, &reason); + assert(curl_); + assert(curl_->handle); + curl_easy_getinfo(curl_->handle, CURLINFO_RESPONSE_CODE, &status_code); + curl_easy_getinfo(curl_->handle, CURLINFO_TOTAL_TIME, &elapsed); + char* url_string{nullptr}; + curl_easy_getinfo(curl_->handle, CURLINFO_EFFECTIVE_URL, &url_string); + url = Url(url_string); +#if LIBCURL_VERSION_NUM >= 0x073700 // 7.55.0 + curl_easy_getinfo(curl_->handle, CURLINFO_SIZE_DOWNLOAD_T, &downloaded_bytes); + curl_easy_getinfo(curl_->handle, CURLINFO_SIZE_UPLOAD_T, &uploaded_bytes); +#else + double downloaded_bytes_double, uploaded_bytes_double; + curl_easy_getinfo(curl_->handle, CURLINFO_SIZE_DOWNLOAD, &downloaded_bytes_double); + curl_easy_getinfo(curl_->handle, CURLINFO_SIZE_UPLOAD, &uploaded_bytes_double); + downloaded_bytes = downloaded_bytes_double; + uploaded_bytes = uploaded_bytes_double; +#endif + curl_easy_getinfo(curl_->handle, CURLINFO_REDIRECT_COUNT, &redirect_count); +} + +std::vector Response::GetCertInfos() const { + assert(curl_); + assert(curl_->handle); + curl_certinfo* ci{nullptr}; + curl_easy_getinfo(curl_->handle, CURLINFO_CERTINFO, &ci); + + std::vector cert_infos; + for (int i = 0; i < ci->num_of_certs; i++) { + CertInfo cert_info; + // NOLINTNEXTLINE (cppcoreguidelines-pro-bounds-pointer-arithmetic) + for (curl_slist* slist = ci->certinfo[i]; slist; slist = slist->next) { + cert_info.emplace_back(std::string{slist->data}); + } + cert_infos.emplace_back(cert_info); + } + return cert_infos; +} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/session.cpp b/3rdparty/cpr/cpr/session.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f47f5a9aeb17815150f67dea5aa049319dbe356a --- /dev/null +++ b/3rdparty/cpr/cpr/session.cpp @@ -0,0 +1,1038 @@ +#include "cpr/session.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "cpr/accept_encoding.h" +#include "cpr/async.h" +#include "cpr/auth.h" +#include "cpr/bearer.h" +#include "cpr/body.h" +#include "cpr/callback.h" +#include "cpr/connect_timeout.h" +#include "cpr/cookies.h" +#include "cpr/cprtypes.h" +#include "cpr/curlholder.h" +#include "cpr/error.h" +#include "cpr/file.h" +#include "cpr/filesystem.h" // IWYU pragma: keep +#include "cpr/http_version.h" +#include "cpr/interceptor.h" +#include "cpr/interface.h" +#include "cpr/limit_rate.h" +#include "cpr/local_port.h" +#include "cpr/local_port_range.h" +#include "cpr/low_speed.h" +#include "cpr/multipart.h" +#include "cpr/parameters.h" +#include "cpr/payload.h" +#include "cpr/proxies.h" +#include "cpr/proxyauth.h" +#include "cpr/range.h" +#include "cpr/redirect.h" +#include "cpr/reserve_size.h" +#include "cpr/resolve.h" +#include "cpr/response.h" +#include "cpr/ssl_options.h" +#include "cpr/timeout.h" +#include "cpr/unix_socket.h" +#include "cpr/user_agent.h" +#include "cpr/util.h" +#include "cpr/verbose.h" + +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION +#include "cpr/ssl_ctx.h" +#endif + + +namespace cpr { +// Ignored here since libcurl reqires a long: +// NOLINTNEXTLINE(google-runtime-int) +constexpr long ON = 1L; +// Ignored here since libcurl reqires a long: +// NOLINTNEXTLINE(google-runtime-int) +constexpr long OFF = 0L; + +CURLcode Session::DoEasyPerform() { + if (isUsedInMultiPerform) { + std::cerr << "curl_easy_perform cannot be executed if the CURL handle is used in a MultiPerform.\n"; + return CURLcode::CURLE_FAILED_INIT; + } + return curl_easy_perform(curl_->handle); +} + +void Session::prepareHeader() { + curl_slist* chunk = nullptr; + for (const std::pair& item : header_) { + std::string header_string = item.first; + if (item.second.empty()) { + header_string += ";"; + } else { + header_string += ": " + item.second; + } + + curl_slist* temp = curl_slist_append(chunk, header_string.c_str()); + if (temp) { + chunk = temp; + } + } + + // Set the chunked transfer encoding in case it does not already exist: + if (chunkedTransferEncoding_ && header_.find("Transfer-Encoding") == header_.end()) { + curl_slist* temp = curl_slist_append(chunk, "Transfer-Encoding:chunked"); + if (temp) { + chunk = temp; + } + } + + // libcurl would prepare the header "Expect: 100-continue" by default when uploading files larger than 1 MB. + // Here we would like to disable this feature: + curl_slist* temp = curl_slist_append(chunk, "Expect:"); + if (temp) { + chunk = temp; + } + + curl_easy_setopt(curl_->handle, CURLOPT_HTTPHEADER, chunk); + + curl_slist_free_all(curl_->chunk); + curl_->chunk = chunk; +} + +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 +void Session::SetBearer(const Bearer& token) { + // Ignore here since this has been defined by libcurl. + curl_easy_setopt(curl_->handle, CURLOPT_HTTPAUTH, CURLAUTH_BEARER); + curl_easy_setopt(curl_->handle, CURLOPT_XOAUTH2_BEARER, token.GetToken()); +} +#endif + +Session::Session() : curl_(new CurlHolder()) { + // Set up some sensible defaults + curl_version_info_data* version_info = curl_version_info(CURLVERSION_NOW); + const std::string version = "curl/" + std::string{version_info->version}; + curl_easy_setopt(curl_->handle, CURLOPT_USERAGENT, version.c_str()); + SetRedirect(Redirect()); + curl_easy_setopt(curl_->handle, CURLOPT_NOPROGRESS, 1L); + curl_easy_setopt(curl_->handle, CURLOPT_ERRORBUFFER, curl_->error.data()); + curl_easy_setopt(curl_->handle, CURLOPT_COOKIEFILE, ""); +#ifdef CPR_CURL_NOSIGNAL + curl_easy_setopt(curl_->handle, CURLOPT_NOSIGNAL, 1L); +#endif + +#if LIBCURL_VERSION_NUM >= 0x071900 // 7.25.0 + curl_easy_setopt(curl_->handle, CURLOPT_TCP_KEEPALIVE, 1L); +#endif + current_interceptor_ = interceptors_.end(); + first_interceptor_ = interceptors_.end(); +} + +Response Session::makeDownloadRequest() { + const std::optional r = intercept(); + if (r.has_value()) { + return r.value(); + } + + const CURLcode curl_error = DoEasyPerform(); + + return CompleteDownload(curl_error); +} + +void Session::prepareCommonShared() { + assert(curl_->handle); + + // Set Header: + prepareHeader(); + + // URL parameter: + const std::string parametersContent = parameters_.GetContent(*curl_); + if (!parametersContent.empty()) { + const Url new_url{url_ + "?" + parametersContent}; + curl_easy_setopt(curl_->handle, CURLOPT_URL, new_url.c_str()); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_URL, url_.c_str()); + } + + // Proxy: + const std::string protocol = url_.str().substr(0, url_.str().find(':')); + if (proxies_.has(protocol)) { + curl_easy_setopt(curl_->handle, CURLOPT_PROXY, proxies_[protocol].c_str()); + if (proxyAuth_.has(protocol)) { + curl_easy_setopt(curl_->handle, CURLOPT_PROXYUSERNAME, proxyAuth_.GetUsername(protocol)); + curl_easy_setopt(curl_->handle, CURLOPT_PROXYPASSWORD, proxyAuth_.GetPassword(protocol)); + } + } + +#if LIBCURL_VERSION_NUM >= 0x071506 // 7.21.6 + if (acceptEncoding_.empty()) { + // Enable all supported built-in compressions + curl_easy_setopt(curl_->handle, CURLOPT_ACCEPT_ENCODING, ""); + } else if (acceptEncoding_.disabled()) { + // Disable curl adding the 'Accept-Encoding' header + curl_easy_setopt(curl_->handle, CURLOPT_ACCEPT_ENCODING, nullptr); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_ACCEPT_ENCODING, acceptEncoding_.getString().c_str()); + } +#endif + + curl_->error[0] = '\0'; + + // Clear the response + response_string_.clear(); + if (response_string_reserve_size_ > 0) { + response_string_.reserve(response_string_reserve_size_); + } + + // Enable so we are able to retrieve certificate information: + curl_easy_setopt(curl_->handle, CURLOPT_CERTINFO, 1L); +} + +void Session::prepareCommon() { + assert(curl_->handle); + + // Everything else: + prepareCommonShared(); + + // Set Content: + prepareBodyPayloadOrMultipart(); + + if (!cbs_->writecb_.callback) { + curl_easy_setopt(curl_->handle, CURLOPT_WRITEFUNCTION, cpr::util::writeFunction); + curl_easy_setopt(curl_->handle, CURLOPT_WRITEDATA, &response_string_); + } + + header_string_.clear(); + if (!cbs_->headercb_.callback) { + curl_easy_setopt(curl_->handle, CURLOPT_HEADERFUNCTION, cpr::util::writeFunction); + curl_easy_setopt(curl_->handle, CURLOPT_HEADERDATA, &header_string_); + } +} + +void Session::prepareCommonDownload() { + assert(curl_->handle); + + // Everything else: + prepareCommonShared(); + + header_string_.clear(); + if (cbs_->headercb_.callback) { + curl_easy_setopt(curl_->handle, CURLOPT_HEADERFUNCTION, cpr::util::headerUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_HEADERDATA, &cbs_->headercb_); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_HEADERFUNCTION, cpr::util::writeFunction); + curl_easy_setopt(curl_->handle, CURLOPT_HEADERDATA, &header_string_); + } +} + +Response Session::makeRequest() { + const std::optional r = intercept(); + if (r.has_value()) { + return r.value(); + } + + const CURLcode curl_error = DoEasyPerform(); + + return Complete(curl_error); +} + +void Session::SetLimitRate(const LimitRate& limit_rate) { + curl_easy_setopt(curl_->handle, CURLOPT_MAX_RECV_SPEED_LARGE, limit_rate.downrate); + curl_easy_setopt(curl_->handle, CURLOPT_MAX_SEND_SPEED_LARGE, limit_rate.uprate); +} + +void Session::SetReadCallback(const ReadCallback& read) { + cbs_->readcb_ = read; + curl_easy_setopt(curl_->handle, CURLOPT_INFILESIZE_LARGE, read.size); + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDSIZE_LARGE, read.size); + curl_easy_setopt(curl_->handle, CURLOPT_READFUNCTION, cpr::util::readUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_READDATA, &cbs_->readcb_); + chunkedTransferEncoding_ = read.size == -1; +} + +void Session::SetHeaderCallback(const HeaderCallback& header) { + curl_easy_setopt(curl_->handle, CURLOPT_HEADERFUNCTION, cpr::util::headerUserFunction); + cbs_->headercb_ = header; + curl_easy_setopt(curl_->handle, CURLOPT_HEADERDATA, &cbs_->headercb_); +} + +void Session::SetWriteCallback(const WriteCallback& write) { + curl_easy_setopt(curl_->handle, CURLOPT_WRITEFUNCTION, cpr::util::writeUserFunction); + cbs_->writecb_ = write; + curl_easy_setopt(curl_->handle, CURLOPT_WRITEDATA, &cbs_->writecb_); +} + +void Session::SetProgressCallback(const ProgressCallback& progress) { + cbs_->progresscb_ = progress; + if (isCancellable) { + cbs_->cancellationcb_.SetProgressCallback(cbs_->progresscb_); + return; + } +#if LIBCURL_VERSION_NUM < 0x072000 // 7.32.0 + curl_easy_setopt(curl_->handle, CURLOPT_PROGRESSFUNCTION, cpr::util::progressUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_PROGRESSDATA, &cbs_->progresscb_); +#else + curl_easy_setopt(curl_->handle, CURLOPT_XFERINFOFUNCTION, cpr::util::progressUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_XFERINFODATA, &cbs_->progresscb_); +#endif + curl_easy_setopt(curl_->handle, CURLOPT_NOPROGRESS, 0L); +} + +void Session::SetDebugCallback(const DebugCallback& debug) { + curl_easy_setopt(curl_->handle, CURLOPT_DEBUGFUNCTION, cpr::util::debugUserFunction); + cbs_->debugcb_ = debug; + curl_easy_setopt(curl_->handle, CURLOPT_DEBUGDATA, &cbs_->debugcb_); + curl_easy_setopt(curl_->handle, CURLOPT_VERBOSE, 1L); +} + +void Session::SetUrl(const Url& url) { + url_ = url; +} + +void Session::SetResolve(const Resolve& resolve) { + SetResolves({resolve}); +} + +void Session::SetResolves(const std::vector& resolves) { + curl_slist_free_all(curl_->resolveCurlList); + curl_->resolveCurlList = nullptr; + for (const Resolve& resolve : resolves) { + for (const uint16_t port : resolve.ports) { + curl_->resolveCurlList = curl_slist_append(curl_->resolveCurlList, (resolve.host + ":" + std::to_string(port) + ":" + resolve.addr).c_str()); + } + } + curl_easy_setopt(curl_->handle, CURLOPT_RESOLVE, curl_->resolveCurlList); +} + +void Session::SetParameters(const Parameters& parameters) { + parameters_ = parameters; +} + +void Session::SetParameters(Parameters&& parameters) { + parameters_ = std::move(parameters); +} + +void Session::SetHeader(const Header& header) { + header_ = header; +} + +void Session::UpdateHeader(const Header& header) { + for (const std::pair& item : header) { + header_[item.first] = item.second; + } +} + +void Session::SetTimeout(const Timeout& timeout) { + curl_easy_setopt(curl_->handle, CURLOPT_TIMEOUT_MS, timeout.Milliseconds()); +} + +void Session::SetConnectTimeout(const ConnectTimeout& timeout) { + curl_easy_setopt(curl_->handle, CURLOPT_CONNECTTIMEOUT_MS, timeout.Milliseconds()); +} + +void Session::SetAuth(const Authentication& auth) { + // Ignore here since this has been defined by libcurl. + switch (auth.GetAuthMode()) { + case AuthMode::BASIC: + curl_easy_setopt(curl_->handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_easy_setopt(curl_->handle, CURLOPT_USERPWD, auth.GetAuthString()); + break; + case AuthMode::DIGEST: + curl_easy_setopt(curl_->handle, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + curl_easy_setopt(curl_->handle, CURLOPT_USERPWD, auth.GetAuthString()); + break; + case AuthMode::NTLM: + curl_easy_setopt(curl_->handle, CURLOPT_HTTPAUTH, CURLAUTH_NTLM); + curl_easy_setopt(curl_->handle, CURLOPT_USERPWD, auth.GetAuthString()); + break; + case AuthMode::NEGOTIATE: + curl_easy_setopt(curl_->handle, CURLOPT_HTTPAUTH, CURLAUTH_NEGOTIATE); + curl_easy_setopt(curl_->handle, CURLOPT_USERPWD, auth.GetAuthString()); + break; + } +} + +void Session::SetUserAgent(const UserAgent& ua) { + curl_easy_setopt(curl_->handle, CURLOPT_USERAGENT, ua.c_str()); +} + +void Session::SetPayload(const Payload& payload) { + content_ = payload; +} + +void Session::SetPayload(Payload&& payload) { + content_ = std::move(payload); +} + +void Session::SetProxies(const Proxies& proxies) { + proxies_ = proxies; +} + +void Session::SetProxies(Proxies&& proxies) { + proxies_ = std::move(proxies); +} + +void Session::SetProxyAuth(ProxyAuthentication&& proxy_auth) { + proxyAuth_ = std::move(proxy_auth); +} + +void Session::SetProxyAuth(const ProxyAuthentication& proxy_auth) { + proxyAuth_ = proxy_auth; +} + +void Session::SetMultipart(const Multipart& multipart) { + content_ = multipart; +} + +void Session::SetMultipart(Multipart&& multipart) { + content_ = std::move(multipart); +} + +void Session::SetRedirect(const Redirect& redirect) { + curl_easy_setopt(curl_->handle, CURLOPT_FOLLOWLOCATION, redirect.follow ? 1L : 0L); + curl_easy_setopt(curl_->handle, CURLOPT_MAXREDIRS, redirect.maximum); + curl_easy_setopt(curl_->handle, CURLOPT_UNRESTRICTED_AUTH, redirect.cont_send_cred ? 1L : 0L); + + // NOLINTNEXTLINE (google-runtime-int) + long mask = 0; + if (any(redirect.post_flags & PostRedirectFlags::POST_301)) { + mask |= CURL_REDIR_POST_301; + } + if (any(redirect.post_flags & PostRedirectFlags::POST_302)) { + mask |= CURL_REDIR_POST_302; + } + if (any(redirect.post_flags & PostRedirectFlags::POST_303)) { + mask |= CURL_REDIR_POST_303; + } + curl_easy_setopt(curl_->handle, CURLOPT_POSTREDIR, mask); +} + +void Session::SetCookies(const Cookies& cookies) { + curl_easy_setopt(curl_->handle, CURLOPT_COOKIELIST, "ALL"); + curl_easy_setopt(curl_->handle, CURLOPT_COOKIE, cookies.GetEncoded(*curl_).c_str()); +} + +void Session::SetBody(const Body& body) { + content_ = body; +} + +void Session::SetBody(Body&& body) { + content_ = std::move(body); +} + +void Session::SetLowSpeed(const LowSpeed& low_speed) { + curl_easy_setopt(curl_->handle, CURLOPT_LOW_SPEED_LIMIT, low_speed.limit); + curl_easy_setopt(curl_->handle, CURLOPT_LOW_SPEED_TIME, low_speed.time); +} + +void Session::SetVerifySsl(const VerifySsl& verify) { + curl_easy_setopt(curl_->handle, CURLOPT_SSL_VERIFYPEER, verify ? ON : OFF); + curl_easy_setopt(curl_->handle, CURLOPT_SSL_VERIFYHOST, verify ? 2L : 0L); +} + +void Session::SetUnixSocket(const UnixSocket& unix_socket) { + curl_easy_setopt(curl_->handle, CURLOPT_UNIX_SOCKET_PATH, unix_socket.GetUnixSocketString()); +} + +void Session::SetSslOptions(const SslOptions& options) { + if (!options.cert_file.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSLCERT, options.cert_file.c_str()); + if (!options.cert_type.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSLCERTTYPE, options.cert_type.c_str()); + } + } + if (!options.key_file.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSLKEY, options.key_file.c_str()); + if (!options.key_type.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSLKEYTYPE, options.key_type.c_str()); + } + if (!options.key_pass.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_KEYPASSWD, options.key_pass.c_str()); + } +#if SUPPORT_CURLOPT_SSLKEY_BLOB + } else if (!options.key_blob.empty()) { + std::string key_blob(options.key_blob); + curl_blob blob{}; + // NOLINTNEXTLINE (readability-container-data-pointer) + blob.data = &key_blob[0]; + blob.len = key_blob.length(); + curl_easy_setopt(curl_->handle, CURLOPT_SSLKEY_BLOB, &blob); + if (!options.key_type.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSLKEYTYPE, options.key_type.c_str()); + } + if (!options.key_pass.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_KEYPASSWD, options.key_pass.c_str()); + } +#endif + } + if (!options.pinned_public_key.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_PINNEDPUBLICKEY, options.pinned_public_key.c_str()); + } +#if SUPPORT_ALPN + curl_easy_setopt(curl_->handle, CURLOPT_SSL_ENABLE_ALPN, options.enable_alpn ? ON : OFF); +#endif +#if SUPPORT_NPN + curl_easy_setopt(curl_->handle, CURLOPT_SSL_ENABLE_NPN, options.enable_npn ? ON : OFF); +#endif + curl_easy_setopt(curl_->handle, CURLOPT_SSL_VERIFYPEER, options.verify_peer ? ON : OFF); + curl_easy_setopt(curl_->handle, CURLOPT_SSL_VERIFYHOST, options.verify_host ? 2L : 0L); +#if LIBCURL_VERSION_NUM >= 0x072900 // 7.41.0 + curl_easy_setopt(curl_->handle, CURLOPT_SSL_VERIFYSTATUS, options.verify_status ? ON : OFF); +#endif + + int maxTlsVersion = options.ssl_version; +#if SUPPORT_MAX_TLS_VERSION + maxTlsVersion |= options.max_version; +#endif + + curl_easy_setopt(curl_->handle, CURLOPT_SSLVERSION, + // Ignore here since this has been defined by libcurl. + maxTlsVersion); + + // NOLINTNEXTLINE (google-runtime-int) + long curlSslOptions = 0; +#if SUPPORT_SSL_NO_REVOKE + sslNoRevoke_ = options.ssl_no_revoke; + if (options.ssl_no_revoke) { + curlSslOptions |= CURLSSLOPT_NO_REVOKE; + } +#endif +#if LIBCURL_VERSION_NUM >= 0x074700 // 7.71.0 + // Fix loading certs from Windows cert store when using OpenSSL: + curlSslOptions |= CURLSSLOPT_NATIVE_CA; +#endif + curl_easy_setopt(curl_->handle, CURLOPT_SSL_OPTIONS, curlSslOptions); + + if (!options.ca_info.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_CAINFO, options.ca_info.c_str()); + } + if (!options.ca_path.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_CAPATH, options.ca_path.c_str()); + } +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION +#ifdef OPENSSL_BACKEND_USED + if (!options.ca_buffer.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSL_CTX_FUNCTION, sslctx_function_load_ca_cert_from_buffer); + curl_easy_setopt(curl_->handle, CURLOPT_SSL_CTX_DATA, options.ca_buffer.c_str()); + } +#endif +#endif + if (!options.crl_file.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_CRLFILE, options.crl_file.c_str()); + } + if (!options.ciphers.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_SSL_CIPHER_LIST, options.ciphers.c_str()); + } +#if SUPPORT_TLSv13_CIPHERS + if (!options.tls13_ciphers.empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_TLS13_CIPHERS, options.ciphers.c_str()); + } +#endif +#if SUPPORT_SESSIONID_CACHE + curl_easy_setopt(curl_->handle, CURLOPT_SSL_SESSIONID_CACHE, options.session_id_cache ? ON : OFF); +#endif +} + +void Session::SetVerbose(const Verbose& verbose) { + curl_easy_setopt(curl_->handle, CURLOPT_VERBOSE, verbose.verbose ? ON : OFF); +} + +void Session::SetInterface(const Interface& iface) { + if (iface.str().empty()) { + curl_easy_setopt(curl_->handle, CURLOPT_INTERFACE, nullptr); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_INTERFACE, iface.c_str()); + } +} + +void Session::SetLocalPort(const LocalPort& local_port) { + curl_easy_setopt(curl_->handle, CURLOPT_LOCALPORT, static_cast(static_cast(local_port))); +} + +void Session::SetLocalPortRange(const LocalPortRange& local_port_range) { + curl_easy_setopt(curl_->handle, CURLOPT_LOCALPORTRANGE, static_cast(static_cast(local_port_range))); +} + +void Session::SetHttpVersion(const HttpVersion& version) { + switch (version.code) { + case HttpVersionCode::VERSION_NONE: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_NONE); + break; + + case HttpVersionCode::VERSION_1_0: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + break; + + case HttpVersionCode::VERSION_1_1: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + break; + +#if LIBCURL_VERSION_NUM >= 0x072100 // 7.33.0 + case HttpVersionCode::VERSION_2_0: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); + break; +#endif + +#if LIBCURL_VERSION_NUM >= 0x072F00 // 7.47.0 + case HttpVersionCode::VERSION_2_0_TLS: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); + break; +#endif + +#if LIBCURL_VERSION_NUM >= 0x073100 // 7.49.0 + case HttpVersionCode::VERSION_2_0_PRIOR_KNOWLEDGE: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE); + break; +#endif + +#if LIBCURL_VERSION_NUM >= 0x074200 // 7.66.0 + case HttpVersionCode::VERSION_3_0: + curl_easy_setopt(curl_->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3); + break; +#endif + + default: // Should not happen + throw std::invalid_argument("Invalid/Unknown HTTP version type."); + } +} + +void Session::SetRange(const Range& range) { + const std::string range_str = range.str(); + curl_easy_setopt(curl_->handle, CURLOPT_RANGE, range_str.c_str()); +} + +void Session::SetMultiRange(const MultiRange& multi_range) { + const std::string multi_range_str = multi_range.str(); + curl_easy_setopt(curl_->handle, CURLOPT_RANGE, multi_range_str.c_str()); +} + +void Session::SetReserveSize(const ReserveSize& reserve_size) { + ResponseStringReserve(reserve_size.size); +} + +void Session::SetAcceptEncoding(const AcceptEncoding& accept_encoding) { + acceptEncoding_ = accept_encoding; +} + +void Session::SetAcceptEncoding(AcceptEncoding&& accept_encoding) { + acceptEncoding_ = std::move(accept_encoding); +} + +cpr_off_t Session::GetDownloadFileLength() { + cpr_off_t downloadFileLength = -1; + curl_easy_setopt(curl_->handle, CURLOPT_URL, url_.c_str()); + + const std::string protocol = url_.str().substr(0, url_.str().find(':')); + if (proxies_.has(protocol)) { + curl_easy_setopt(curl_->handle, CURLOPT_PROXY, proxies_[protocol].c_str()); + if (proxyAuth_.has(protocol)) { + curl_easy_setopt(curl_->handle, CURLOPT_PROXYUSERNAME, proxyAuth_.GetUsername(protocol)); + curl_easy_setopt(curl_->handle, CURLOPT_PROXYPASSWORD, proxyAuth_.GetPassword(protocol)); + } + } + + curl_easy_setopt(curl_->handle, CURLOPT_HTTPGET, 1); + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 1); + if (DoEasyPerform() == CURLE_OK) { + // NOLINTNEXTLINE (google-runtime-int) + long status_code{}; + curl_easy_getinfo(curl_->handle, CURLINFO_RESPONSE_CODE, &status_code); + if (200 == status_code) { + curl_easy_getinfo(curl_->handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &downloadFileLength); + } + } + return downloadFileLength; +} + +void Session::ResponseStringReserve(size_t size) { + response_string_reserve_size_ = size; +} + +Response Session::Delete() { + PrepareDelete(); + return makeRequest(); +} + +Response Session::Download(const WriteCallback& write) { + PrepareDownload(write); + return makeDownloadRequest(); +} + +Response Session::Download(std::ofstream& file) { + PrepareDownload(file); + return makeDownloadRequest(); +} + +Response Session::Get() { + PrepareGet(); + return makeRequest(); +} + +Response Session::Head() { + PrepareHead(); + return makeRequest(); +} + +Response Session::Options() { + PrepareOptions(); + return makeRequest(); +} + +Response Session::Patch() { + PreparePatch(); + return makeRequest(); +} + +Response Session::Post() { + PreparePost(); + return makeRequest(); +} + +Response Session::Put() { + PreparePut(); + return makeRequest(); +} + +std::shared_ptr Session::GetSharedPtrFromThis() { + try { + return shared_from_this(); + } catch (std::bad_weak_ptr&) { + throw std::runtime_error("Failed to get a shared pointer from this. The reason is probably that the session object is not managed by a shared pointer, which is required to use this functionality."); + } +} + +AsyncResponse Session::GetAsync() { + auto shared_this = shared_from_this(); + return async([shared_this]() { return shared_this->Get(); }); +} + +AsyncResponse Session::DeleteAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Delete(); }); +} + +AsyncResponse Session::DownloadAsync(const WriteCallback& write) { + return async([shared_this = GetSharedPtrFromThis(), write]() { return shared_this->Download(write); }); +} + +AsyncResponse Session::DownloadAsync(std::ofstream& file) { + return async([shared_this = GetSharedPtrFromThis(), &file]() { return shared_this->Download(file); }); +} + +AsyncResponse Session::HeadAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Head(); }); +} + +AsyncResponse Session::OptionsAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Options(); }); +} + +AsyncResponse Session::PatchAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Patch(); }); +} + +AsyncResponse Session::PostAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Post(); }); +} + +AsyncResponse Session::PutAsync() { + return async([shared_this = GetSharedPtrFromThis()]() { return shared_this->Put(); }); +} + +std::shared_ptr Session::GetCurlHolder() { + return curl_; +} + +std::string Session::GetFullRequestUrl() { + const std::string parametersContent = parameters_.GetContent(*curl_); + return url_.str() + (parametersContent.empty() ? "" : "?") + parametersContent; +} + +void Session::PrepareDelete() { + curl_easy_setopt(curl_->handle, CURLOPT_HTTPGET, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "DELETE"); + prepareCommon(); +} + +void Session::PrepareGet() { + // In case there is a body or payload for this request, we create a custom GET-Request since a + // GET-Request with body is based on the HTTP RFC **not** a leagal request. + if (hasBodyOrPayload()) { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "GET"); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, nullptr); + curl_easy_setopt(curl_->handle, CURLOPT_HTTPGET, 1L); + } + prepareCommon(); +} + +void Session::PrepareHead() { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 1L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, nullptr); + prepareCommon(); +} + +void Session::PrepareOptions() { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "OPTIONS"); + prepareCommon(); +} + +void Session::PreparePatch() { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "PATCH"); + prepareCommon(); +} + +void Session::PreparePost() { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + + // In case there is no body or payload set it to an empty post: + if (hasBodyOrPayload()) { + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, nullptr); + } else { + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDS, cbs_->readcb_.callback ? nullptr : ""); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "POST"); + } + prepareCommon(); +} + +void Session::PreparePut() { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + if (!hasBodyOrPayload() && cbs_->readcb_.callback) { + /** + * Yes, this one has to be CURLOPT_POSTFIELDS even if we are performing a PUT request. + * In case we don't set this one, performing a POST-request with PUT won't work. + * It in theory this only enforces the usage of the readcallback for POST requests, but works here as well. + **/ + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDS, nullptr); + } + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl_->handle, CURLOPT_RANGE, nullptr); + prepareCommon(); +} + +void Session::PrepareDownload(std::ofstream& file) { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_HTTPGET, 1); + curl_easy_setopt(curl_->handle, CURLOPT_WRITEFUNCTION, cpr::util::writeFileFunction); + curl_easy_setopt(curl_->handle, CURLOPT_WRITEDATA, &file); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, nullptr); + + prepareCommonDownload(); +} + +void Session::PrepareDownload(const WriteCallback& write) { + curl_easy_setopt(curl_->handle, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_->handle, CURLOPT_HTTPGET, 1); + curl_easy_setopt(curl_->handle, CURLOPT_CUSTOMREQUEST, nullptr); + + SetWriteCallback(write); + + prepareCommonDownload(); +} + +Response Session::Complete(CURLcode curl_error) { + curl_slist* raw_cookies{nullptr}; + curl_easy_getinfo(curl_->handle, CURLINFO_COOKIELIST, &raw_cookies); + Cookies cookies = util::parseCookies(raw_cookies); + curl_slist_free_all(raw_cookies); + + std::string errorMsg = curl_->error.data(); + return Response(curl_, std::move(response_string_), std::move(header_string_), std::move(cookies), Error(curl_error, std::move(errorMsg))); +} + +Response Session::CompleteDownload(CURLcode curl_error) { + if (!cbs_->headercb_.callback) { + curl_easy_setopt(curl_->handle, CURLOPT_HEADERFUNCTION, nullptr); + curl_easy_setopt(curl_->handle, CURLOPT_HEADERDATA, 0); + } + + curl_slist* raw_cookies{nullptr}; + curl_easy_getinfo(curl_->handle, CURLINFO_COOKIELIST, &raw_cookies); + Cookies cookies = util::parseCookies(raw_cookies); + curl_slist_free_all(raw_cookies); + std::string errorMsg = curl_->error.data(); + + return Response(curl_, "", std::move(header_string_), std::move(cookies), Error(curl_error, std::move(errorMsg))); +} + +void Session::AddInterceptor(const std::shared_ptr& pinterceptor) { + // Shall only add before first interceptor run + assert(current_interceptor_ == interceptors_.end()); + interceptors_.push_back(pinterceptor); + first_interceptor_ = interceptors_.begin(); +} + +Response Session::proceed() { + prepareCommon(); + return makeRequest(); +} + +const std::optional Session::intercept() { + if (current_interceptor_ == interceptors_.end()) { + current_interceptor_ = first_interceptor_; + } else { + current_interceptor_++; + } + + if (current_interceptor_ != interceptors_.end()) { + auto icpt = current_interceptor_; + // Nested makeRequest() start at first_interceptor_, thus excluding previous interceptors. + first_interceptor_ = current_interceptor_; + ++first_interceptor_; + + const std::optional r = (*current_interceptor_)->intercept(*this); + + first_interceptor_ = icpt; + + return r; + } + return std::nullopt; +} + +void Session::prepareBodyPayloadOrMultipart() const { + // Either a body, multipart or a payload is allowed. + + if (std::holds_alternative(content_)) { + const std::string payload = std::get(content_).GetContent(*curl_); + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDSIZE_LARGE, static_cast(payload.length())); + curl_easy_setopt(curl_->handle, CURLOPT_COPYPOSTFIELDS, payload.c_str()); + } else if (std::holds_alternative(content_)) { + const std::string& body = std::get(content_).str(); + curl_easy_setopt(curl_->handle, CURLOPT_POSTFIELDSIZE_LARGE, static_cast(body.length())); + curl_easy_setopt(curl_->handle, CURLOPT_COPYPOSTFIELDS, body.c_str()); + } else if (std::holds_alternative(content_)) { + // Make sure, we have a empty multipart to start with: + if (curl_->multipart) { + curl_mime_free(curl_->multipart); + } + curl_->multipart = curl_mime_init(curl_->handle); + + // Add all multipart pieces: + const cpr::Multipart& multipart = std::get(content_); + for (const Part& part : multipart.parts) { + if (part.is_file) { + for (const File& file : part.files) { + curl_mimepart* mimePart = curl_mime_addpart(curl_->multipart); + if (!part.content_type.empty()) { + curl_mime_type(mimePart, part.content_type.c_str()); + } + + curl_mime_filedata(mimePart, file.filepath.c_str()); + curl_mime_name(mimePart, part.name.c_str()); + + if (file.hasOverridenFilename()) { + curl_mime_filename(mimePart, file.overriden_filename.c_str()); + } else { + // NOLINTNEXTLINE (misc-include-cleaner) + curl_mime_filename(mimePart, fs::path(file.filepath).filename().string().c_str()); + } + } + } else { + curl_mimepart* mimePart = curl_mime_addpart(curl_->multipart); + if (!part.content_type.empty()) { + curl_mime_type(mimePart, part.content_type.c_str()); + } + if (part.is_buffer) { + // Do not use formdata, to prevent having to use reinterpreter_cast: + curl_mime_name(mimePart, part.name.c_str()); + curl_mime_data(mimePart, part.data, part.datalen); + curl_mime_filename(mimePart, part.value.c_str()); + } else { + curl_mime_name(mimePart, part.name.c_str()); + curl_mime_data(mimePart, part.value.c_str(), CURL_ZERO_TERMINATED); + } + } + } + + curl_easy_setopt(curl_->handle, CURLOPT_MIMEPOST, curl_->multipart); + } +} + +[[nodiscard]] bool Session::hasBodyOrPayload() const { + return std::holds_alternative(content_) || std::holds_alternative(content_); +} + +// clang-format off +void Session::SetOption(const Resolve& resolve) { SetResolve(resolve); } +void Session::SetOption(const std::vector& resolves) { SetResolves(resolves); } +void Session::SetOption(const ReadCallback& read) { SetReadCallback(read); } +void Session::SetOption(const HeaderCallback& header) { SetHeaderCallback(header); } +void Session::SetOption(const WriteCallback& write) { SetWriteCallback(write); } +void Session::SetOption(const ProgressCallback& progress) { SetProgressCallback(progress); } +void Session::SetOption(const DebugCallback& debug) { SetDebugCallback(debug); } +void Session::SetOption(const Url& url) { SetUrl(url); } +void Session::SetOption(const Parameters& parameters) { SetParameters(parameters); } +void Session::SetOption(Parameters&& parameters) { SetParameters(std::move(parameters)); } +void Session::SetOption(const Header& header) { SetHeader(header); } +void Session::SetOption(const Timeout& timeout) { SetTimeout(timeout); } +void Session::SetOption(const ConnectTimeout& timeout) { SetConnectTimeout(timeout); } +void Session::SetOption(const Authentication& auth) { SetAuth(auth); } +void Session::SetOption(const LimitRate& limit_rate) { SetLimitRate(limit_rate); } +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 +void Session::SetOption(const Bearer& auth) { SetBearer(auth); } +#endif +void Session::SetOption(const UserAgent& ua) { SetUserAgent(ua); } +void Session::SetOption(const Payload& payload) { SetPayload(payload); } +void Session::SetOption(Payload&& payload) { SetPayload(std::move(payload)); } +void Session::SetOption(const Proxies& proxies) { SetProxies(proxies); } +void Session::SetOption(Proxies&& proxies) { SetProxies(std::move(proxies)); } +void Session::SetOption(ProxyAuthentication&& proxy_auth) { SetProxyAuth(std::move(proxy_auth)); } +void Session::SetOption(const ProxyAuthentication& proxy_auth) { SetProxyAuth(proxy_auth); } +void Session::SetOption(const Multipart& multipart) { SetMultipart(multipart); } +void Session::SetOption(Multipart&& multipart) { SetMultipart(std::move(multipart)); } +void Session::SetOption(const Redirect& redirect) { SetRedirect(redirect); } +void Session::SetOption(const Cookies& cookies) { SetCookies(cookies); } +void Session::SetOption(const Body& body) { SetBody(body); } +void Session::SetOption(Body&& body) { SetBody(std::move(body)); } +void Session::SetOption(const LowSpeed& low_speed) { SetLowSpeed(low_speed); } +void Session::SetOption(const VerifySsl& verify) { SetVerifySsl(verify); } +void Session::SetOption(const Verbose& verbose) { SetVerbose(verbose); } +void Session::SetOption(const UnixSocket& unix_socket) { SetUnixSocket(unix_socket); } +void Session::SetOption(const SslOptions& options) { SetSslOptions(options); } +void Session::SetOption(const Interface& iface) { SetInterface(iface); } +void Session::SetOption(const LocalPort& local_port) { SetLocalPort(local_port); } +void Session::SetOption(const LocalPortRange& local_port_range) { SetLocalPortRange(local_port_range); } +void Session::SetOption(const HttpVersion& version) { SetHttpVersion(version); } +void Session::SetOption(const Range& range) { SetRange(range); } +void Session::SetOption(const MultiRange& multi_range) { SetMultiRange(multi_range); } +void Session::SetOption(const ReserveSize& reserve_size) { SetReserveSize(reserve_size.size); } +void Session::SetOption(const AcceptEncoding& accept_encoding) { SetAcceptEncoding(accept_encoding); } +void Session::SetOption(AcceptEncoding&& accept_encoding) { SetAcceptEncoding(std::move(accept_encoding)); } +// clang-format on + +void Session::SetCancellationParam(std::shared_ptr param) { + cbs_->cancellationcb_ = CancellationCallback{std::move(param)}; + isCancellable = true; +#if LIBCURL_VERSION_NUM < 0x072000 // 7.32.0 + curl_easy_setopt(curl_->handle, CURLOPT_PROGRESSFUNCTION, cpr::util::progressUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_PROGRESSDATA, &cbs_->cancellationcb_); +#else + curl_easy_setopt(curl_->handle, CURLOPT_XFERINFOFUNCTION, cpr::util::progressUserFunction); + curl_easy_setopt(curl_->handle, CURLOPT_XFERINFODATA, &cbs_->cancellationcb_); +#endif + curl_easy_setopt(curl_->handle, CURLOPT_NOPROGRESS, 0L); +} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/ssl_ctx.cpp b/3rdparty/cpr/cpr/ssl_ctx.cpp new file mode 100644 index 0000000000000000000000000000000000000000..dcc1cdec8265720a3c3a8e03c9c1eb64036de2fd --- /dev/null +++ b/3rdparty/cpr/cpr/ssl_ctx.cpp @@ -0,0 +1,115 @@ + +#include "cpr/ssl_ctx.h" +#include "cpr/ssl_options.h" +#include +#include +#include +#include +#include +#include + +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION + +#ifdef OPENSSL_BACKEND_USED + +#include +#include +#include +#include +#include +#include +#include + +// openssl/types.h was added in later version of openssl and is therefore not always available. +// This is for example the case on Ubuntu 20.04. +// We try to include it if available to satisfy clang-tidy. +// Ref: https://github.com/openssl/openssl/commit/50cd4768c6b89c757645f28519236bb989216f8d +#if __has_include() +#include +#else +#include +#endif + +namespace cpr { + +/** + * The ssl_ctx parameter is actually a pointer to the SSL library's SSL_CTX for OpenSSL. + * If an error is returned from the callback no attempt to establish a connection is made and + * the perform operation will return the callback's error code. + * + * Sources: https://curl.se/libcurl/c/CURLOPT_SSL_CTX_FUNCTION.html + * https://curl.se/libcurl/c/CURLOPT_SSL_CTX_DATA.html + */ + +template +struct deleter_from_fn { + template + constexpr void operator()(T* arg) const { + fn(arg); + } +}; + +template +using custom_unique_ptr = std::unique_ptr>; +using x509_ptr = custom_unique_ptr; +using bio_ptr = custom_unique_ptr; + +namespace { +inline std::string get_openssl_print_errors() { + std::ostringstream oss; + ERR_print_errors_cb( + [](char const* str, size_t len, void* data) -> int { + auto& oss = *static_cast(data); + oss << str; + return static_cast(len); + }, + &oss); + return oss.str(); +} + +} // namespace + +CURLcode sslctx_function_load_ca_cert_from_buffer(CURL* /*curl*/, void* sslctx, void* raw_cert_buf) { + // Check arguments + if (raw_cert_buf == nullptr || sslctx == nullptr) { + std::cerr << "Invalid callback arguments!\n"; + return CURLE_ABORTED_BY_CALLBACK; + } + + // Get a pointer to the current certificate verification storage + auto* store = SSL_CTX_get_cert_store(static_cast(sslctx)); + + // Create a memory BIO using the data of cert_buf. + // Note: It is assumed, that cert_buf is nul terminated and its length is determined by strlen. + const bio_ptr bio{BIO_new_mem_buf(static_cast(raw_cert_buf), -1)}; + + bool at_least_got_one = false; + for (;;) { + // Load the PEM formatted certicifate into an X509 structure which OpenSSL can use. + const x509_ptr x{PEM_read_bio_X509_AUX(bio.get(), nullptr, nullptr, nullptr)}; + if (x == nullptr) { + if ((ERR_GET_REASON(ERR_peek_last_error()) == PEM_R_NO_START_LINE) && at_least_got_one) { + ERR_clear_error(); + break; + } + std::cerr << "PEM_read_bio_X509_AUX failed: \n" << get_openssl_print_errors() << '\n'; + return CURLE_ABORTED_BY_CALLBACK; + } + + // Add the loaded certificate to the verification storage + if (X509_STORE_add_cert(store, x.get()) == 0) { + std::cerr << "X509_STORE_add_cert failed: \n" << get_openssl_print_errors() << '\n'; + return CURLE_ABORTED_BY_CALLBACK; + } + at_least_got_one = true; + } + + // The CA certificate was loaded successfully into the verification storage + return CURLE_OK; +} + +} // namespace cpr + +#endif // OPENSSL_BACKEND_USED + +#endif // SUPPORT_CURLOPT_SSL_CTX_FUNCTION diff --git a/3rdparty/cpr/cpr/threadpool.cpp b/3rdparty/cpr/cpr/threadpool.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ff4317037ef89869475e0e0e715f061ac2246585 --- /dev/null +++ b/3rdparty/cpr/cpr/threadpool.cpp @@ -0,0 +1,160 @@ +#include "cpr/threadpool.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cpr { + +ThreadPool::ThreadPool(size_t min_threads, size_t max_threads, std::chrono::milliseconds max_idle_ms) : min_thread_num(min_threads), max_thread_num(max_threads), max_idle_time(max_idle_ms) {} + +ThreadPool::~ThreadPool() { + Stop(); +} + +int ThreadPool::Start(size_t start_threads) { + if (status != STOP) { + return -1; + } + status = RUNNING; + start_threads = std::clamp(start_threads, min_thread_num, max_thread_num); + for (size_t i = 0; i < start_threads; ++i) { + CreateThread(); + } + return 0; +} + +int ThreadPool::Stop() { + const std::unique_lock status_lock(status_wait_mutex); + if (status == STOP) { + return -1; + } + + status = STOP; + status_wait_cond.notify_all(); + task_cond.notify_all(); + + for (auto& i : threads) { + if (i.thread->joinable()) { + i.thread->join(); + } + } + + threads.clear(); + cur_thread_num = 0; + idle_thread_num = 0; + return 0; +} + +int ThreadPool::Pause() { + if (status == RUNNING) { + status = PAUSE; + } + return 0; +} + +int ThreadPool::Resume() { + const std::unique_lock status_lock(status_wait_mutex); + if (status == PAUSE) { + status = RUNNING; + status_wait_cond.notify_all(); + } + return 0; +} + +int ThreadPool::Wait() { + while (true) { + if (status == STOP || (tasks.empty() && idle_thread_num == cur_thread_num)) { + break; + } + std::this_thread::yield(); + } + return 0; +} + +bool ThreadPool::CreateThread() { + if (cur_thread_num >= max_thread_num) { + return false; + } + std::thread* thread = new std::thread([this] { + bool initialRun = true; + while (status != STOP) { + { + std::unique_lock status_lock(status_wait_mutex); + status_wait_cond.wait(status_lock, [this]() { return status != Status::PAUSE; }); + } + + Task task; + { + std::unique_lock locker(task_mutex); + task_cond.wait_for(locker, std::chrono::milliseconds(max_idle_time), [this]() { return status == STOP || !tasks.empty(); }); + if (status == STOP) { + return; + } + if (tasks.empty()) { + if (cur_thread_num > min_thread_num) { + DelThread(std::this_thread::get_id()); + return; + } + continue; + } + if (!initialRun) { + --idle_thread_num; + } + task = std::move(tasks.front()); + tasks.pop(); + } + if (task) { + task(); + ++idle_thread_num; + if (initialRun) { + initialRun = false; + } + } + } + }); + AddThread(thread); + return true; +} + +void ThreadPool::AddThread(std::thread* thread) { + thread_mutex.lock(); + ++cur_thread_num; + ThreadData data; + data.thread = std::shared_ptr(thread); + data.id = thread->get_id(); + data.status = RUNNING; + data.start_time = std::chrono::steady_clock::now(); + data.stop_time = std::chrono::steady_clock::time_point::max(); + threads.emplace_back(data); + thread_mutex.unlock(); +} + +void ThreadPool::DelThread(std::thread::id id) { + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + + thread_mutex.lock(); + --cur_thread_num; + --idle_thread_num; + auto iter = threads.begin(); + while (iter != threads.end()) { + if (iter->status == STOP && now > iter->stop_time) { + if (iter->thread->joinable()) { + iter->thread->join(); + iter = threads.erase(iter); + continue; + } + } else if (iter->id == id) { + iter->status = STOP; + iter->stop_time = std::chrono::steady_clock::now(); + } + ++iter; + } + thread_mutex.unlock(); +} + +} // namespace cpr diff --git a/3rdparty/cpr/cpr/timeout.cpp b/3rdparty/cpr/cpr/timeout.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a2a7c90fb5015a49c94fb19b99c29ca998cf6cc2 --- /dev/null +++ b/3rdparty/cpr/cpr/timeout.cpp @@ -0,0 +1,32 @@ +#include "cpr/timeout.h" + +#include +#include +#include +#include +#include + +namespace cpr { + +// No way around since curl uses a long here. +// NOLINTNEXTLINE(google-runtime-int) +long Timeout::Milliseconds() const { + static_assert(std::is_same_v, "Following casting expects milliseconds."); + + // No way around since curl uses a long here. + // NOLINTNEXTLINE(google-runtime-int) + if (ms.count() > static_cast(std::numeric_limits::max())) { + throw std::overflow_error("cpr::Timeout: timeout value overflow: " + std::to_string(ms.count()) + " ms."); + } + // No way around since curl uses a long here. + // NOLINTNEXTLINE(google-runtime-int) + if (ms.count() < static_cast(std::numeric_limits::min())) { + throw std::underflow_error("cpr::Timeout: timeout value underflow: " + std::to_string(ms.count()) + " ms."); + } + + // No way around since curl uses a long here. + // NOLINTNEXTLINE(google-runtime-int) + return static_cast(ms.count()); +} + +} // namespace cpr diff --git a/3rdparty/cpr/cpr/unix_socket.cpp b/3rdparty/cpr/cpr/unix_socket.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3171dbc8f6874397c5220f791f39943350948d6a --- /dev/null +++ b/3rdparty/cpr/cpr/unix_socket.cpp @@ -0,0 +1,8 @@ + +#include "cpr/unix_socket.h" + +namespace cpr { +const char* UnixSocket::GetUnixSocketString() const noexcept { + return unix_socket_.data(); +} +} // namespace cpr diff --git a/3rdparty/cpr/cpr/util.cpp b/3rdparty/cpr/cpr/util.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a33c138d1266a56d34bea07456fe5ee9dce27e0c --- /dev/null +++ b/3rdparty/cpr/cpr/util.cpp @@ -0,0 +1,254 @@ +#include "cpr/util.h" +#include "cpr/callback.h" +#include "cpr/cookies.h" +#include "cpr/cprtypes.h" +#include "cpr/curlholder.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_Win32) +#include +#else +#ifdef __clang__ +#pragma clang diagnostic push +#if __has_warning("-Wreserved-macro-identifier") // Not all versions of clang support this flag like the one used on Ubuntu 18.04 +#pragma clang diagnostic ignored "-Wreserved-macro-identifier" +#endif +#pragma clang diagnostic ignored "-Wunused-macros" +#endif +// https://en.cppreference.com/w/c/string/byte/memset +// NOLINTNEXTLINE(bugprone-reserved-identifier, cert-dcl37-c, cert-dcl51-cpp, cppcoreguidelines-macro-usage) +#define __STDC_WANT_LIB_EXT1__ 1 +#ifdef __clang__ +#pragma clang diagnostic pop +#endif +#include +#endif + +namespace cpr::util { + +enum class CurlHTTPCookieField : uint8_t { + Domain = 0, + IncludeSubdomains, + Path, + HttpsOnly, + Expires, + Name, + Value, +}; + +Cookies parseCookies(curl_slist* raw_cookies) { + const int CURL_HTTP_COOKIE_SIZE = static_cast(CurlHTTPCookieField::Value) + 1; + Cookies cookies; + for (curl_slist* nc = raw_cookies; nc; nc = nc->next) { + std::vector tokens = cpr::util::split(nc->data, '\t'); + while (tokens.size() < CURL_HTTP_COOKIE_SIZE) { + tokens.emplace_back(""); + } + const std::time_t expires = sTimestampToT(tokens.at(static_cast(CurlHTTPCookieField::Expires))); + cookies.emplace_back(Cookie{ + tokens.at(static_cast(CurlHTTPCookieField::Name)), + tokens.at(static_cast(CurlHTTPCookieField::Value)), + tokens.at(static_cast(CurlHTTPCookieField::Domain)), + isTrue(tokens.at(static_cast(CurlHTTPCookieField::IncludeSubdomains))), + tokens.at(static_cast(CurlHTTPCookieField::Path)), + isTrue(tokens.at(static_cast(CurlHTTPCookieField::HttpsOnly))), + std::chrono::system_clock::from_time_t(expires), + }); + } + return cookies; +} + +Header parseHeader(const std::string& headers, std::string* status_line, std::string* reason) { + Header header; + std::istringstream stream(headers); + std::string line; + while (std::getline(stream, line, '\n')) { + if (line.substr(0, 5) == "HTTP/") { + // set the status_line if it was given + if ((status_line != nullptr) || (reason != nullptr)) { + line.resize(std::min(line.size(), line.find_last_not_of("\t\n\r ") + 1)); + if (status_line != nullptr) { + *status_line = line; + } + + // set the reason if it was given + if (reason != nullptr) { + const size_t pos1 = line.find_first_of("\t "); + size_t pos2 = std::string::npos; + if (pos1 != std::string::npos) { + pos2 = line.find_first_of("\t ", pos1 + 1); + } + if (pos2 != std::string::npos) { + line.erase(0, pos2 + 1); + *reason = line; + } + } + } + header.clear(); + } + + if (!line.empty()) { + const size_t found = line.find(':'); + if (found != std::string::npos) { + std::string value = line.substr(found + 1); + value.erase(0, value.find_first_not_of("\t ")); + value.resize(std::min(value.size(), value.find_last_not_of("\t\n\r ") + 1)); + header[line.substr(0, found)] = value; + } + } + } + + return header; +} + +std::vector split(const std::string& to_split, char delimiter) { + std::vector tokens; + + std::stringstream stream(to_split); + std::string item; + while (std::getline(stream, item, delimiter)) { + tokens.push_back(item); + } + + return tokens; +} + +size_t readUserFunction(char* ptr, size_t size, size_t nitems, const ReadCallback* read) { + size *= nitems; + return (*read)(ptr, size) ? size : CURL_READFUNC_ABORT; +} + +size_t headerUserFunction(char* ptr, size_t size, size_t nmemb, const HeaderCallback* header) { + size *= nmemb; + return (*header)({ptr, size}) ? size : 0; +} + +size_t writeFunction(char* ptr, size_t size, size_t nmemb, std::string* data) { + size *= nmemb; + data->append(ptr, size); + return size; +} + +size_t writeFileFunction(char* ptr, size_t size, size_t nmemb, std::ofstream* file) { + size *= nmemb; + file->write(ptr, static_cast(size)); + return size; +} + +size_t writeUserFunction(char* ptr, size_t size, size_t nmemb, const WriteCallback* write) { + size *= nmemb; + return (*write)({ptr, size}) ? size : 0; +} + +int debugUserFunction(CURL* /*handle*/, curl_infotype type, char* data, size_t size, const DebugCallback* debug) { + (*debug)(static_cast(type), std::string(data, size)); + return 0; +} + +/** + * Creates a temporary CurlHolder object and uses it to escape the given string. + * If you plan to use this methode on a regular basis think about creating a CurlHolder + * object and calling urlEncode(std::string) on it. + * + * Example: + * CurlHolder holder; + * std::string input = "Hello World!"; + * std::string result = holder.urlEncode(input); + **/ +std::string urlEncode(const std::string& s) { + const CurlHolder holder; // Create a temporary new holder for URL encoding + return holder.urlEncode(s); +} + +/** + * Creates a temporary CurlHolder object and uses it to unescape the given string. + * If you plan to use this methode on a regular basis think about creating a CurlHolder + * object and calling urlDecode(std::string) on it. + * + * Example: + * CurlHolder holder; + * std::string input = "Hello%20World%21"; + * std::string result = holder.urlDecode(input); + **/ +std::string urlDecode(const std::string& s) { + const CurlHolder holder; // Create a temporary new holder for URL decoding + return holder.urlDecode(s); +} + +#if defined(__STDC_LIB_EXT1__) +void secureStringClear(std::string& s) { + if (s.empty()) { + return; + } + memset_s(&s.front(), s.length(), 0, s.length()); + s.clear(); +} +#elif defined(_WIN32) +void secureStringClear(std::string& s) { + if (s.empty()) { + return; + } + SecureZeroMemory(&s.front(), s.length()); + s.clear(); +} +#else +#if defined(__clang__) +#pragma clang optimize off // clang +#elif defined(__GNUC__) || defined(__MINGW32__) || defined(__MINGW32__) || defined(__MINGW64__) +#pragma GCC push_options // g++ +#pragma GCC optimize("O0") // g++ +#endif +void secureStringClear(std::string& s) { + if (s.empty()) { + return; + } + // NOLINTNEXTLINE (readability-container-data-pointer) + char* ptr = &(s[0]); + memset(ptr, '\0', s.length()); + s.clear(); +} + +#if defined(__clang__) +#pragma clang optimize on // clang +#elif defined(__GNUC__) || defined(__MINGW32__) || defined(__MINGW32__) || defined(__MINGW64__) +#pragma GCC pop_options // g++ +#endif +#endif + +bool isTrue(const std::string& s) { + std::string temp_string{s}; + std::transform(temp_string.begin(), temp_string.end(), temp_string.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return temp_string == "true"; +} + +time_t sTimestampToT(const std::string& st) { + // NOLINTNEXTLINE(google-runtime-int) + if (std::is_same_v) { + return static_cast(std::stoul(st)); + } + // NOLINTNEXTLINE(google-runtime-int) + if (std::is_same_v) { + return static_cast(std::stoull(st)); + } + if (std::is_same_v) { + return static_cast(std::stoi(st)); + } + // NOLINTNEXTLINE(google-runtime-int) + if (std::is_same_v) { + return static_cast(std::stol(st)); + } + return static_cast(std::stoll(st)); +} + +} // namespace cpr::util diff --git a/3rdparty/cpr/include/CMakeLists.txt b/3rdparty/cpr/include/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..dc704d56ab39d692a73bcfad276f72a25d60ee7f --- /dev/null +++ b/3rdparty/cpr/include/CMakeLists.txt @@ -0,0 +1,102 @@ +cmake_minimum_required(VERSION 3.15) + +target_include_directories(cpr PUBLIC + $ + $ + $) + +target_sources(cpr PRIVATE + # Header files (useful in IDEs) + cpr/accept_encoding.h + cpr/api.h + cpr/async.h + cpr/async_wrapper.h + cpr/auth.h + cpr/bearer.h + cpr/body.h + cpr/buffer.h + cpr/cert_info.h + cpr/cookies.h + cpr/cpr.h + cpr/cprtypes.h + cpr/curlholder.h + cpr/curlholder.h + cpr/error.h + cpr/file.h + cpr/limit_rate.h + cpr/local_port.h + cpr/local_port_range.h + cpr/multipart.h + cpr/parameters.h + cpr/payload.h + cpr/proxies.h + cpr/proxyauth.h + cpr/response.h + cpr/session.h + cpr/singleton.h + cpr/ssl_ctx.h + cpr/ssl_options.h + cpr/threadpool.h + cpr/timeout.h + cpr/unix_socket.h + cpr/util.h + cpr/verbose.h + cpr/interface.h + cpr/redirect.h + cpr/http_version.h + cpr/interceptor.h + cpr/filesystem.h + cpr/curlmultiholder.h + cpr/multiperform.h + cpr/resolve.h + ${PROJECT_BINARY_DIR}/cpr_generated_includes/cpr/cprver.h +) + +# Filesystem +if(CPR_USE_BOOST_FILESYSTEM) + find_package(Boost 1.77 REQUIRED COMPONENTS filesystem) + if(Boost_FOUND) + target_link_libraries(cpr PUBLIC Boost::filesystem) + endif() +else() + try_compile( + STD_FS_SUPPORTED + "${CMAKE_CURRENT_BINARY_DIR}/std_fs" + SOURCES "${PROJECT_SOURCE_DIR}/cmake/std_fs_support_test.cpp" + CXX_STANDARD ${CMAKE_CXX_STANDARD} + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + ) + if(NOT STD_FS_SUPPORTED) + try_compile( + STDCXXFS_SUPPORTED + "${CMAKE_CURRENT_BINARY_DIR}/stdcxxfs" + SOURCES "${PROJECT_SOURCE_DIR}/cmake/std_fs_support_test.cpp" + LINK_LIBRARIES "stdc++fs" + CXX_STANDARD ${CMAKE_CXX_STANDARD} + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + ) + if(STDCXXFS_SUPPORTED) + target_link_libraries(cpr PUBLIC stdc++fs) + else() + try_compile( + CXXFS_SUPPORTED + "${CMAKE_CURRENT_BINARY_DIR}/cxxfs" + SOURCES "${PROJECT_SOURCE_DIR}/cmake/std_fs_support_test.cpp" + LINK_LIBRARIES "c++fs" + CXX_STANDARD ${CMAKE_CXX_STANDARD} + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + ) + if(CXXFS_SUPPORTED) + target_link_libraries(cpr PUBLIC c++fs) + else() + message(FATAL_ERROR "Your compiler does not support the `` include or cpr is unable to determine the proper compiler flags for it.") + endif() + endif() + endif() +endif() + +install(DIRECTORY cpr DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(DIRECTORY ${PROJECT_BINARY_DIR}/cpr_generated_includes/cpr DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) diff --git a/3rdparty/cpr/include/cpr/accept_encoding.h b/3rdparty/cpr/include/cpr/accept_encoding.h new file mode 100644 index 0000000000000000000000000000000000000000..167d7c2cf636150a33de9e4c6d3b833ff8197b20 --- /dev/null +++ b/3rdparty/cpr/include/cpr/accept_encoding.h @@ -0,0 +1,41 @@ +#ifndef CPR_ACCEPT_ENCODING_H +#define CPR_ACCEPT_ENCODING_H + +#include +#include +#include +#include +#include + +namespace cpr { + +enum class AcceptEncodingMethods { + identity, + deflate, + zlib, + gzip, + disabled, +}; + +// NOLINTNEXTLINE(cert-err58-cpp) +static const std::map AcceptEncodingMethodsStringMap{{AcceptEncodingMethods::identity, "identity"}, {AcceptEncodingMethods::deflate, "deflate"}, {AcceptEncodingMethods::zlib, "zlib"}, {AcceptEncodingMethods::gzip, "gzip"}, {AcceptEncodingMethods::disabled, "disabled"}}; + +class AcceptEncoding { + public: + AcceptEncoding() = default; + // NOLINTNEXTLINE(google-explicit-constructor) + AcceptEncoding(const std::initializer_list& methods); + // NOLINTNEXTLINE(google-explicit-constructor) + AcceptEncoding(const std::initializer_list& methods); + + [[nodiscard]] bool empty() const noexcept; + [[nodiscard]] const std::string getString() const; + [[nodiscard]] bool disabled() const; + + private: + std::unordered_set methods_; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/api.h b/3rdparty/cpr/include/cpr/api.h new file mode 100644 index 0000000000000000000000000000000000000000..a53418bb21311d16ec7f417d0665e9db1921ddf3 --- /dev/null +++ b/3rdparty/cpr/include/cpr/api.h @@ -0,0 +1,392 @@ +#ifndef CPR_API_H +#define CPR_API_H + +#include +#include +#include +#include +#include + +#include "cpr/async.h" +#include "cpr/async_wrapper.h" +#include "cpr/auth.h" +#include "cpr/bearer.h" +#include "cpr/cprtypes.h" +#include "cpr/filesystem.h" +#include "cpr/multipart.h" +#include "cpr/multiperform.h" +#include "cpr/payload.h" +#include "cpr/response.h" +#include "cpr/session.h" + +namespace cpr { + +using AsyncResponse = AsyncWrapper; + +namespace priv { + +template +void set_option_internal(Session& session, CurrentType&& current_option) { + session.SetOption(std::forward(current_option)); +} + +template <> +inline void set_option_internal(Session& session, Header&& current_option) { + // Header option was already provided -> Update previous header + session.UpdateHeader(std::forward
(current_option)); +} + +template +void set_option_internal(Session& session, CurrentType&& current_option, Ts&&... ts) { + set_option_internal(session, std::forward(current_option)); + + if (std::is_same_v) { + set_option_internal(session, std::forward(ts)...); + } else { + set_option_internal(session, std::forward(ts)...); + } +} + +template +void set_option(Session& session, Ts&&... ts) { + set_option_internal(session, std::forward(ts)...); +} + +// Idea: https://stackoverflow.com/a/19060157 +template +void apply_set_option_internal(Session& session, Tuple&& t, std::index_sequence) { + set_option(session, std::get(std::forward(t))...); +} + +// Idea: https://stackoverflow.com/a/19060157 +template +void apply_set_option(Session& session, Tuple&& t) { + using Indices = std::make_index_sequence>::value>; + apply_set_option_internal(session, std::forward(t), Indices()); +} + +template +void setup_multiperform_internal(MultiPerform& multiperform, T&& t) { + std::shared_ptr session = std::make_shared(); + apply_set_option(*session, t); + multiperform.AddSession(session); +} + +template +void setup_multiperform_internal(MultiPerform& multiperform, T&& t, Ts&&... ts) { + std::shared_ptr session = std::make_shared(); + apply_set_option(*session, t); + multiperform.AddSession(session); + setup_multiperform_internal(multiperform, std::forward(ts)...); +} + +template +void setup_multiperform(MultiPerform& multiperform, Ts&&... ts) { + setup_multiperform_internal(multiperform, std::forward(ts)...); +} + +using session_action_t = cpr::Response (cpr::Session::*)(); + +template +void setup_multiasync(std::vector>& responses, T&& parameters) { + std::shared_ptr cancellation_state = std::make_shared(false); + + std::function execFn{[cancellation_state](T params) { + if (cancellation_state->load()) { + return Response{}; + } + cpr::Session s{}; + s.SetCancellationParam(cancellation_state); + apply_set_option(s, std::forward(params)); + return std::invoke(SessionAction, s); + }}; + responses.emplace_back(GlobalThreadPool::GetInstance()->Submit(std::move(execFn), std::forward(parameters)), std::move(cancellation_state)); +} + +template +void setup_multiasync(std::vector>& responses, T&& head, Ts&&... tail) { + setup_multiasync(responses, std::forward(head)); + if constexpr (sizeof...(Ts) > 0) { + setup_multiasync(responses, std::forward(tail)...); + } +} + +} // namespace priv + +// Get methods +template +Response Get(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Get(); +} + +// Get async methods +template +AsyncResponse GetAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Get(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Get callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto GetCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Get(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Post methods +template +Response Post(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Post(); +} + +// Post async methods +template +AsyncResponse PostAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Post(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Post callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto PostCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Post(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Put methods +template +Response Put(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Put(); +} + +// Put async methods +template +AsyncResponse PutAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Put(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Put callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto PutCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Put(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Head methods +template +Response Head(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Head(); +} + +// Head async methods +template +AsyncResponse HeadAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Head(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Head callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto HeadCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Head(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Delete methods +template +Response Delete(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Delete(); +} + +// Delete async methods +template +AsyncResponse DeleteAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Delete(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Delete callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto DeleteCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Delete(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Options methods +template +Response Options(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Options(); +} + +// Options async methods +template +AsyncResponse OptionsAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Options(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Options callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto OptionsCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Options(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Patch methods +template +Response Patch(Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Patch(); +} + +// Patch async methods +template +AsyncResponse PatchAsync(Ts... ts) { + return cpr::async([](Ts... ts_inner) { return Patch(std::move(ts_inner)...); }, std::move(ts)...); +} + +// Patch callback methods +template +// NOLINTNEXTLINE(fuchsia-trailing-return) +auto PatchCallback(Then then, Ts... ts) { + return cpr::async([](Then then_inner, Ts... ts_inner) { return then_inner(Patch(std::move(ts_inner)...)); }, std::move(then), std::move(ts)...); +} + +// Download methods +template +Response Download(std::ofstream& file, Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Download(file); +} + +// Download async method +template +AsyncResponse DownloadAsync(fs::path local_path, Ts... ts) { + return AsyncWrapper{std::async( + std::launch::async, + [](fs::path local_path_, Ts... ts_) { + std::ofstream f(local_path_.c_str()); + return Download(f, std::move(ts_)...); + }, + std::move(local_path), std::move(ts)...)}; +} + +// Download with user callback +template +Response Download(const WriteCallback& write, Ts&&... ts) { + Session session; + priv::set_option(session, std::forward(ts)...); + return session.Download(write); +} + +// Multi requests +template +std::vector MultiGet(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Get(); +} + +template +std::vector MultiDelete(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Delete(); +} + +template +std::vector MultiPut(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Put(); +} + +template +std::vector MultiHead(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Head(); +} + +template +std::vector MultiOptions(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Options(); +} + +template +std::vector MultiPatch(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Patch(); +} + +template +std::vector MultiPost(Ts&&... ts) { + MultiPerform multiperform; + priv::setup_multiperform(multiperform, std::forward(ts)...); + return multiperform.Post(); +} + +template +std::vector> MultiGetAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Get>(ret, std::forward(ts)...); + return ret; +} + +template +std::vector> MultiDeleteAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Delete>(ret, std::forward(ts)...); + return ret; +} + +template +std::vector> MultiHeadAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Head>(ret, std::forward(ts)...); + return ret; +} +template +std::vector> MultiOptionsAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Options>(ret, std::forward(ts)...); + return ret; +} + +template +std::vector> MultiPatchAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Patch>(ret, std::forward(ts)...); + return ret; +} + +template +std::vector> MultiPostAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Post>(ret, std::forward(ts)...); + return ret; +} + +template +std::vector> MultiPutAsync(Ts&&... ts) { + std::vector> ret{}; + priv::setup_multiasync<&cpr::Session::Put>(ret, std::forward(ts)...); + return ret; +} + + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/async.h b/3rdparty/cpr/include/cpr/async.h new file mode 100644 index 0000000000000000000000000000000000000000..1305834fba5dbed70554b7d67657afde8914f2df --- /dev/null +++ b/3rdparty/cpr/include/cpr/async.h @@ -0,0 +1,50 @@ +#ifndef CPR_ASYNC_H +#define CPR_ASYNC_H + +#include "async_wrapper.h" +#include "singleton.h" +#include "threadpool.h" + +namespace cpr { + +class GlobalThreadPool : public ThreadPool { + CPR_SINGLETON_DECL(GlobalThreadPool) + protected: + GlobalThreadPool() = default; + + public: + ~GlobalThreadPool() override = default; +}; + +/** + * Return a wrapper for a future, calling future.get() will wait until the task is done and return RetType. + * async(fn, args...) + * async(std::bind(&Class::mem_fn, &obj)) + * async(std::mem_fn(&Class::mem_fn, &obj)) + **/ +template +auto async(Fn&& fn, Args&&... args) { + return AsyncWrapper{GlobalThreadPool::GetInstance()->Submit(std::forward(fn), std::forward(args)...)}; +} + +class async { + public: + static void startup(size_t min_threads = CPR_DEFAULT_THREAD_POOL_MIN_THREAD_NUM, size_t max_threads = CPR_DEFAULT_THREAD_POOL_MAX_THREAD_NUM, std::chrono::milliseconds max_idle_ms = CPR_DEFAULT_THREAD_POOL_MAX_IDLE_TIME) { + GlobalThreadPool* gtp = GlobalThreadPool::GetInstance(); + if (gtp->IsStarted()) { + return; + } + gtp->SetMinThreadNum(min_threads); + gtp->SetMaxThreadNum(max_threads); + gtp->SetMaxIdleTime(max_idle_ms); + gtp->Start(); + } + + static void cleanup() { + GlobalThreadPool::ExitInstance(); + } +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/async_wrapper.h b/3rdparty/cpr/include/cpr/async_wrapper.h new file mode 100644 index 0000000000000000000000000000000000000000..93558f86e72c0cc58eb33f5a19756bf031746b6b --- /dev/null +++ b/3rdparty/cpr/include/cpr/async_wrapper.h @@ -0,0 +1,140 @@ +#ifndef CPR_ASYNC_WRAPPER_H +#define CPR_ASYNC_WRAPPER_H + +#include +#include +#include + +#include "cpr/response.h" + +namespace cpr { +enum class [[nodiscard]] CancellationResult { failure, success, invalid_operation }; + +/** + * A class template intended to wrap results of async operations (instances of std::future) + * and also provide extended capablilities relaed to these requests, for example cancellation. + * + * The RAII semantics are the same as std::future - moveable, not copyable. + */ +template +class AsyncWrapper { + private: + std::future future; + std::shared_ptr is_cancelled; + + public: + // Constructors + explicit AsyncWrapper(std::future&& f) : future{std::move(f)} {} + AsyncWrapper(std::future&& f, std::shared_ptr&& cancelledState) : future{std::move(f)}, is_cancelled{std::move(cancelledState)} {} + + // Copy Semantics + AsyncWrapper(const AsyncWrapper&) = delete; + AsyncWrapper& operator=(const AsyncWrapper&) = delete; + + // Move Semantics + AsyncWrapper(AsyncWrapper&&) noexcept = default; + AsyncWrapper& operator=(AsyncWrapper&&) noexcept = default; + + // Destructor + ~AsyncWrapper() { + if constexpr (isCancellable) { + if (is_cancelled) { + is_cancelled->store(true); + } + } + } + // These methods replicate the behaviour of std::future + [[nodiscard]] T get() { + if constexpr (isCancellable) { + if (IsCancelled()) { + throw std::logic_error{"Calling AsyncWrapper::get on a cancelled request!"}; + } + } + if (!future.valid()) { + throw std::logic_error{"Calling AsyncWrapper::get when the associated future instance is invalid!"}; + } + return future.get(); + } + + [[nodiscard]] bool valid() const noexcept { + if constexpr (isCancellable) { + return !is_cancelled->load() && future.valid(); + } else { + return future.valid(); + } + } + + void wait() const { + if constexpr (isCancellable) { + if (is_cancelled->load()) { + throw std::logic_error{"Calling AsyncWrapper::wait when the associated future is invalid or cancelled!"}; + } + } + if (!future.valid()) { + throw std::logic_error{"Calling AsyncWrapper::wait_until when the associated future is invalid!"}; + } + future.wait(); + } + + template + std::future_status wait_for(const std::chrono::duration& timeout_duration) const { + if constexpr (isCancellable) { + if (IsCancelled()) { + throw std::logic_error{"Calling AsyncWrapper::wait_for when the associated future is cancelled!"}; + } + } + if (!future.valid()) { + throw std::logic_error{"Calling AsyncWrapper::wait_until when the associated future is invalid!"}; + } + return future.wait_for(timeout_duration); + } + + template + std::future_status wait_until(const std::chrono::time_point& timeout_time) const { + if constexpr (isCancellable) { + if (IsCancelled()) { + throw std::logic_error{"Calling AsyncWrapper::wait_until when the associated future is cancelled!"}; + } + } + if (!future.valid()) { + throw std::logic_error{"Calling AsyncWrapper::wait_until when the associated future is invalid!"}; + } + return future.wait_until(timeout_time); + } + + std::shared_future share() noexcept { + return future.share(); + } + + // Cancellation-related methods + CancellationResult Cancel() { + if constexpr (!isCancellable) { + return CancellationResult::invalid_operation; + } + if (!future.valid() || is_cancelled->load()) { + return CancellationResult::invalid_operation; + } + is_cancelled->store(true); + return CancellationResult::success; + } + + [[nodiscard]] bool IsCancelled() const { + if constexpr (isCancellable) { + return is_cancelled->load(); + } else { + return false; + } + } +}; + +// Deduction guides +template +AsyncWrapper(std::future&&) -> AsyncWrapper; + +template +AsyncWrapper(std::future&&, std::shared_ptr&&) -> AsyncWrapper; + +} // namespace cpr + + +#endif diff --git a/3rdparty/cpr/include/cpr/auth.h b/3rdparty/cpr/include/cpr/auth.h new file mode 100644 index 0000000000000000000000000000000000000000..effa2858a309fcc76b9b137543c16362a049f1a3 --- /dev/null +++ b/3rdparty/cpr/include/cpr/auth.h @@ -0,0 +1,29 @@ +#ifndef CPR_AUTH_H +#define CPR_AUTH_H + +#include +#include + +namespace cpr { + +enum class AuthMode { BASIC, DIGEST, NTLM, NEGOTIATE }; + +class Authentication { + public: + Authentication(std::string_view username, std::string_view password, AuthMode auth_mode); + Authentication(const Authentication& other) = default; + ~Authentication() noexcept; + + Authentication& operator=(const Authentication& other) = default; + + const char* GetAuthString() const noexcept; + AuthMode GetAuthMode() const noexcept; + + private: + std::string auth_string_; + AuthMode auth_mode_; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/bearer.h b/3rdparty/cpr/include/cpr/bearer.h new file mode 100644 index 0000000000000000000000000000000000000000..5e58a7d9345da4fbd439dcf8c6dd24aa30604806 --- /dev/null +++ b/3rdparty/cpr/include/cpr/bearer.h @@ -0,0 +1,34 @@ +#ifndef CPR_BEARER_H +#define CPR_BEARER_H + +#include +#include + +#include + +namespace cpr { + +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 +class Bearer { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Bearer(std::string token) : token_string_{std::move(token)} {} + Bearer(const Bearer& other) = default; + Bearer(Bearer&& old) noexcept = default; + virtual ~Bearer() noexcept; + + Bearer& operator=(Bearer&& old) noexcept = default; + Bearer& operator=(const Bearer& other) = default; + + virtual const char* GetToken() const noexcept; + + protected: + std::string token_string_; +}; +#endif + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/body.h b/3rdparty/cpr/include/cpr/body.h new file mode 100644 index 0000000000000000000000000000000000000000..f691d9c6200575cb7efa4b4dcc8cd881778ad055 --- /dev/null +++ b/3rdparty/cpr/include/cpr/body.h @@ -0,0 +1,54 @@ +#ifndef CPR_BODY_H +#define CPR_BODY_H + +#include +#include +#include +#include +#include + +#include "cpr/buffer.h" +#include "cpr/cprtypes.h" +#include "cpr/file.h" + +namespace cpr { + +class Body : public StringHolder { + public: + Body() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Body(std::string body) : StringHolder(std::move(body)) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Body(std::string_view body) : StringHolder(body) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Body(const char* body) : StringHolder(body) {} + Body(const char* str, size_t len) : StringHolder(str, len) {} + Body(const std::initializer_list args) : StringHolder(args) {} + // NOLINTNEXTLINE(google-explicit-constructor, cppcoreguidelines-pro-type-reinterpret-cast) + Body(const Buffer& buffer) : StringHolder(reinterpret_cast(buffer.data), static_cast(buffer.datalen)) {} + // NOLINTNEXTLINE(google-explicit-constructor) + Body(const File& file) { + std::ifstream is(file.filepath, std::ifstream::binary); + if (!is) { + throw std::invalid_argument("Can't open the file for HTTP request body!"); + } + + is.seekg(0, std::ios::end); + const std::streampos length = is.tellg(); + is.seekg(0, std::ios::beg); + std::string buffer; + buffer.resize(static_cast(length)); + is.read(buffer.data(), length); + str_ = std::move(buffer); + } + Body(const Body& other) = default; + Body(Body&& old) noexcept = default; + ~Body() override = default; + + Body& operator=(Body&& old) noexcept = default; + Body& operator=(const Body& other) = default; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/buffer.h b/3rdparty/cpr/include/cpr/buffer.h new file mode 100644 index 0000000000000000000000000000000000000000..7b4ccfcf0fb2240ae872f2cb102e953946a90d31 --- /dev/null +++ b/3rdparty/cpr/include/cpr/buffer.h @@ -0,0 +1,33 @@ +#ifndef CPR_BUFFER_H +#define CPR_BUFFER_H + +#include + +#include "cpr/filesystem.h" + +namespace cpr { + +struct Buffer { + using data_t = const char*; + + template + Buffer(Iterator begin, Iterator end, fs::path&& p_filename) + // Ignored here since libcurl reqires a long. + // There is also no way around the reinterpret_cast. + // NOLINTNEXTLINE(google-runtime-int, cppcoreguidelines-pro-type-reinterpret-cast) + : data{reinterpret_cast(&(*begin))}, datalen{static_cast(std::distance(begin, end))}, filename(std::move(p_filename)) { + is_random_access_iterator(begin, end); + static_assert(sizeof(*begin) == 1, "Only byte buffers can be used"); + } + + template + typename std::enable_if::iterator_category, std::random_access_iterator_tag>::value>::type is_random_access_iterator(Iterator /* begin */, Iterator /* end */) {} + + data_t data; + size_t datalen; + const fs::path filename; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/callback.h b/3rdparty/cpr/include/cpr/callback.h new file mode 100644 index 0000000000000000000000000000000000000000..b20ce2ad0aee7ec782230dd935ba31e398b1d016 --- /dev/null +++ b/3rdparty/cpr/include/cpr/callback.h @@ -0,0 +1,112 @@ +#ifndef CPR_CALLBACK_H +#define CPR_CALLBACK_H + +#include "cprtypes.h" + +#include +#include +#include +#include +#include + +namespace cpr { + +class ReadCallback { + public: + ReadCallback() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + ReadCallback(std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), size{-1}, callback{std::move(p_callback)} {} + ReadCallback(cpr_off_t p_size, std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), size{p_size}, callback{std::move(p_callback)} {} + bool operator()(char* buffer, size_t& buffer_size) const { + return callback(buffer, buffer_size, userdata); + } + + intptr_t userdata{}; + cpr_off_t size{}; + std::function callback; +}; + +class HeaderCallback { + public: + HeaderCallback() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + HeaderCallback(std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), callback(std::move(p_callback)) {} + bool operator()(const std::string_view& header) const { + return callback(header, userdata); + } + + intptr_t userdata{}; + std::function callback; +}; + +class WriteCallback { + public: + WriteCallback() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + WriteCallback(std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), callback(std::move(p_callback)) {} + bool operator()(const std::string_view& data) const { + return callback(data, userdata); + } + + intptr_t userdata{}; + std::function callback; +}; + +class ProgressCallback { + public: + ProgressCallback() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + ProgressCallback(std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), callback(std::move(p_callback)) {} + bool operator()(cpr_pf_arg_t downloadTotal, cpr_pf_arg_t downloadNow, cpr_pf_arg_t uploadTotal, cpr_pf_arg_t uploadNow) const { + return callback(downloadTotal, downloadNow, uploadTotal, uploadNow, userdata); + } + + intptr_t userdata{}; + std::function callback; +}; + +class DebugCallback { + public: + enum class InfoType { + TEXT = 0, + HEADER_IN = 1, + HEADER_OUT = 2, + DATA_IN = 3, + DATA_OUT = 4, + SSL_DATA_IN = 5, + SSL_DATA_OUT = 6, + }; + DebugCallback() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + DebugCallback(std::function p_callback, intptr_t p_userdata = 0) : userdata(p_userdata), callback(std::move(p_callback)) {} + void operator()(InfoType type, std::string data) const { + callback(type, std::move(data), userdata); + } + + intptr_t userdata{}; + std::function callback; +}; + +/** + * Functor class for progress functions that will be used in cancellable requests. + */ +class CancellationCallback { + public: + CancellationCallback() = default; + explicit CancellationCallback(std::shared_ptr&& cs) : cancellation_state{std::move(cs)} {} + + CancellationCallback(std::shared_ptr&& cs, ProgressCallback& u_cb) : cancellation_state{std::move(cs)}, user_cb{std::reference_wrapper{u_cb}} {} + + bool operator()(cpr_pf_arg_t dltotal, cpr_pf_arg_t dlnow, cpr_pf_arg_t ultotal, cpr_pf_arg_t ulnow) const; + + void SetProgressCallback(ProgressCallback& u_cb); + + private: + std::shared_ptr cancellation_state; + std::optional> user_cb; +}; + + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/cert_info.h b/3rdparty/cpr/include/cpr/cert_info.h new file mode 100644 index 0000000000000000000000000000000000000000..4d957b026530687ca0e197eb98a8b0ea75839ec7 --- /dev/null +++ b/3rdparty/cpr/include/cpr/cert_info.h @@ -0,0 +1,37 @@ +#ifndef CPR_CERT_INFO_H +#define CPR_CERT_INFO_H + +#include +#include +#include + +namespace cpr { + +class CertInfo { + private: + std::vector cert_info_; + + public: + CertInfo() = default; + CertInfo(const CertInfo& other) = default; + CertInfo(CertInfo&& old) = default; + CertInfo(const std::initializer_list& entry) : cert_info_{entry} {} + ~CertInfo() noexcept = default; + + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; + + std::string& operator[](const size_t& pos); + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + const_iterator cbegin() const; + const_iterator cend() const; + void emplace_back(const std::string& str); + void push_back(const std::string& str); + void pop_back(); +}; +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/connect_timeout.h b/3rdparty/cpr/include/cpr/connect_timeout.h new file mode 100644 index 0000000000000000000000000000000000000000..546e8a585c327967a9921bed13f1a7e9f2ec37ee --- /dev/null +++ b/3rdparty/cpr/include/cpr/connect_timeout.h @@ -0,0 +1,18 @@ +#ifndef CPR_CONNECT_TIMEOUT_H +#define CPR_CONNECT_TIMEOUT_H + +#include "cpr/timeout.h" + +namespace cpr { + +class ConnectTimeout : public Timeout { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + ConnectTimeout(const std::chrono::milliseconds& duration) : Timeout{duration} {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + ConnectTimeout(const std::int32_t& milliseconds) : Timeout{milliseconds} {} +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/cookies.h b/3rdparty/cpr/include/cpr/cookies.h new file mode 100644 index 0000000000000000000000000000000000000000..60f421bc4be4373c6e9b48fdf60e191c082d9bca --- /dev/null +++ b/3rdparty/cpr/include/cpr/cookies.h @@ -0,0 +1,93 @@ +#ifndef CPR_COOKIES_H +#define CPR_COOKIES_H + +#include "cpr/curlholder.h" +#include +#include +#include +#include +#include + +namespace cpr { +/** + * EXPIRES_STRING_SIZE is an explicitly static and const variable that could be only accessed within the same namespace and is immutable. + * To be used for "std::array", the expression must have a constant value, so EXPIRES_STRING_SIZE must be a const value. + **/ +static const std::size_t EXPIRES_STRING_SIZE = 100; + +class Cookie { + public: + Cookie() = default; + /** + * Some notes for the default value used by expires: + * std::chrono::system_clock::time_point::min() won't work on Windows due to the min, max clash there. + * So we fall back to std::chrono::system_clock::from_time_t(0) for the minimum value here. + **/ + Cookie(const std::string& name, const std::string& value, const std::string& domain = "", bool p_isIncludingSubdomains = false, const std::string& path = "/", bool p_isHttpsOnly = false, std::chrono::system_clock::time_point expires = std::chrono::system_clock::from_time_t(0)) : name_{name}, value_{value}, domain_{domain}, includeSubdomains_{p_isIncludingSubdomains}, path_{path}, httpsOnly_{p_isHttpsOnly}, expires_{expires} {} + const std::string GetDomain() const; + bool IsIncludingSubdomains() const; + const std::string GetPath() const; + bool IsHttpsOnly() const; + const std::chrono::system_clock::time_point GetExpires() const; + const std::string GetExpiresString() const; + const std::string GetName() const; + const std::string GetValue() const; + + private: + std::string name_; + std::string value_; + std::string domain_; + bool includeSubdomains_{}; + std::string path_; + bool httpsOnly_{}; + /** + * TODO: Update the implementation using `std::chrono::utc_clock` of C++20 + **/ + std::chrono::system_clock::time_point expires_{}; +}; + +class Cookies { + public: + /** + * Should we URL-encode cookies when making a request. + * Based on RFC6265, it is recommended but not mandatory to encode cookies. + * + * ------- + * To maximize compatibility with user agents, servers that wish to + * store arbitrary data in a cookie-value SHOULD encode that data, for + * example, using Base64 [RFC4648]. + * ------- + * Source: RFC6265 (https://www.ietf.org/rfc/rfc6265.txt) + **/ + bool encode{true}; + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Cookies(bool p_encode = true) : encode{p_encode} {} + Cookies(const std::initializer_list& cookies, bool p_encode = true) : encode{p_encode}, cookies_{cookies} {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Cookies(const cpr::Cookie& cookie, bool p_encode = true) : encode{p_encode}, cookies_{cookie} {} + + cpr::Cookie& operator[](size_t pos); + const std::string GetEncoded(const CurlHolder& holder) const; + + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; + + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + const_iterator cbegin() const; + const_iterator cend() const; + void emplace_back(const Cookie& str); + [[nodiscard]] bool empty() const; + void push_back(const Cookie& str); + void pop_back(); + + private: + std::vector cookies_; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/cpr.h b/3rdparty/cpr/include/cpr/cpr.h new file mode 100644 index 0000000000000000000000000000000000000000..fbad1726a65e0df949012c00b0b40b6ee8894131 --- /dev/null +++ b/3rdparty/cpr/include/cpr/cpr.h @@ -0,0 +1,46 @@ +#ifndef CPR_CPR_H +#define CPR_CPR_H + +#include "cpr/api.h" +#include "cpr/auth.h" +#include "cpr/bearer.h" +#include "cpr/callback.h" +#include "cpr/cert_info.h" +#include "cpr/connect_timeout.h" +#include "cpr/cookies.h" +#include "cpr/cprtypes.h" +#include "cpr/cprver.h" +#include "cpr/curl_container.h" +#include "cpr/curlholder.h" +#include "cpr/error.h" +#include "cpr/http_version.h" +#include "cpr/interceptor.h" +#include "cpr/interface.h" +#include "cpr/limit_rate.h" +#include "cpr/local_port.h" +#include "cpr/local_port_range.h" +#include "cpr/low_speed.h" +#include "cpr/multipart.h" +#include "cpr/multiperform.h" +#include "cpr/parameters.h" +#include "cpr/payload.h" +#include "cpr/proxies.h" +#include "cpr/proxyauth.h" +#include "cpr/range.h" +#include "cpr/redirect.h" +#include "cpr/reserve_size.h" +#include "cpr/resolve.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include "cpr/ssl_ctx.h" +#include "cpr/ssl_options.h" +#include "cpr/status_codes.h" +#include "cpr/timeout.h" +#include "cpr/unix_socket.h" +#include "cpr/user_agent.h" +#include "cpr/util.h" +#include "cpr/verbose.h" + +#define CPR_LIBCURL_VERSION_NUM LIBCURL_VERSION_NUM + +#endif diff --git a/3rdparty/cpr/include/cpr/cprtypes.h b/3rdparty/cpr/include/cpr/cprtypes.h new file mode 100644 index 0000000000000000000000000000000000000000..6692ffd7b5ca318c9e15dab9111fd0f0acb1793d --- /dev/null +++ b/3rdparty/cpr/include/cpr/cprtypes.h @@ -0,0 +1,144 @@ +#ifndef CPR_CPR_TYPES_H +#define CPR_CPR_TYPES_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cpr { + +/** + * Wrapper around "curl_off_t" to prevent applications from having to link against libcurl. + **/ +using cpr_off_t = curl_off_t; + +/** + * The argument type for progress functions, dependent on libcurl version + **/ +#if LIBCURL_VERSION_NUM < 0x072000 +using cpr_pf_arg_t = double; +#else +using cpr_pf_arg_t = cpr_off_t; +#endif + +template +class StringHolder { + public: + StringHolder() = default; + explicit StringHolder(std::string str) : str_(std::move(str)) {} + explicit StringHolder(std::string_view str) : str_(str) {} + explicit StringHolder(const char* str) : str_(str) {} + StringHolder(const char* str, size_t len) : str_(str, len) {} + StringHolder(const std::initializer_list args) { + str_ = std::accumulate(args.begin(), args.end(), str_); + } + StringHolder(const StringHolder& other) = default; + StringHolder(StringHolder&& old) noexcept = default; + virtual ~StringHolder() = default; + + StringHolder& operator=(StringHolder&& old) noexcept = default; + + StringHolder& operator=(const StringHolder& other) = default; + + explicit operator std::string() const { + return str_; + } + + T operator+(const char* rhs) const { + return T(str_ + rhs); + } + + T operator+(const std::string& rhs) const { + return T(str_ + rhs); + } + + T operator+(const StringHolder& rhs) const { + return T(str_ + rhs.str_); + } + + void operator+=(const char* rhs) { + str_ += rhs; + } + void operator+=(const std::string& rhs) { + str_ += rhs; + } + void operator+=(const StringHolder& rhs) { + str_ += rhs; + } + + bool operator==(const char* rhs) const { + return str_ == rhs; + } + bool operator==(const std::string& rhs) const { + return str_ == rhs; + } + bool operator==(const StringHolder& rhs) const { + return str_ == rhs.str_; + } + + bool operator!=(const char* rhs) const { + return str_.c_str() != rhs; + } + bool operator!=(const std::string& rhs) const { + return str_ != rhs; + } + bool operator!=(const StringHolder& rhs) const { + return str_ != rhs.str_; + } + + const std::string& str() { + return str_; + } + [[nodiscard]] const std::string& str() const { + return str_; + } + [[nodiscard]] const char* c_str() const { + return str_.c_str(); + } + [[nodiscard]] const char* data() const { + return str_.data(); + } + + protected: + std::string str_{}; +}; + +template +std::ostream& operator<<(std::ostream& os, const StringHolder& s) { + os << s.str(); + return os; +} + +class Url : public StringHolder { + public: + Url() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Url(std::string url) : StringHolder(std::move(url)) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Url(std::string_view url) : StringHolder(url) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Url(const char* url) : StringHolder(url) {} + Url(const char* str, size_t len) : StringHolder(std::string(str, len)) {} + Url(const std::initializer_list args) : StringHolder(args) {} + Url(const Url& other) = default; + Url(Url&& old) noexcept = default; + ~Url() override = default; + + Url& operator=(Url&& old) noexcept = default; + Url& operator=(const Url& other) = default; +}; + +struct CaseInsensitiveCompare { + bool operator()(const std::string& a, const std::string& b) const noexcept; +}; + +using Header = std::map; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/curl_container.h b/3rdparty/cpr/include/cpr/curl_container.h new file mode 100644 index 0000000000000000000000000000000000000000..c2409b226c7188c8c8e3d242d3828a2a83b3d69e --- /dev/null +++ b/3rdparty/cpr/include/cpr/curl_container.h @@ -0,0 +1,51 @@ +#ifndef CURL_CONTAINER_H +#define CURL_CONTAINER_H + +#include +#include +#include +#include + +#include "cpr/curlholder.h" + + +namespace cpr { + +struct Parameter { + Parameter(std::string p_key, std::string p_value) : key{std::move(p_key)}, value{std::move(p_value)} {} + + std::string key; + std::string value; +}; + +struct Pair { + Pair(std::string p_key, std::string p_value) : key(std::move(p_key)), value(std::move(p_value)) {} + + std::string key; + std::string value; +}; + + +template +class CurlContainer { + public: + /** + * Enables or disables URL encoding for keys and values when calling GetContent(...). + **/ + bool encode = true; + + CurlContainer() = default; + CurlContainer(const std::initializer_list&); + + void Add(const std::initializer_list&); + void Add(const T&); + + const std::string GetContent(const CurlHolder&) const; + + protected: + std::vector containerList_; +}; + +} // namespace cpr + +#endif // diff --git a/3rdparty/cpr/include/cpr/curlholder.h b/3rdparty/cpr/include/cpr/curlholder.h new file mode 100644 index 0000000000000000000000000000000000000000..cfd0b7137d7a48cd0a6da364d3ee04092510ac45 --- /dev/null +++ b/3rdparty/cpr/include/cpr/curlholder.h @@ -0,0 +1,53 @@ +#ifndef CPR_CURL_HOLDER_H +#define CPR_CURL_HOLDER_H + +#include +#include +#include +#include + +namespace cpr { +struct CurlHolder { + private: + /** + * Mutex for curl_easy_init(). + * curl_easy_init() is not thread save. + * References: + * https://curl.haxx.se/libcurl/c/curl_easy_init.html + * https://curl.haxx.se/libcurl/c/threadsafe.html + **/ + + // Avoids initalization order problems in a static build + static std::mutex& curl_easy_init_mutex_() { + static std::mutex curl_easy_init_mutex_; + return curl_easy_init_mutex_; + } + + public: + CURL* handle{nullptr}; + struct curl_slist* chunk{nullptr}; + struct curl_slist* resolveCurlList{nullptr}; + curl_mime* multipart{nullptr}; + std::array error{}; + + CurlHolder(); + CurlHolder(const CurlHolder& other) = default; + CurlHolder(CurlHolder&& old) noexcept = default; + ~CurlHolder(); + + CurlHolder& operator=(CurlHolder&& old) noexcept = default; + CurlHolder& operator=(const CurlHolder& other) = default; + + /** + * Uses curl_easy_escape(...) for escaping the given string. + **/ + [[nodiscard]] std::string urlEncode(const std::string& s) const; + + /** + * Uses curl_easy_unescape(...) for unescaping the given string. + **/ + [[nodiscard]] std::string urlDecode(const std::string& s) const; +}; +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/curlmultiholder.h b/3rdparty/cpr/include/cpr/curlmultiholder.h new file mode 100644 index 0000000000000000000000000000000000000000..401df0eb65b879b1f0b464ef702f837594e7f22d --- /dev/null +++ b/3rdparty/cpr/include/cpr/curlmultiholder.h @@ -0,0 +1,18 @@ +#ifndef CPR_CURLMULTIHOLDER_H +#define CPR_CURLMULTIHOLDER_H + +#include + +namespace cpr { + +class CurlMultiHolder { + public: + CurlMultiHolder(); + ~CurlMultiHolder(); + + CURLM* handle{nullptr}; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/error.h b/3rdparty/cpr/include/cpr/error.h new file mode 100644 index 0000000000000000000000000000000000000000..faacf6b3c64308bd552b72227a36a85063ef2e1d --- /dev/null +++ b/3rdparty/cpr/include/cpr/error.h @@ -0,0 +1,111 @@ +#ifndef CPR_ERROR_H +#define CPR_ERROR_H + +#include +#include + +#include "cpr/cprtypes.h" +#include + +namespace cpr { + +/** + * cpr error codes that match the ones found inside 'curl.h'. + * These error codes only include relevant error codes meaning no support for e.g. FTP errors since cpr does only support HTTP. + **/ +enum class ErrorCode { + /** + * Everything is good and no error occurred. + **/ + OK = 0, + UNSUPPORTED_PROTOCOL, + FAILED_INIT, + URL_MALFORMAT, + NOT_BUILT_IN, + COULDNT_RESOLVE_PROXY, + COULDNT_RESOLVE_HOST, + COULDNT_CONNECT, + WEIRD_SERVER_REPLY, + REMOTE_ACCESS_DENIED, + HTTP2, + PARTIAL_FILE, + QUOTE_ERROR, + HTTP_RETURNED_ERROR, + WRITE_ERROR, + UPLOAD_FAILED, + READ_ERROR, + OUT_OF_MEMORY, + OPERATION_TIMEDOUT, + RANGE_ERROR, + HTTP_POST_ERROR, + SSL_CONNECT_ERROR, + BAD_DOWNLOAD_RESUME, + FILE_COULDNT_READ_FILE, + FUNCTION_NOT_FOUND, + ABORTED_BY_CALLBACK, + BAD_FUNCTION_ARGUMENT, + INTERFACE_FAILED, + TOO_MANY_REDIRECTS, + UNKNOWN_OPTION, + SETOPT_OPTION_SYNTAX, + GOT_NOTHING, + SSL_ENGINE_NOTFOUND, + SSL_ENGINE_SETFAILED, + SEND_ERROR, + RECV_ERROR, + SSL_CERTPROBLEM, + SSL_CIPHER, + PEER_FAILED_VERIFICATION, + BAD_CONTENT_ENCODING, + FILESIZE_EXCEEDED, + USE_SSL_FAILED, + SEND_FAIL_REWIND, + SSL_ENGINE_INITFAILED, + LOGIN_DENIED, + SSL_CACERT_BADFILE, + SSL_SHUTDOWN_FAILED, + AGAIN, + SSL_CRL_BADFILE, + SSL_ISSUER_ERROR, + CHUNK_FAILED, + NO_CONNECTION_AVAILABLE, + SSL_PINNEDPUBKEYNOTMATCH, + SSL_INVALIDCERTSTATUS, + HTTP2_STREAM, + RECURSIVE_API_CALL, + AUTH_ERROR, + HTTP3, + QUIC_CONNECT_ERROR, + PROXY, + SSL_CLIENTCERT, + UNRECOVERABLE_POLL, + TOO_LARGE, + /** + * An unknown error inside curl occurred. + * Please try to reproduce it and then report it to us. + * It might be that there is a new curl error code we are not aware yet. + * Reporting bugs: https://github.com/libcpr/cpr + **/ + UNKNOWN_ERROR = 1000, +}; + +class Error { + public: + ErrorCode code = ErrorCode::OK; + std::string message; + + Error() = default; + + Error(const std::int32_t& curl_code, std::string&& p_error_message) : code{getErrorCodeForCurlError(curl_code)}, message(std::move(p_error_message)) {} + + explicit operator bool() const { + return code != ErrorCode::OK; + } + + private: + static ErrorCode getErrorCodeForCurlError(std::int32_t curl_code); +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/file.h b/3rdparty/cpr/include/cpr/file.h new file mode 100644 index 0000000000000000000000000000000000000000..9f1f18cf31ef7f0a30b7701414e994bd36733f6c --- /dev/null +++ b/3rdparty/cpr/include/cpr/file.h @@ -0,0 +1,59 @@ +#ifndef CPR_FILE_H +#define CPR_FILE_H + +#include +#include +#include + +#include "cpr/filesystem.h" + +namespace cpr { + +struct File { + explicit File(std::string p_filepath, const std::string& p_overriden_filename = {}) : filepath(std::move(p_filepath)), overriden_filename(p_overriden_filename) {} + + std::string filepath; + std::string overriden_filename; + + [[nodiscard]] bool hasOverridenFilename() const noexcept { + return !overriden_filename.empty(); + } +}; + +class Files { + public: + Files() = default; + // NOLINTNEXTLINE(google-explicit-constructor) + Files(const File& p_file) : files{p_file} {} + + Files(const Files& other) = default; + Files(Files&& old) noexcept = default; + + Files(const std::initializer_list& p_files) : files{p_files} {} + Files(const std::initializer_list& p_filepaths); + + ~Files() noexcept = default; + + Files& operator=(const Files& other); + Files& operator=(Files&& old) noexcept; + + using iterator = std::vector::iterator; + using const_iterator = std::vector::const_iterator; + + iterator begin(); + iterator end(); + [[nodiscard]] const_iterator begin() const; + [[nodiscard]] const_iterator end() const; + [[nodiscard]] const_iterator cbegin() const; + [[nodiscard]] const_iterator cend() const; + void emplace_back(const File& file); + void push_back(const File& file); + void pop_back(); + + private: + std::vector files; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/filesystem.h b/3rdparty/cpr/include/cpr/filesystem.h new file mode 100644 index 0000000000000000000000000000000000000000..f296770cf1fe73568acac98b04e769f4afaf0c8f --- /dev/null +++ b/3rdparty/cpr/include/cpr/filesystem.h @@ -0,0 +1,26 @@ +#ifndef CPR_FILESYSTEM_H +#define CPR_FILESYSTEM_H + +// Include filesystem into the namespace "fs" from either "filesystem" or "experimental/filesystem" or "boost/filesystem" +#ifdef CPR_USE_BOOST_FILESYSTEM +#define BOOST_FILESYSTEM_VERSION 4 // Use the latest, with the closest behavior to std::filesystem. +#include +namespace cpr { +namespace fs = boost::filesystem; +} +// cppcheck-suppress preprocessorErrorDirective +#elif __has_include() +#include +namespace cpr { +namespace fs = std::filesystem; +} +#elif __has_include("experimental/filesystem") +#include +namespace cpr { +namespace fs = std::experimental::filesystem; +} +#else +#error "Failed to include header!" +#endif + +#endif diff --git a/3rdparty/cpr/include/cpr/http_version.h b/3rdparty/cpr/include/cpr/http_version.h new file mode 100644 index 0000000000000000000000000000000000000000..45b5028734dac9c94ebc37d3db13aab2f142fa8f --- /dev/null +++ b/3rdparty/cpr/include/cpr/http_version.h @@ -0,0 +1,67 @@ +#ifndef CPR_HTTP_VERSION_H +#define CPR_HTTP_VERSION_H + +#include + +namespace cpr { +enum class HttpVersionCode { + /** + * Let libcurl decide which version is the best. + **/ + VERSION_NONE, + /** + * Enforce HTTP 1.0 requests. + **/ + VERSION_1_0, + /** + * Enforce HTTP 1.1 requests. + **/ + VERSION_1_1, +#if LIBCURL_VERSION_NUM >= 0x072100 // 7.33.0 + /** + * Attempt HTTP 2.0 requests. + * Fallback to HTTP 1.1 if negotiation fails. + **/ + VERSION_2_0, +#endif +#if LIBCURL_VERSION_NUM >= 0x072F00 // 7.47.0 + /** + * Attempt HTTP 2.0 for HTTPS requests only. + * Fallback to HTTP 1.1 if negotiation fails. + * HTTP 1.1 will be used for HTTP connections. + **/ + VERSION_2_0_TLS, +#endif +#if LIBCURL_VERSION_NUM >= 0x073100 // 7.49.0 + /** + * Start HTTP 2.0 for HTTP requests. + * Requires prior knowledge that the server supports HTTP 2.0. + * For HTTPS requests we will negotiate the protocol version in the TLS handshake. + **/ + VERSION_2_0_PRIOR_KNOWLEDGE, +#endif +#if LIBCURL_VERSION_NUM >= 0x074200 // 7.66.0 + /** + * Attempt HTTP 3.0 requests. + * Requires prior knowledge that the server supports HTTP 3.0 since there is no gracefully downgrade. + * Fallback to HTTP 1.1 if negotiation fails. + **/ + VERSION_3_0 +#endif +}; + +class HttpVersion { + public: + /** + * The HTTP version that should be used by libcurl when initiating a HTTP(S) connection. + * Default: HttpVersionCode::VERSION_NONE + **/ + HttpVersionCode code = HttpVersionCode::VERSION_NONE; + + HttpVersion() = default; + explicit HttpVersion(HttpVersionCode _code) : code(_code) {} +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/interceptor.h b/3rdparty/cpr/include/cpr/interceptor.h new file mode 100644 index 0000000000000000000000000000000000000000..d05f4c9c231cbd1afdecc402bb7a1fafaba9aa45 --- /dev/null +++ b/3rdparty/cpr/include/cpr/interceptor.h @@ -0,0 +1,74 @@ +#ifndef CPR_INTERCEPTOR_H +#define CPR_INTERCEPTOR_H + +#include "cpr/multiperform.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include + +namespace cpr { +class Interceptor { + public: + enum class ProceedHttpMethod { + GET_REQUEST = 0, + POST_REQUEST, + PUT_REQUEST, + DELETE_REQUEST, + PATCH_REQUEST, + HEAD_REQUEST, + OPTIONS_REQUEST, + DOWNLOAD_CALLBACK_REQUEST, + DOWNLOAD_FILE_REQUEST, + }; + + Interceptor() = default; + Interceptor(const Interceptor& other) = default; + Interceptor(Interceptor&& old) = default; + virtual ~Interceptor() = default; + + Interceptor& operator=(const Interceptor& other) = default; + Interceptor& operator=(Interceptor&& old) = default; + + virtual Response intercept(Session& session) = 0; + + protected: + static Response proceed(Session& session); + static Response proceed(Session& session, ProceedHttpMethod httpMethod); + static Response proceed(Session& session, ProceedHttpMethod httpMethod, std::ofstream& file); + static Response proceed(Session& session, ProceedHttpMethod httpMethod, const WriteCallback& write); +}; + +class InterceptorMulti { + public: + enum class ProceedHttpMethod { + GET_REQUEST = 0, + POST_REQUEST, + PUT_REQUEST, + DELETE_REQUEST, + PATCH_REQUEST, + HEAD_REQUEST, + OPTIONS_REQUEST, + DOWNLOAD_CALLBACK_REQUEST, + DOWNLOAD_FILE_REQUEST, + }; + + InterceptorMulti() = default; + InterceptorMulti(const InterceptorMulti& other) = default; + InterceptorMulti(InterceptorMulti&& old) = default; + virtual ~InterceptorMulti() = default; + + InterceptorMulti& operator=(const InterceptorMulti& other) = default; + InterceptorMulti& operator=(InterceptorMulti&& old) = default; + + virtual std::vector intercept(MultiPerform& multi) = 0; + + protected: + static std::vector proceed(MultiPerform& multi); + + static void PrepareDownloadSession(MultiPerform& multi, size_t sessions_index, const WriteCallback& write); +}; + +} // namespace cpr + + +#endif diff --git a/3rdparty/cpr/include/cpr/interface.h b/3rdparty/cpr/include/cpr/interface.h new file mode 100644 index 0000000000000000000000000000000000000000..b98940ec22081d1ea74eff6928b94939a843623c --- /dev/null +++ b/3rdparty/cpr/include/cpr/interface.h @@ -0,0 +1,32 @@ +#ifndef CPR_INTERFACE_H +#define CPR_INTERFACE_H + +#include +#include + +#include "cpr/cprtypes.h" + +namespace cpr { + +class Interface : public StringHolder { + public: + Interface() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Interface(std::string iface) : StringHolder(std::move(iface)) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Interface(std::string_view iface) : StringHolder(iface) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Interface(const char* iface) : StringHolder(iface) {} + Interface(const char* str, size_t len) : StringHolder(str, len) {} + Interface(const std::initializer_list args) : StringHolder(args) {} + Interface(const Interface& other) = default; + Interface(Interface&& old) noexcept = default; + ~Interface() override = default; + + Interface& operator=(Interface&& old) noexcept = default; + Interface& operator=(const Interface& other) = default; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/limit_rate.h b/3rdparty/cpr/include/cpr/limit_rate.h new file mode 100644 index 0000000000000000000000000000000000000000..f25c09e5bdc60ebd6122854b6c97af967658732d --- /dev/null +++ b/3rdparty/cpr/include/cpr/limit_rate.h @@ -0,0 +1,18 @@ +#ifndef CPR_SPEED_LIMIT_H +#define CPR_SPEED_LIMIT_H + +#include + +namespace cpr { + +class LimitRate { + public: + LimitRate(const std::int64_t p_downrate, const std::int64_t p_uprate) : downrate(p_downrate), uprate(p_uprate) {} + + std::int64_t downrate = 0; + std::int64_t uprate = 0; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/local_port.h b/3rdparty/cpr/include/cpr/local_port.h new file mode 100644 index 0000000000000000000000000000000000000000..a6efe7ecfd01c74e6db02a6428b3fa71332cd9c8 --- /dev/null +++ b/3rdparty/cpr/include/cpr/local_port.h @@ -0,0 +1,23 @@ +#ifndef CPR_LOCAL_PORT_H +#define CPR_LOCAL_PORT_H + +#include + +namespace cpr { + +class LocalPort { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + LocalPort(const std::uint16_t p_localport) : localport_(p_localport) {} + + operator std::uint16_t() const { + return localport_; + } + + private: + std::uint16_t localport_ = 0; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/local_port_range.h b/3rdparty/cpr/include/cpr/local_port_range.h new file mode 100644 index 0000000000000000000000000000000000000000..e048b6e97572a82a890a2b119ea4fadb4a5ea54f --- /dev/null +++ b/3rdparty/cpr/include/cpr/local_port_range.h @@ -0,0 +1,23 @@ +#ifndef CPR_LOCAL_PORT_RANGE_H +#define CPR_LOCAL_PORT_RANGE_H + +#include + +namespace cpr { + +class LocalPortRange { + public: + // NOLINTNEXTLINE(google-explicit-constructor) + LocalPortRange(const std::uint16_t p_localportrange) : localportrange_(p_localportrange) {} + + operator std::uint16_t() const { + return localportrange_; + } + + private: + std::uint16_t localportrange_ = 0; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/low_speed.h b/3rdparty/cpr/include/cpr/low_speed.h new file mode 100644 index 0000000000000000000000000000000000000000..ff77fd2eda29044fc69d22eae5dfe3d7e6200627 --- /dev/null +++ b/3rdparty/cpr/include/cpr/low_speed.h @@ -0,0 +1,18 @@ +#ifndef CPR_LOW_SPEED_H +#define CPR_LOW_SPEED_H + +#include + +namespace cpr { + +class LowSpeed { + public: + LowSpeed(const std::int32_t p_limit, const std::int32_t p_time) : limit(p_limit), time(p_time) {} + + std::int32_t limit; + std::int32_t time; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/multipart.h b/3rdparty/cpr/include/cpr/multipart.h new file mode 100644 index 0000000000000000000000000000000000000000..9ee62d5ab056d9f61f91c93e9ccd0e37dbc065a7 --- /dev/null +++ b/3rdparty/cpr/include/cpr/multipart.h @@ -0,0 +1,45 @@ +#ifndef CPR_MULTIPART_H +#define CPR_MULTIPART_H + +#include +#include +#include +#include +#include + +#include "buffer.h" +#include "file.h" + +namespace cpr { + +struct Part { + Part(const std::string& p_name, const std::string& p_value, const std::string& p_content_type = {}) : name{p_name}, value{p_value}, content_type{p_content_type}, is_file{false}, is_buffer{false} {} + Part(const std::string& p_name, const std::int32_t& p_value, const std::string& p_content_type = {}) : name{p_name}, value{std::to_string(p_value)}, content_type{p_content_type}, is_file{false}, is_buffer{false} {} + Part(const std::string& p_name, const Files& p_files, const std::string& p_content_type = {}) : name{p_name}, content_type{p_content_type}, is_file{true}, is_buffer{false}, files{p_files} {} + Part(const std::string& p_name, Files&& p_files, const std::string& p_content_type = {}) : name{p_name}, content_type{p_content_type}, is_file{true}, is_buffer{false}, files{p_files} {} + Part(const std::string& p_name, const Buffer& buffer, const std::string& p_content_type = {}) : name{p_name}, value{buffer.filename.string()}, content_type{p_content_type}, data{buffer.data}, datalen{buffer.datalen}, is_file{false}, is_buffer{true} {} + + std::string name; + // We don't use fs::path here, as this leads to problems using windows + std::string value; + std::string content_type; + Buffer::data_t data{nullptr}; + size_t datalen{0}; + bool is_file; + bool is_buffer; + + Files files; +}; + +class Multipart { + public: + Multipart(const std::initializer_list& parts); + Multipart(const std::vector& parts); + Multipart(const std::vector&& parts); + + std::vector parts; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/multiperform.h b/3rdparty/cpr/include/cpr/multiperform.h new file mode 100644 index 0000000000000000000000000000000000000000..d4ac445ee2bb8f5281aab2658483eca974d26504 --- /dev/null +++ b/3rdparty/cpr/include/cpr/multiperform.h @@ -0,0 +1,142 @@ +#ifndef CPR_MULTIPERFORM_H +#define CPR_MULTIPERFORM_H + +#include "cpr/curlmultiholder.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include +#include +#include +#include +#include + +namespace cpr { + +class InterceptorMulti; + +class MultiPerform { + public: + enum class HttpMethod { + UNDEFINED = 0, + GET_REQUEST, + POST_REQUEST, + PUT_REQUEST, + DELETE_REQUEST, + PATCH_REQUEST, + HEAD_REQUEST, + OPTIONS_REQUEST, + DOWNLOAD_REQUEST, + }; + + MultiPerform(); + MultiPerform(const MultiPerform& other) = delete; + MultiPerform(MultiPerform&& old) = default; + ~MultiPerform(); + + MultiPerform& operator=(const MultiPerform& other) = delete; + MultiPerform& operator=(MultiPerform&& old) noexcept = default; + + std::vector Get(); + std::vector Delete(); + template + std::vector Download(DownloadArgTypes... args); + std::vector Put(); + std::vector Head(); + std::vector Options(); + std::vector Patch(); + std::vector Post(); + + std::vector Perform(); + template + std::vector PerformDownload(DownloadArgTypes... args); + + void AddSession(std::shared_ptr& session, HttpMethod method = HttpMethod::UNDEFINED); + void RemoveSession(const std::shared_ptr& session); + std::vector, HttpMethod>>& GetSessions(); + [[nodiscard]] const std::vector, HttpMethod>>& GetSessions() const; + + void AddInterceptor(const std::shared_ptr& pinterceptor); + + private: + // Interceptors should be able to call the private proceed() and PrepareDownloadSessions() functions + friend InterceptorMulti; + + void SetHttpMethod(HttpMethod method); + + void PrepareSessions(); + template + void PrepareDownloadSessions(size_t sessions_index, CurrentDownloadArgType current_arg, DownloadArgTypes... args); + template + void PrepareDownloadSessions(size_t sessions_index, CurrentDownloadArgType current_arg); + void PrepareDownloadSession(size_t sessions_index, std::ofstream& file); + void PrepareDownloadSession(size_t sessions_index, const WriteCallback& write); + + void PrepareGet(); + void PrepareDelete(); + void PreparePut(); + void PreparePatch(); + void PrepareHead(); + void PrepareOptions(); + void PreparePost(); + template + void PrepareDownload(DownloadArgTypes... args); + + const std::optional> intercept(); + std::vector proceed(); + std::vector MakeRequest(); + std::vector MakeDownloadRequest(); + + void DoMultiPerform(); + std::vector ReadMultiInfo(const std::function& complete_function); + + std::vector, HttpMethod>> sessions_; + std::unique_ptr multicurl_; + bool is_download_multi_perform{false}; + + using InterceptorsContainer = std::list>; + InterceptorsContainer interceptors_; + // Currently running interceptor + InterceptorsContainer::iterator current_interceptor_; + // Interceptor within the chain where to start with each repeated request + InterceptorsContainer::iterator first_interceptor_; +}; + +template +void MultiPerform::PrepareDownloadSessions(size_t sessions_index, CurrentDownloadArgType current_arg) { + PrepareDownloadSession(sessions_index, current_arg); +} + +template +void MultiPerform::PrepareDownloadSessions(size_t sessions_index, CurrentDownloadArgType current_arg, DownloadArgTypes... args) { + PrepareDownloadSession(sessions_index, current_arg); + PrepareDownloadSessions(sessions_index + 1, args...); +} + + +template +void MultiPerform::PrepareDownload(DownloadArgTypes... args) { + SetHttpMethod(HttpMethod::DOWNLOAD_REQUEST); + PrepareDownloadSessions(0, args...); +} + +template +std::vector MultiPerform::Download(DownloadArgTypes... args) { + if (sizeof...(args) != sessions_.size()) { + throw std::invalid_argument("Number of download arguments has to match the number of sessions added to the multiperform!"); + } + PrepareDownload(args...); + return MakeDownloadRequest(); +} + +template +std::vector MultiPerform::PerformDownload(DownloadArgTypes... args) { + if (sizeof...(args) != sessions_.size()) { + throw std::invalid_argument("Number of download arguments has to match the number of sessions added to the multiperform!"); + } + PrepareDownloadSessions(0, args...); + return MakeDownloadRequest(); +} + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/parameters.h b/3rdparty/cpr/include/cpr/parameters.h new file mode 100644 index 0000000000000000000000000000000000000000..620962778f8ae348348c72ab0fd8fa0933089634 --- /dev/null +++ b/3rdparty/cpr/include/cpr/parameters.h @@ -0,0 +1,18 @@ +#ifndef CPR_PARAMETERS_H +#define CPR_PARAMETERS_H + +#include + +#include "cpr/curl_container.h" + +namespace cpr { + +class Parameters : public CurlContainer { + public: + Parameters() = default; + Parameters(const std::initializer_list& parameters) : CurlContainer(parameters) {} +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/payload.h b/3rdparty/cpr/include/cpr/payload.h new file mode 100644 index 0000000000000000000000000000000000000000..0741a88fae0d4be0663136bd973f65a3792b645c --- /dev/null +++ b/3rdparty/cpr/include/cpr/payload.h @@ -0,0 +1,23 @@ +#ifndef CPR_PAYLOAD_H +#define CPR_PAYLOAD_H + +#include + +#include "cpr/curl_container.h" + + +namespace cpr { +class Payload : public CurlContainer { + public: + template + Payload(const It begin, const It end) { + for (It pair = begin; pair != end; ++pair) { + Add(*pair); + } + } + Payload(const std::initializer_list& pairs) : CurlContainer(pairs) {} +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/proxies.h b/3rdparty/cpr/include/cpr/proxies.h new file mode 100644 index 0000000000000000000000000000000000000000..6970442de55634adb98464f3cd9688293dde8c13 --- /dev/null +++ b/3rdparty/cpr/include/cpr/proxies.h @@ -0,0 +1,23 @@ +#ifndef CPR_PROXIES_H +#define CPR_PROXIES_H + +#include +#include +#include + +namespace cpr { +class Proxies { + public: + Proxies() = default; + Proxies(const std::initializer_list>& hosts); + Proxies(const std::map& hosts); + + bool has(const std::string& protocol) const; + const std::string& operator[](const std::string& protocol); + + private: + std::map hosts_; +}; +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/proxyauth.h b/3rdparty/cpr/include/cpr/proxyauth.h new file mode 100644 index 0000000000000000000000000000000000000000..521a0c58993b87c83460f31681c33799e2b2f74e --- /dev/null +++ b/3rdparty/cpr/include/cpr/proxyauth.h @@ -0,0 +1,51 @@ +#ifndef CPR_PROXYAUTH_H +#define CPR_PROXYAUTH_H + +#include +#include +#include + +#include "cpr/auth.h" +#include "cpr/util.h" + +namespace cpr { +class ProxyAuthentication; + +class EncodedAuthentication { + friend ProxyAuthentication; + + public: + EncodedAuthentication() = default; + EncodedAuthentication(const std::string& p_username, const std::string& p_password) : username(util::urlEncode(p_username)), password(util::urlEncode(p_password)) {} + EncodedAuthentication(const EncodedAuthentication& other) = default; + EncodedAuthentication(EncodedAuthentication&& old) noexcept = default; + virtual ~EncodedAuthentication() noexcept; + + EncodedAuthentication& operator=(EncodedAuthentication&& old) noexcept = default; + EncodedAuthentication& operator=(const EncodedAuthentication& other) = default; + + [[nodiscard]] const std::string& GetUsername() const; + [[nodiscard]] const std::string& GetPassword() const; + + private: + std::string username; + std::string password; +}; + +class ProxyAuthentication { + public: + ProxyAuthentication() = default; + ProxyAuthentication(const std::initializer_list>& auths) : proxyAuth_{auths} {} + explicit ProxyAuthentication(const std::map& auths) : proxyAuth_{auths} {} + + [[nodiscard]] bool has(const std::string& protocol) const; + const char* GetUsername(const std::string& protocol); + const char* GetPassword(const std::string& protocol); + + private: + std::map proxyAuth_; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/range.h b/3rdparty/cpr/include/cpr/range.h new file mode 100644 index 0000000000000000000000000000000000000000..2c5a145d54a93d96643020aae89064bf1393ec93 --- /dev/null +++ b/3rdparty/cpr/include/cpr/range.h @@ -0,0 +1,44 @@ +#ifndef CPR_RANGE_H +#define CPR_RANGE_H + +#include +#include + +namespace cpr { + +class Range { + public: + Range(const std::optional p_resume_from = std::nullopt, const std::optional p_finish_at = std::nullopt) { + resume_from = p_resume_from.value_or(0); + finish_at = p_finish_at.value_or(-1); + } + + std::int64_t resume_from; + std::int64_t finish_at; + + const std::string str() const { + std::string from_str = (resume_from < 0U) ? "" : std::to_string(resume_from); + std::string to_str = (finish_at < 0U) ? "" : std::to_string(finish_at); + return from_str + "-" + to_str; + } +}; + +class MultiRange { + public: + MultiRange(std::initializer_list rs) : ranges{rs} {} + + const std::string str() const { + std::string multi_range_string{}; + for (Range range : ranges) { + multi_range_string += ((multi_range_string.empty()) ? "" : ", ") + range.str(); + } + return multi_range_string; + } + + private: + std::vector ranges; +}; // namespace cpr + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/redirect.h b/3rdparty/cpr/include/cpr/redirect.h new file mode 100644 index 0000000000000000000000000000000000000000..d4c8e576b8610a5f5422cda9a718c630963f3e61 --- /dev/null +++ b/3rdparty/cpr/include/cpr/redirect.h @@ -0,0 +1,84 @@ +#ifndef CPR_REDIRECT_H +#define CPR_REDIRECT_H + +#include + +namespace cpr { +enum class PostRedirectFlags : uint8_t { + /** + * Respect RFC 7231 (section 6.4.2 to 6.4.4). + * Same as CURL_REDIR_POST_301 (https://curl.se/libcurl/c/CURLOPT_POSTREDIR.html). + **/ + POST_301 = 0x1 << 0, + /** + * Maintain the request method after a 302 redirect. + * Same as CURL_REDIR_POST_302 (https://curl.se/libcurl/c/CURLOPT_POSTREDIR.html). + **/ + POST_302 = 0x1 << 1, + /** + * Maintain the request method after a 303 redirect. + * Same as CURL_REDIR_POST_303 (https://curl.se/libcurl/c/CURLOPT_POSTREDIR.html). + **/ + POST_303 = 0x1 << 2, + /** + * Default value. + * Convenience option to enable all flags. + * Same as CURL_REDIR_POST_ALL (https://curl.se/libcurl/c/CURLOPT_POSTREDIR.html). + **/ + POST_ALL = POST_301 | POST_302 | POST_303, + /** + * * Convenience option to disable all flags. + **/ + NONE = 0x0 +}; + +PostRedirectFlags operator|(PostRedirectFlags lhs, PostRedirectFlags rhs); +PostRedirectFlags operator&(PostRedirectFlags lhs, PostRedirectFlags rhs); +PostRedirectFlags operator^(PostRedirectFlags lhs, PostRedirectFlags rhs); +PostRedirectFlags operator~(PostRedirectFlags flag); +PostRedirectFlags& operator|=(PostRedirectFlags& lhs, PostRedirectFlags rhs); +PostRedirectFlags& operator&=(PostRedirectFlags& lhs, PostRedirectFlags rhs); +PostRedirectFlags& operator^=(PostRedirectFlags& lhs, PostRedirectFlags rhs); +bool any(PostRedirectFlags flag); + +class Redirect { + public: + /** + * The maximum number of redirects to follow. + * 0: Refuse any redirects. + * -1: Infinite number of redirects. + * Default: 50 + * https://curl.se/libcurl/c/CURLOPT_MAXREDIRS.html + **/ + // NOLINTNEXTLINE (google-runtime-int) + long maximum{50L}; + /** + * Follow 3xx redirects. + * Default: true + * https://curl.se/libcurl/c/CURLOPT_FOLLOWLOCATION.html + **/ + bool follow{true}; + /** + * Continue to send authentication (user+password) credentials when following locations, even when hostname changed. + * Default: false + * https://curl.se/libcurl/c/CURLOPT_UNRESTRICTED_AUTH.html + **/ + bool cont_send_cred{false}; + /** + * Flags to control how to act after a redirect for a post request. + * Default: POST_ALL + **/ + PostRedirectFlags post_flags{PostRedirectFlags::POST_ALL}; + + Redirect() = default; + // NOLINTNEXTLINE (google-runtime-int) + Redirect(long p_maximum, bool p_follow, bool p_cont_send_cred, PostRedirectFlags p_post_flags) : maximum(p_maximum), follow(p_follow), cont_send_cred(p_cont_send_cred), post_flags(p_post_flags) {} + // NOLINTNEXTLINE (google-runtime-int) + explicit Redirect(long p_maximum) : maximum(p_maximum) {} + explicit Redirect(bool p_follow) : follow(p_follow) {} + Redirect(bool p_follow, bool p_cont_send_cred) : follow(p_follow), cont_send_cred(p_cont_send_cred) {} + explicit Redirect(PostRedirectFlags p_post_flags) : post_flags(p_post_flags) {} +}; +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/reserve_size.h b/3rdparty/cpr/include/cpr/reserve_size.h new file mode 100644 index 0000000000000000000000000000000000000000..5eae4c80ac1508eefeefb0281ba411d497ae6fed --- /dev/null +++ b/3rdparty/cpr/include/cpr/reserve_size.h @@ -0,0 +1,17 @@ +#ifndef CPR_RESERVE_SIZE_H +#define CPR_RESERVE_SIZE_H + +#include + +namespace cpr { + +class ReserveSize { + public: + ReserveSize(const size_t _size) : size(_size) {} + + size_t size = 0; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/resolve.h b/3rdparty/cpr/include/cpr/resolve.h new file mode 100644 index 0000000000000000000000000000000000000000..6f0e52c160b5de69b3670264979aca4464010ab6 --- /dev/null +++ b/3rdparty/cpr/include/cpr/resolve.h @@ -0,0 +1,23 @@ +#ifndef CPR_RESOLVE_H +#define CPR_RESOLVE_H + +#include +#include + +namespace cpr { +class Resolve { + public: + std::string host; + std::string addr; + std::set ports; + + Resolve(const std::string& host_param, const std::string& addr_param, const std::set& ports_param = std::set{80U, 443U}) : host(host_param), addr(addr_param), ports(ports_param) { + if (this->ports.empty()) { + this->ports.insert(80U); + this->ports.insert(443U); + } + } +}; +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/response.h b/3rdparty/cpr/include/cpr/response.h new file mode 100644 index 0000000000000000000000000000000000000000..3550d0910703a99d7c351be3cf622e714eb58182 --- /dev/null +++ b/3rdparty/cpr/include/cpr/response.h @@ -0,0 +1,58 @@ +#ifndef CPR_RESPONSE_H +#define CPR_RESPONSE_H + +#include +#include +#include +#include +#include +#include + +#include "cpr/cert_info.h" +#include "cpr/cookies.h" +#include "cpr/cprtypes.h" +#include "cpr/error.h" +#include "cpr/ssl_options.h" +#include "cpr/util.h" + +namespace cpr { + +class MultiPerform; + +class Response { + private: + friend MultiPerform; + std::shared_ptr curl_{nullptr}; + + public: + // Ignored here since libcurl uses a long for this. + // NOLINTNEXTLINE(google-runtime-int) + long status_code{}; + std::string text{}; + Header header{}; + Url url{}; + double elapsed{}; + Cookies cookies{}; + Error error{}; + std::string raw_header{}; + std::string status_line{}; + std::string reason{}; + cpr_off_t uploaded_bytes{}; + cpr_off_t downloaded_bytes{}; + // Ignored here since libcurl uses a long for this. + // NOLINTNEXTLINE(google-runtime-int) + long redirect_count{}; + + Response() = default; + Response(std::shared_ptr curl, std::string&& p_text, std::string&& p_header_string, Cookies&& p_cookies, Error&& p_error); + [[nodiscard]] std::vector GetCertInfos() const; + Response(const Response& other) = default; + Response(Response&& old) noexcept = default; + ~Response() noexcept = default; + + Response& operator=(Response&& old) noexcept = default; + Response& operator=(const Response& other) = default; +}; +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/session.h b/3rdparty/cpr/include/cpr/session.h new file mode 100644 index 0000000000000000000000000000000000000000..89c0e1d9850a018ef6bb4a488535212cf4404be5 --- /dev/null +++ b/3rdparty/cpr/include/cpr/session.h @@ -0,0 +1,342 @@ +#ifndef CPR_SESSION_H +#define CPR_SESSION_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cpr/accept_encoding.h" +#include "cpr/async_wrapper.h" +#include "cpr/auth.h" +#include "cpr/bearer.h" +#include "cpr/body.h" +#include "cpr/callback.h" +#include "cpr/connect_timeout.h" +#include "cpr/cookies.h" +#include "cpr/cprtypes.h" +#include "cpr/curlholder.h" +#include "cpr/http_version.h" +#include "cpr/interface.h" +#include "cpr/limit_rate.h" +#include "cpr/local_port.h" +#include "cpr/local_port_range.h" +#include "cpr/low_speed.h" +#include "cpr/multipart.h" +#include "cpr/parameters.h" +#include "cpr/payload.h" +#include "cpr/proxies.h" +#include "cpr/proxyauth.h" +#include "cpr/range.h" +#include "cpr/redirect.h" +#include "cpr/reserve_size.h" +#include "cpr/resolve.h" +#include "cpr/response.h" +#include "cpr/ssl_options.h" +#include "cpr/timeout.h" +#include "cpr/unix_socket.h" +#include "cpr/user_agent.h" +#include "cpr/util.h" +#include "cpr/verbose.h" + +namespace cpr { + +using AsyncResponse = AsyncWrapper; + +class Interceptor; +class MultiPerform; + +class Session : public std::enable_shared_from_this { + public: + Session(); + Session(const Session& other) = delete; + Session(Session&& old) = delete; + + ~Session() = default; + + Session& operator=(Session&& old) noexcept = delete; + Session& operator=(const Session& other) = delete; + + void SetUrl(const Url& url); + void SetParameters(const Parameters& parameters); + void SetParameters(Parameters&& parameters); + void SetHeader(const Header& header); + void UpdateHeader(const Header& header); + void SetTimeout(const Timeout& timeout); + void SetConnectTimeout(const ConnectTimeout& timeout); + void SetAuth(const Authentication& auth); +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 + void SetBearer(const Bearer& token); +#endif + void SetUserAgent(const UserAgent& ua); + void SetPayload(Payload&& payload); + void SetPayload(const Payload& payload); + void SetProxies(Proxies&& proxies); + void SetProxies(const Proxies& proxies); + void SetProxyAuth(ProxyAuthentication&& proxy_auth); + void SetProxyAuth(const ProxyAuthentication& proxy_auth); + void SetMultipart(Multipart&& multipart); + void SetMultipart(const Multipart& multipart); + void SetRedirect(const Redirect& redirect); + void SetCookies(const Cookies& cookies); + void SetBody(Body&& body); + void SetBody(const Body& body); + void SetLowSpeed(const LowSpeed& low_speed); + void SetVerifySsl(const VerifySsl& verify); + void SetUnixSocket(const UnixSocket& unix_socket); + void SetSslOptions(const SslOptions& options); + void SetReadCallback(const ReadCallback& read); + void SetHeaderCallback(const HeaderCallback& header); + void SetWriteCallback(const WriteCallback& write); + void SetProgressCallback(const ProgressCallback& progress); + void SetDebugCallback(const DebugCallback& debug); + void SetVerbose(const Verbose& verbose); + void SetInterface(const Interface& iface); + void SetLocalPort(const LocalPort& local_port); + void SetLocalPortRange(const LocalPortRange& local_port_range); + void SetHttpVersion(const HttpVersion& version); + void SetRange(const Range& range); + void SetResolve(const Resolve& resolve); + void SetResolves(const std::vector& resolves); + void SetMultiRange(const MultiRange& multi_range); + void SetReserveSize(const ReserveSize& reserve_size); + void SetAcceptEncoding(const AcceptEncoding& accept_encoding); + void SetAcceptEncoding(AcceptEncoding&& accept_encoding); + void SetLimitRate(const LimitRate& limit_rate); + + // For cancellable requests + void SetCancellationParam(std::shared_ptr param); + + // Used in templated functions + void SetOption(const Url& url); + void SetOption(const Parameters& parameters); + void SetOption(Parameters&& parameters); + void SetOption(const Header& header); + void SetOption(const Timeout& timeout); + void SetOption(const ConnectTimeout& timeout); + void SetOption(const Authentication& auth); +// Only supported with libcurl >= 7.61.0. +// As an alternative use SetHeader and add the token manually. +#if LIBCURL_VERSION_NUM >= 0x073D00 + void SetOption(const Bearer& auth); +#endif + void SetOption(const UserAgent& ua); + void SetOption(Payload&& payload); + void SetOption(const Payload& payload); + void SetOption(const LimitRate& limit_rate); + void SetOption(Proxies&& proxies); + void SetOption(const Proxies& proxies); + void SetOption(ProxyAuthentication&& proxy_auth); + void SetOption(const ProxyAuthentication& proxy_auth); + void SetOption(Multipart&& multipart); + void SetOption(const Multipart& multipart); + void SetOption(const Redirect& redirect); + void SetOption(const Cookies& cookies); + void SetOption(Body&& body); + void SetOption(const Body& body); + void SetOption(const ReadCallback& read); + void SetOption(const HeaderCallback& header); + void SetOption(const WriteCallback& write); + void SetOption(const ProgressCallback& progress); + void SetOption(const DebugCallback& debug); + void SetOption(const LowSpeed& low_speed); + void SetOption(const VerifySsl& verify); + void SetOption(const Verbose& verbose); + void SetOption(const UnixSocket& unix_socket); + void SetOption(const SslOptions& options); + void SetOption(const Interface& iface); + void SetOption(const LocalPort& local_port); + void SetOption(const LocalPortRange& local_port_range); + void SetOption(const HttpVersion& version); + void SetOption(const Range& range); + void SetOption(const MultiRange& multi_range); + void SetOption(const ReserveSize& reserve_size); + void SetOption(const AcceptEncoding& accept_encoding); + void SetOption(AcceptEncoding&& accept_encoding); + void SetOption(const Resolve& resolve); + void SetOption(const std::vector& resolves); + + cpr_off_t GetDownloadFileLength(); + /** + * Attempt to preallocate enough memory for specified number of characters in the response string. + * Pass 0 to disable this behavior and let the response string be allocated dynamically on demand. + * + * Example: + * cpr::Session session; + * session.SetUrl(cpr::Url{"http://xxx/file"}); + * session.ResponseStringReserve(1024 * 512); // Reserve space for at least 1024 * 512 characters + * cpr::Response r = session.Get(); + **/ + void ResponseStringReserve(size_t size); + Response Delete(); + Response Download(const WriteCallback& write); + Response Download(std::ofstream& file); + Response Get(); + Response Head(); + Response Options(); + Response Patch(); + Response Post(); + Response Put(); + + AsyncResponse GetAsync(); + AsyncResponse DeleteAsync(); + AsyncResponse DownloadAsync(const WriteCallback& write); + AsyncResponse DownloadAsync(std::ofstream& file); + AsyncResponse HeadAsync(); + AsyncResponse OptionsAsync(); + AsyncResponse PatchAsync(); + AsyncResponse PostAsync(); + AsyncResponse PutAsync(); + + template + auto GetCallback(Then then); + template + auto PostCallback(Then then); + template + auto PutCallback(Then then); + template + auto HeadCallback(Then then); + template + auto DeleteCallback(Then then); + template + auto OptionsCallback(Then then); + template + auto PatchCallback(Then then); + + std::shared_ptr GetCurlHolder(); + std::string GetFullRequestUrl(); + + void PrepareDelete(); + void PrepareGet(); + void PrepareHead(); + void PrepareOptions(); + void PreparePatch(); + void PreparePost(); + void PreparePut(); + void PrepareDownload(const WriteCallback& write); + void PrepareDownload(std::ofstream& file); + Response Complete(CURLcode curl_error); + Response CompleteDownload(CURLcode curl_error); + + void AddInterceptor(const std::shared_ptr& pinterceptor); + + std::shared_ptr GetSharedPtrFromThis(); + + private: + // Interceptors should be able to call the private proceed() function + friend Interceptor; + friend MultiPerform; + + + bool chunkedTransferEncoding_{false}; + std::variant content_{std::monostate{}}; + std::shared_ptr curl_; + Url url_; + Parameters parameters_; + Proxies proxies_; + ProxyAuthentication proxyAuth_; + Header header_; + AcceptEncoding acceptEncoding_; + + + struct Callbacks { + /** + * Will be set by the read callback. + * Ensures that the "Transfer-Encoding" is set to "chunked", if not overriden in header_. + **/ + ReadCallback readcb_; + HeaderCallback headercb_; + WriteCallback writecb_; + ProgressCallback progresscb_; + DebugCallback debugcb_; + CancellationCallback cancellationcb_; + }; + + std::unique_ptr cbs_{std::make_unique()}; + + size_t response_string_reserve_size_{0}; + std::string response_string_; + std::string header_string_; + // Container type is required to keep iterator valid on elem insertion. E.g. list but not vector. + using InterceptorsContainer = std::list>; + InterceptorsContainer interceptors_; + // Currently running interceptor + InterceptorsContainer::const_iterator current_interceptor_; + // Interceptor within the chain where to start with each repeated request + InterceptorsContainer::const_iterator first_interceptor_; + bool isUsedInMultiPerform{false}; + bool isCancellable{false}; + +#if SUPPORT_SSL_NO_REVOKE + bool sslNoRevoke_{false}; +#endif + + Response makeDownloadRequest(); + Response makeRequest(); + Response proceed(); + const std::optional intercept(); + /** + * Prepares the curl object for a request with everything used by all requests. + **/ + void prepareCommonShared(); + /** + * Prepares the curl object for a request with everything used by all non download related requests. + **/ + void prepareCommon(); + /** + * Prepares the curl object for a request with everything used by the download request. + **/ + void prepareCommonDownload(); + void prepareHeader(); + CURLcode DoEasyPerform(); + void prepareBodyPayloadOrMultipart() const; + /** + * Returns true in case content_ is of type cpr::Body or cpr::Payload. + **/ + [[nodiscard]] bool hasBodyOrPayload() const; +}; + +template +auto Session::GetCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Get()); }, std::move(then)); +} + +template +auto Session::PostCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Post()); }, std::move(then)); +} + +template +auto Session::PutCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Put()); }, std::move(then)); +} + +template +auto Session::HeadCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Head()); }, std::move(then)); +} + +template +auto Session::DeleteCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Delete()); }, std::move(then)); +} + +template +auto Session::OptionsCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Options()); }, std::move(then)); +} + +template +auto Session::PatchCallback(Then then) { + return async([shared_this = GetSharedPtrFromThis()](Then then_inner) { return then_inner(shared_this->Patch()); }, std::move(then)); +} + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/singleton.h b/3rdparty/cpr/include/cpr/singleton.h new file mode 100644 index 0000000000000000000000000000000000000000..e52439bf1adce16435eb3fb5951bfd58c73a8e87 --- /dev/null +++ b/3rdparty/cpr/include/cpr/singleton.h @@ -0,0 +1,44 @@ +#ifndef CPR_SINGLETON_H +#define CPR_SINGLETON_H + +#include +#include + +#ifndef CPR_DISABLE_COPY +#define CPR_DISABLE_COPY(Class) \ + Class(const Class&) = delete; \ + Class& operator=(const Class&) = delete; +#endif + +#ifndef CPR_SINGLETON_DECL +#define CPR_SINGLETON_DECL(Class) \ + public: \ + static Class* GetInstance(); \ + static void ExitInstance(); \ + \ + private: \ + CPR_DISABLE_COPY(Class) \ + static Class* s_pInstance; \ + static std::once_flag s_getFlag; \ + static std::once_flag s_exitFlag; +#endif + +#ifndef CPR_SINGLETON_IMPL +#define CPR_SINGLETON_IMPL(Class) \ + Class* Class::s_pInstance = nullptr; \ + std::once_flag Class::s_getFlag{}; \ + std::once_flag Class::s_exitFlag{}; \ + Class* Class::GetInstance() { \ + std::call_once(Class::s_getFlag, []() { s_pInstance = new Class; }); \ + return s_pInstance; \ + } \ + void Class::ExitInstance() { \ + std::call_once(Class::s_exitFlag, []() { \ + assert(s_pInstance); \ + delete s_pInstance; \ + s_pInstance = nullptr; \ + }); \ + } +#endif + +#endif diff --git a/3rdparty/cpr/include/cpr/ssl_ctx.h b/3rdparty/cpr/include/cpr/ssl_ctx.h new file mode 100644 index 0000000000000000000000000000000000000000..b6bc811909848586e19d0729209f960d0401e65a --- /dev/null +++ b/3rdparty/cpr/include/cpr/ssl_ctx.h @@ -0,0 +1,26 @@ +#ifndef CPR_SSL_CTX_H +#define CPR_SSL_CTX_H + +#include "cpr/ssl_options.h" +#include + +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION + +namespace cpr { + +/** + * This callback function loads a CA certificate from raw_cert_buf and gets called by libcurl + * just before the initialization of an SSL connection. + * The raw_cert_buf argument is set with the CURLOPT_SSL_CTX_DATA option and has to be a nul + * terminated buffer. + * + * Sources: https://curl.se/libcurl/c/CURLOPT_SSL_CTX_FUNCTION.html + * https://curl.se/libcurl/c/CURLOPT_SSL_CTX_DATA.html + */ +CURLcode sslctx_function_load_ca_cert_from_buffer(CURL* curl, void* sslctx, void* raw_cert_buf); + +} // Namespace cpr + +#endif + +#endif diff --git a/3rdparty/cpr/include/cpr/ssl_options.h b/3rdparty/cpr/include/cpr/ssl_options.h new file mode 100644 index 0000000000000000000000000000000000000000..41942a4862603158152b32afb2622e08c9203149 --- /dev/null +++ b/3rdparty/cpr/include/cpr/ssl_options.h @@ -0,0 +1,619 @@ +#ifndef CPR_SSLOPTIONS_H +#define CPR_SSLOPTIONS_H + +#include +#include +#include + +#include "cpr/filesystem.h" +#include + +#include "cpr/util.h" +#include + +#ifndef SUPPORT_ALPN +#define SUPPORT_ALPN LIBCURL_VERSION_NUM >= 0x072400 // 7.36.0 +#endif +#ifndef SUPPORT_NPN +#define SUPPORT_NPN LIBCURL_VERSION_NUM >= 0x072400 && LIBCURL_VERSION_NUM < 0x075600 // 7.36.0 - 7.85.0 +#endif + +#ifndef SUPPORT_SSLv2 +#define SUPPORT_SSLv2 LIBCURL_VERSION_NUM <= 0x071300 // 7.19.0 +#endif +#ifndef SUPPORT_SSLv3 +#define SUPPORT_SSLv3 LIBCURL_VERSION_NUM <= 0x072700 // 7.39.0 +#endif +#ifndef SUPPORT_TLSv1_0 +#define SUPPORT_TLSv1_0 LIBCURL_VERSION_NUM >= 0x072200 // 7.34.0 +#endif +#ifndef SUPPORT_TLSv1_1 +#define SUPPORT_TLSv1_1 LIBCURL_VERSION_NUM >= 0x072200 // 7.34.0 +#endif +#ifndef SUPPORT_TLSv1_2 +#define SUPPORT_TLSv1_2 LIBCURL_VERSION_NUM >= 0x072200 // 7.34.0 +#endif +#ifndef SUPPORT_TLSv1_3 +#define SUPPORT_TLSv1_3 LIBCURL_VERSION_NUM >= 0x073400 // 7.52.0 +#endif +#ifndef SUPPORT_MAX_TLS_VERSION +#define SUPPORT_MAX_TLS_VERSION LIBCURL_VERSION_NUM >= 0x073600 // 7.54.0 +#endif +#ifndef SUPPORT_MAX_TLSv1_1 +#define SUPPORT_MAX_TLSv1_1 LIBCURL_VERSION_NUM >= 0x073600 // 7.54.0 +#endif +#ifndef SUPPORT_MAX_TLSv1_2 +#define SUPPORT_MAX_TLSv1_2 LIBCURL_VERSION_NUM >= 0x073600 // 7.54.0 +#endif +#ifndef SUPPORT_MAX_TLSv1_3 +#define SUPPORT_MAX_TLSv1_3 LIBCURL_VERSION_NUM >= 0x073600 // 7.54.0 +#endif +#ifndef SUPPORT_TLSv13_CIPHERS +#define SUPPORT_TLSv13_CIPHERS LIBCURL_VERSION_NUM >= 0x073D00 // 7.61.0 +#endif +#ifndef SUPPORT_SESSIONID_CACHE +#define SUPPORT_SESSIONID_CACHE LIBCURL_VERSION_NUM >= 0x071000 // 7.16.0 +#endif +#ifndef SUPPORT_SSL_FALSESTART +#define SUPPORT_SSL_FALSESTART LIBCURL_VERSION_NUM >= 0x072A00 // 7.42.0 +#endif +#ifndef SUPPORT_SSL_NO_REVOKE +#define SUPPORT_SSL_NO_REVOKE LIBCURL_VERSION_NUM >= 0x072C00 // 7.44.0 +#endif +#ifndef SUPPORT_CURLOPT_SSLKEY_BLOB +#define SUPPORT_CURLOPT_SSLKEY_BLOB LIBCURL_VERSION_NUM >= 0x074700 // 7.71.0 +#endif +#ifndef SUPPORT_CURLOPT_SSL_CTX_FUNCTION +#define SUPPORT_CURLOPT_SSL_CTX_FUNCTION LIBCURL_VERSION_NUM >= 0x070B00 // 7.11.0 +#endif + +namespace cpr { + +class VerifySsl { + public: + VerifySsl() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + VerifySsl(bool p_verify) : verify(p_verify) {} + + explicit operator bool() const { + return verify; + } + + bool verify = true; +}; + +namespace ssl { + +// set SSL client certificate +class CertFile { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + CertFile(fs::path&& p_filename) : filename(std::move(p_filename)) {} + + virtual ~CertFile() = default; + + const fs::path filename; + + virtual const char* GetCertType() const { + return "PEM"; + } +}; + +using PemCert = CertFile; + +class DerCert : public CertFile { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + DerCert(fs::path&& p_filename) : CertFile(std::move(p_filename)) {} + + ~DerCert() override = default; + + const char* GetCertType() const override { + return "DER"; + } +}; + +// specify private keyfile for TLS and SSL client cert +class KeyFile { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + KeyFile(fs::path&& p_filename) : filename(std::move(p_filename)) {} + + template + KeyFile(FileType&& p_filename, PassType p_password) : filename(std::forward(p_filename)), password(std::move(p_password)) {} + + virtual ~KeyFile() { + util::secureStringClear(password); + } + + fs::path filename; + std::string password; + + virtual const char* GetKeyType() const { + return "PEM"; + } +}; + +#if SUPPORT_CURLOPT_SSLKEY_BLOB +class KeyBlob { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + KeyBlob(std::string&& p_blob) : blob(std::move(p_blob)) {} + + template + KeyBlob(BlobType&& p_blob, PassType p_password) : blob(std::forward(p_blob)), password(std::move(p_password)) {} + + virtual ~KeyBlob() { + util::secureStringClear(password); + } + + std::string blob; + std::string password; + + virtual const char* GetKeyType() const { + return "PEM"; + } +}; +#endif + +using PemKey = KeyFile; + +class DerKey : public KeyFile { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + DerKey(fs::path&& p_filename) : KeyFile(std::move(p_filename)) {} + + template + DerKey(FileType&& p_filename, PassType p_password) : KeyFile(std::forward(p_filename), std::move(p_password)) {} + + ~DerKey() override = default; + + const char* GetKeyType() const override { + return "DER"; + } +}; + +class PinnedPublicKey { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + PinnedPublicKey(std::string&& p_pinned_public_key) : pinned_public_key(std::move(p_pinned_public_key)) {} + + const std::string pinned_public_key; +}; + +#if SUPPORT_ALPN +// This option enables/disables ALPN in the SSL handshake (if the SSL backend libcurl is built to +// use supports it), which can be used to negotiate http2. +class ALPN { + public: + ALPN() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + ALPN(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = true; +}; +#endif // SUPPORT_ALPN + +#if SUPPORT_NPN +// This option enables/disables NPN in the SSL handshake (if the SSL backend libcurl is built to +// use supports it), which can be used to negotiate http2. +class NPN { + public: + NPN() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + NPN(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = true; +}; +#endif // SUPPORT_NPN + +// This option determines whether libcurl verifies that the server cert is for the server it is +// known as. +class VerifyHost { + public: + VerifyHost() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + VerifyHost(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = true; +}; + +// This option determines whether libcurl verifies the authenticity of the peer's certificate. +class VerifyPeer { + public: + VerifyPeer() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + VerifyPeer(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = true; +}; + +// This option determines whether libcurl verifies the status of the server cert using the +// "Certificate Status Request" TLS extension (aka. OCSP stapling). +class VerifyStatus { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + VerifyStatus(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = false; +}; + +// TLS v1.0 or later +struct TLSv1 {}; +#if SUPPORT_SSLv2 +// SSL v2 (but not SSLv3) +struct SSLv2 {}; +#endif +#if SUPPORT_SSLv3 +// SSL v3 (but not SSLv2) +struct SSLv3 {}; +#endif +#if SUPPORT_TLSv1_0 +// TLS v1.0 or later (Added in 7.34.0) +struct TLSv1_0 {}; +#endif +#if SUPPORT_TLSv1_1 +// TLS v1.1 or later (Added in 7.34.0) +struct TLSv1_1 {}; +#endif +#if SUPPORT_TLSv1_2 +// TLS v1.2 or later (Added in 7.34.0) +struct TLSv1_2 {}; +#endif +#if SUPPORT_TLSv1_3 +// TLS v1.3 or later (Added in 7.52.0) +struct TLSv1_3 {}; +#endif +#if SUPPORT_MAX_TLS_VERSION +// The flag defines the maximum supported TLS version by libcurl, or the default value from the SSL +// library is used. +struct MaxTLSVersion {}; +#endif +#if SUPPORT_MAX_TLSv1_0 +// The flag defines maximum supported TLS version as TLSv1.0. (Added in 7.54.0) +struct MaxTLSv1_0 {}; +#endif +#if SUPPORT_MAX_TLSv1_1 +// The flag defines maximum supported TLS version as TLSv1.1. (Added in 7.54.0) +struct MaxTLSv1_1 {}; +#endif +#if SUPPORT_MAX_TLSv1_2 +// The flag defines maximum supported TLS version as TLSv1.2. (Added in 7.54.0) +struct MaxTLSv1_2 {}; +#endif +#if SUPPORT_MAX_TLSv1_3 +// The flag defines maximum supported TLS version as TLSv1.3. (Added in 7.54.0) +struct MaxTLSv1_3 {}; +#endif + +// path to Certificate Authority (CA) bundle +class CaInfo { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + CaInfo(fs::path&& p_filename) : filename(std::move(p_filename)) {} + + fs::path filename; +}; + +// specify directory holding CA certificates +class CaPath { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + CaPath(fs::path&& p_filename) : filename(std::move(p_filename)) {} + + fs::path filename; +}; + +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION +class CaBuffer { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + CaBuffer(std::string&& p_buffer) : buffer(std::move(p_buffer)) {} + + const std::string buffer; +}; +#endif + +// specify a Certificate Revocation List file +class Crl { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Crl(fs::path&& p_filename) : filename(std::move(p_filename)) {} + + fs::path filename; +}; + +// specify ciphers to use for TLS +class Ciphers { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Ciphers(std::string&& p_ciphers) : ciphers(std::move(p_ciphers)) {} + + std::string ciphers; +}; + +#if SUPPORT_TLSv13_CIPHERS +// specify ciphers suites to use for TLS 1.3 +class TLS13_Ciphers { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + TLS13_Ciphers(std::string&& p_ciphers) : ciphers(std::move(p_ciphers)) {} + + std::string ciphers; +}; +#endif + +#if SUPPORT_SESSIONID_CACHE +// enable/disable use of the SSL session-ID cache +class SessionIdCache { + public: + SessionIdCache() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + SessionIdCache(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = true; +}; +#endif + +#if SUPPORT_SSL_FALSESTART +class SslFastStart { + public: + SslFastStart() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + SslFastStart(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = false; +}; +#endif + +class NoRevoke { + public: + NoRevoke() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + NoRevoke(bool p_enabled) : enabled(p_enabled) {} + + explicit operator bool() const { + return enabled; + } + + bool enabled = false; +}; + +} // namespace ssl + +struct SslOptions { + // We don't use fs::path here, as this leads to problems using windows + std::string cert_file; + std::string cert_type; + // We don't use fs::path here, as this leads to problems using windows + std::string key_file; +#if SUPPORT_CURLOPT_SSLKEY_BLOB + std::string key_blob; +#endif + std::string key_type; + std::string key_pass; + std::string pinned_public_key; +#if SUPPORT_ALPN + bool enable_alpn = true; +#endif // SUPPORT_ALPN +#if SUPPORT_NPN + bool enable_npn = true; +#endif // SUPPORT_ALPN + bool verify_host = true; + bool verify_peer = true; + bool verify_status = false; + int ssl_version = CURL_SSLVERSION_DEFAULT; +#if SUPPORT_SSL_NO_REVOKE + bool ssl_no_revoke = false; +#endif +#if SUPPORT_MAX_TLS_VERSION + int max_version = CURL_SSLVERSION_MAX_DEFAULT; +#endif + // We don't use fs::path here, as this leads to problems using windows + std::string ca_info; + // We don't use fs::path here, as this leads to problems using windows + std::string ca_path; +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION + std::string ca_buffer; +#endif + // We don't use fs::path here, as this leads to problems using windows + std::string crl_file; + std::string ciphers; +#if SUPPORT_TLSv13_CIPHERS + std::string tls13_ciphers; +#endif +#if SUPPORT_SESSIONID_CACHE + bool session_id_cache = true; +#endif + + ~SslOptions() noexcept { +#if SUPPORT_CURLOPT_SSLKEY_BLOB + util::secureStringClear(key_blob); +#endif + util::secureStringClear(key_pass); + } + + void SetOption(const ssl::CertFile& opt) { + cert_file = opt.filename.string(); + cert_type = opt.GetCertType(); + } + void SetOption(const ssl::KeyFile& opt) { + key_file = opt.filename.string(); + key_type = opt.GetKeyType(); + key_pass = opt.password; + } +#if SUPPORT_CURLOPT_SSLKEY_BLOB + void SetOption(const ssl::KeyBlob& opt) { + key_blob = opt.blob; + key_type = opt.GetKeyType(); + key_pass = opt.password; + } +#endif + void SetOption(const ssl::PinnedPublicKey& opt) { + pinned_public_key = opt.pinned_public_key; + } + +#if SUPPORT_ALPN + void SetOption(const ssl::ALPN& opt) { + enable_alpn = opt.enabled; + } +#endif // SUPPORT_ALPN +#if SUPPORT_NPN + void SetOption(const ssl::NPN& opt) { + enable_npn = opt.enabled; + } +#endif // SUPPORT_NPN + void SetOption(const ssl::VerifyHost& opt) { + verify_host = opt.enabled; + } + void SetOption(const ssl::VerifyPeer& opt) { + verify_peer = opt.enabled; + } + void SetOption(const ssl::VerifyStatus& opt) { + verify_status = opt.enabled; + } + void SetOption(const ssl::TLSv1& /*opt*/) { + ssl_version = CURL_SSLVERSION_TLSv1; + } +#if SUPPORT_SSL_NO_REVOKE + void SetOption(const ssl::NoRevoke& opt) { + ssl_no_revoke = opt.enabled; + } +#endif +#if SUPPORT_SSLv2 + void SetOption(const ssl::SSLv2& /*opt*/) { + ssl_version = CURL_SSLVERSION_SSLv2; + } +#endif +#if SUPPORT_SSLv3 + void SetOption(const ssl::SSLv3& /*opt*/) { + ssl_version = CURL_SSLVERSION_SSLv3; + } +#endif +#if SUPPORT_TLSv1_0 + void SetOption(const ssl::TLSv1_0& /*opt*/) { + ssl_version = CURL_SSLVERSION_TLSv1_0; + } +#endif +#if SUPPORT_TLSv1_1 + void SetOption(const ssl::TLSv1_1& /*opt*/) { + ssl_version = CURL_SSLVERSION_TLSv1_1; + } +#endif +#if SUPPORT_TLSv1_2 + void SetOption(const ssl::TLSv1_2& /*opt*/) { + ssl_version = CURL_SSLVERSION_TLSv1_2; + } +#endif +#if SUPPORT_TLSv1_3 + void SetOption(const ssl::TLSv1_3& /*opt*/) { + ssl_version = CURL_SSLVERSION_TLSv1_3; + } +#endif +#if SUPPORT_MAX_TLS_VERSION + void SetOption(const ssl::MaxTLSVersion& /*opt*/) { + max_version = CURL_SSLVERSION_DEFAULT; + } +#endif +#if SUPPORT_MAX_TLSv1_0 + void SetOption(const ssl::MaxTLSv1_0& opt) { + max_version = CURL_SSLVERSION_MAX_TLSv1_0; + } +#endif +#if SUPPORT_MAX_TLSv1_1 + void SetOption(const ssl::MaxTLSv1_1& /*opt*/) { + max_version = CURL_SSLVERSION_MAX_TLSv1_1; + } +#endif +#if SUPPORT_MAX_TLSv1_2 + void SetOption(const ssl::MaxTLSv1_2& /*opt*/) { + max_version = CURL_SSLVERSION_MAX_TLSv1_2; + } +#endif +#if SUPPORT_MAX_TLSv1_3 + void SetOption(const ssl::MaxTLSv1_3& /*opt*/) { + max_version = CURL_SSLVERSION_MAX_TLSv1_3; + } +#endif + void SetOption(const ssl::CaInfo& opt) { + ca_info = opt.filename.string(); + } + void SetOption(const ssl::CaPath& opt) { + ca_path = opt.filename.string(); + } +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION + void SetOption(const ssl::CaBuffer& opt) { + ca_buffer = opt.buffer; + } +#endif + void SetOption(const ssl::Crl& opt) { + crl_file = opt.filename.string(); + } + void SetOption(const ssl::Ciphers& opt) { + ciphers = opt.ciphers; + } +#if SUPPORT_TLSv13_CIPHERS + void SetOption(const ssl::TLS13_Ciphers& opt) { + tls13_ciphers = opt.ciphers; + } +#endif +#if SUPPORT_SESSIONID_CACHE + void SetOption(const ssl::SessionIdCache& opt) { + session_id_cache = opt.enabled; + } +#endif +}; + +namespace priv { + +template +void set_ssl_option(SslOptions& opts, T&& t) { + opts.SetOption(std::forward(t)); +} + +template +void set_ssl_option(SslOptions& opts, T&& t, Ts&&... ts) { + set_ssl_option(opts, std::forward(t)); + set_ssl_option(opts, std::move(ts)...); +} + +} // namespace priv + +template +SslOptions Ssl(Ts&&... ts) { + SslOptions opts; + priv::set_ssl_option(opts, std::move(ts)...); + return opts; +} + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/status_codes.h b/3rdparty/cpr/include/cpr/status_codes.h new file mode 100644 index 0000000000000000000000000000000000000000..3a2e9cbf8db9196fc00df90525498918578ec9d3 --- /dev/null +++ b/3rdparty/cpr/include/cpr/status_codes.h @@ -0,0 +1,100 @@ +#ifndef CPR_STATUS_CODES +#define CPR_STATUS_CODES +#include +namespace cpr { +namespace status { +// Information responses +constexpr std::int32_t HTTP_CONTINUE = 100; +constexpr std::int32_t HTTP_SWITCHING_PROTOCOL = 101; +constexpr std::int32_t HTTP_PROCESSING = 102; +constexpr std::int32_t HTTP_EARLY_HINTS = 103; +// Successful responses +constexpr std::int32_t HTTP_OK = 200; +constexpr std::int32_t HTTP_CREATED = 201; +constexpr std::int32_t HTTP_ACCEPTED = 202; +constexpr std::int32_t HTTP_NON_AUTHORITATIVE_INFORMATION = 203; +constexpr std::int32_t HTTP_NO_CONTENT = 204; +constexpr std::int32_t HTTP_RESET_CONTENT = 205; +constexpr std::int32_t HTTP_PARTIAL_CONTENT = 206; +constexpr std::int32_t HTTP_MULTI_STATUS = 207; +constexpr std::int32_t HTTP_ALREADY_REPORTED = 208; +constexpr std::int32_t HTTP_IM_USED = 226; +// Redirection messages +constexpr std::int32_t HTTP_MULTIPLE_CHOICE = 300; +constexpr std::int32_t HTTP_MOVED_PERMANENTLY = 301; +constexpr std::int32_t HTTP_FOUND = 302; +constexpr std::int32_t HTTP_SEE_OTHER = 303; +constexpr std::int32_t HTTP_NOT_MODIFIED = 304; +constexpr std::int32_t HTTP_USE_PROXY = 305; +constexpr std::int32_t HTTP_UNUSED = 306; +constexpr std::int32_t HTTP_TEMPORARY_REDIRECT = 307; +constexpr std::int32_t HTTP_PERMANENT_REDIRECT = 308; +// Client error responses +constexpr std::int32_t HTTP_BAD_REQUEST = 400; +constexpr std::int32_t HTTP_UNAUTHORIZED = 401; +constexpr std::int32_t HTTP_PAYMENT_REQUIRED = 402; +constexpr std::int32_t HTTP_FORBIDDEN = 403; +constexpr std::int32_t HTTP_NOT_FOUND = 404; +constexpr std::int32_t HTTP_METHOD_NOT_ALLOWED = 405; +constexpr std::int32_t HTTP_NOT_ACCEPTABLE = 406; +constexpr std::int32_t HTTP_PROXY_AUTHENTICATION_REQUIRED = 407; +constexpr std::int32_t HTTP_REQUEST_TIMEOUT = 408; +constexpr std::int32_t HTTP_CONFLICT = 409; +constexpr std::int32_t HTTP_GONE = 410; +constexpr std::int32_t HTTP_LENGTH_REQUIRED = 411; +constexpr std::int32_t HTTP_PRECONDITION_FAILED = 412; +constexpr std::int32_t HTTP_PAYLOAD_TOO_LARGE = 413; +constexpr std::int32_t HTTP_URI_TOO_LONG = 414; +constexpr std::int32_t HTTP_UNSUPPORTED_MEDIA_TYPE = 415; +constexpr std::int32_t HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; +constexpr std::int32_t HTTP_EXPECTATION_FAILED = 417; +constexpr std::int32_t HTTP_IM_A_TEAPOT = 418; +constexpr std::int32_t HTTP_MISDIRECTED_REQUEST = 421; +constexpr std::int32_t HTTP_UNPROCESSABLE_ENTITY = 422; +constexpr std::int32_t HTTP_LOCKED = 423; +constexpr std::int32_t HTTP_FAILED_DEPENDENCY = 424; +constexpr std::int32_t HTTP_TOO_EARLY = 425; +constexpr std::int32_t HTTP_UPGRADE_REQUIRED = 426; +constexpr std::int32_t HTTP_PRECONDITION_REQUIRED = 428; +constexpr std::int32_t HTTP_TOO_MANY_REQUESTS = 429; +constexpr std::int32_t HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; +constexpr std::int32_t HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451; +// Server response errors +constexpr std::int32_t HTTP_INTERNAL_SERVER_ERROR = 500; +constexpr std::int32_t HTTP_NOT_IMPLEMENTED = 501; +constexpr std::int32_t HTTP_BAD_GATEWAY = 502; +constexpr std::int32_t HTTP_SERVICE_UNAVAILABLE = 503; +constexpr std::int32_t HTTP_GATEWAY_TIMEOUT = 504; +constexpr std::int32_t HTTP_HTTP_VERSION_NOT_SUPPORTED = 505; +constexpr std::int32_t HTTP_VARIANT_ALSO_NEGOTIATES = 506; +constexpr std::int32_t HTTP_INSUFFICIENT_STORAGE = 507; +constexpr std::int32_t HTTP_LOOP_DETECTED = 508; +constexpr std::int32_t HTTP_NOT_EXTENDED = 510; +constexpr std::int32_t HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; + +constexpr std::int32_t INFO_CODE_OFFSET = 100; +constexpr std::int32_t SUCCESS_CODE_OFFSET = 200; +constexpr std::int32_t REDIRECT_CODE_OFFSET = 300; +constexpr std::int32_t CLIENT_ERROR_CODE_OFFSET = 400; +constexpr std::int32_t SERVER_ERROR_CODE_OFFSET = 500; +constexpr std::int32_t MISC_CODE_OFFSET = 600; + +constexpr bool is_informational(const std::int32_t code) { + return (code >= INFO_CODE_OFFSET && code < SUCCESS_CODE_OFFSET); +} +constexpr bool is_success(const std::int32_t code) { + return (code >= SUCCESS_CODE_OFFSET && code < REDIRECT_CODE_OFFSET); +} +constexpr bool is_redirect(const std::int32_t code) { + return (code >= REDIRECT_CODE_OFFSET && code < CLIENT_ERROR_CODE_OFFSET); +} +constexpr bool is_client_error(const std::int32_t code) { + return (code >= CLIENT_ERROR_CODE_OFFSET && code < SERVER_ERROR_CODE_OFFSET); +} +constexpr bool is_server_error(const std::int32_t code) { + return (code >= SERVER_ERROR_CODE_OFFSET && code < MISC_CODE_OFFSET); +} + +} // namespace status +} // namespace cpr +#endif \ No newline at end of file diff --git a/3rdparty/cpr/include/cpr/threadpool.h b/3rdparty/cpr/include/cpr/threadpool.h new file mode 100644 index 0000000000000000000000000000000000000000..ec402a422fcc41fcdbcbd2f292b51e7110b6e21f --- /dev/null +++ b/3rdparty/cpr/include/cpr/threadpool.h @@ -0,0 +1,138 @@ +#ifndef CPR_THREAD_POOL_H +#define CPR_THREAD_POOL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CPR_DEFAULT_THREAD_POOL_MAX_THREAD_NUM std::thread::hardware_concurrency() + +constexpr size_t CPR_DEFAULT_THREAD_POOL_MIN_THREAD_NUM = 1; +constexpr std::chrono::milliseconds CPR_DEFAULT_THREAD_POOL_MAX_IDLE_TIME{250}; + +namespace cpr { + +class ThreadPool { + public: + using Task = std::function; + + explicit ThreadPool(size_t min_threads = CPR_DEFAULT_THREAD_POOL_MIN_THREAD_NUM, size_t max_threads = CPR_DEFAULT_THREAD_POOL_MAX_THREAD_NUM, std::chrono::milliseconds max_idle_ms = CPR_DEFAULT_THREAD_POOL_MAX_IDLE_TIME); + ThreadPool(const ThreadPool& other) = delete; + ThreadPool(ThreadPool&& old) = delete; + + virtual ~ThreadPool(); + + ThreadPool& operator=(const ThreadPool& other) = delete; + ThreadPool& operator=(ThreadPool&& old) = delete; + + void SetMinThreadNum(size_t min_threads) { + min_thread_num = min_threads; + } + + void SetMaxThreadNum(size_t max_threads) { + max_thread_num = max_threads; + } + + void SetMaxIdleTime(std::chrono::milliseconds ms) { + max_idle_time = ms; + } + + size_t GetCurrentThreadNum() { + return cur_thread_num; + } + + size_t GetIdleThreadNum() { + return idle_thread_num; + } + + bool IsStarted() { + return status != STOP; + } + + bool IsStopped() { + return status == STOP; + } + + int Start(size_t start_threads = 0); + int Stop(); + int Pause(); + int Resume(); + int Wait(); + + /** + * Return a future, calling future.get() will wait task done and return RetType. + * Submit(fn, args...) + * Submit(std::bind(&Class::mem_fn, &obj)) + * Submit(std::mem_fn(&Class::mem_fn, &obj)) + **/ + template + auto Submit(Fn&& fn, Args&&... args) { + if (status == STOP) { + Start(); + } + if (idle_thread_num <= 0 && cur_thread_num < max_thread_num) { + CreateThread(); + } + using RetType = decltype(fn(args...)); + auto task = std::make_shared>([fn = std::forward(fn), args...]() mutable { return std::invoke(fn, args...); }); + std::future future = task->get_future(); + { + std::lock_guard locker(task_mutex); + tasks.emplace([task] { (*task)(); }); + } + + task_cond.notify_one(); + return future; + } + + private: + bool CreateThread(); + void AddThread(std::thread* thread); + void DelThread(std::thread::id id); + + public: + size_t min_thread_num; + size_t max_thread_num; + std::chrono::milliseconds max_idle_time; + + private: + enum Status { + STOP, + RUNNING, + PAUSE, + }; + + struct ThreadData { + std::shared_ptr thread; + std::thread::id id; + Status status; + std::chrono::steady_clock::time_point start_time; + std::chrono::steady_clock::time_point stop_time; + }; + + std::atomic status{Status::STOP}; + std::condition_variable status_wait_cond{}; + std::mutex status_wait_mutex{}; + + std::atomic cur_thread_num{0}; + std::atomic idle_thread_num{0}; + + std::list threads{}; + std::mutex thread_mutex{}; + + std::queue tasks{}; + std::mutex task_mutex{}; + std::condition_variable task_cond{}; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/timeout.h b/3rdparty/cpr/include/cpr/timeout.h new file mode 100644 index 0000000000000000000000000000000000000000..4ae103d08b068ee58cbf9f41abaa00fc8252055d --- /dev/null +++ b/3rdparty/cpr/include/cpr/timeout.h @@ -0,0 +1,29 @@ +#ifndef CPR_TIMEOUT_H +#define CPR_TIMEOUT_H + +#include +#include + +namespace cpr { + +class Timeout { + public: + // Template constructor to accept any chrono duration type and convert it to milliseconds + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + template + Timeout(const std::chrono::duration& duration) + : ms{std::chrono::duration_cast(duration)} {} + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Timeout(const std::int32_t& milliseconds) : Timeout{std::chrono::milliseconds(milliseconds)} {} + + // No way around since curl uses a long here. + // NOLINTNEXTLINE(google-runtime-int) + long Milliseconds() const; + + std::chrono::milliseconds ms; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/unix_socket.h b/3rdparty/cpr/include/cpr/unix_socket.h new file mode 100644 index 0000000000000000000000000000000000000000..152caf0caba5d77c63e50d672aa6003a4fad4f00 --- /dev/null +++ b/3rdparty/cpr/include/cpr/unix_socket.h @@ -0,0 +1,21 @@ +#ifndef CPR_UNIX_SOCKET_H +#define CPR_UNIX_SOCKET_H + +#include + +namespace cpr { + +class UnixSocket { + public: + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + UnixSocket(std::string unix_socket) : unix_socket_(std::move(unix_socket)) {} + + const char* GetUnixSocketString() const noexcept; + + private: + const std::string unix_socket_; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/user_agent.h b/3rdparty/cpr/include/cpr/user_agent.h new file mode 100644 index 0000000000000000000000000000000000000000..a3cc1293d59bdcf0be57c86f1046957dd1c01934 --- /dev/null +++ b/3rdparty/cpr/include/cpr/user_agent.h @@ -0,0 +1,31 @@ +#ifndef CPR_USERAGENT_H +#define CPR_USERAGENT_H + +#include +#include + +#include "cpr/cprtypes.h" + +namespace cpr { +class UserAgent : public StringHolder { + public: + UserAgent() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + UserAgent(std::string useragent) : StringHolder(std::move(useragent)) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + UserAgent(std::string_view useragent) : StringHolder(useragent) {} + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + UserAgent(const char* useragent) : StringHolder(useragent) {} + UserAgent(const char* str, size_t len) : StringHolder(str, len) {} + UserAgent(const std::initializer_list args) : StringHolder(args) {} + UserAgent(const UserAgent& other) = default; + UserAgent(UserAgent&& old) noexcept = default; + ~UserAgent() override = default; + + UserAgent& operator=(UserAgent&& old) noexcept = default; + UserAgent& operator=(const UserAgent& other) = default; +}; + +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/include/cpr/util.h b/3rdparty/cpr/include/cpr/util.h new file mode 100644 index 0000000000000000000000000000000000000000..cc3d819236d042aea3f6fb39a2461c526e2fd4b9 --- /dev/null +++ b/3rdparty/cpr/include/cpr/util.h @@ -0,0 +1,53 @@ +#ifndef CPR_UTIL_H +#define CPR_UTIL_H + +#include +#include +#include + +#include "cpr/callback.h" +#include "cpr/cookies.h" +#include "cpr/cprtypes.h" +#include "cpr/curlholder.h" + +namespace cpr::util { + +Header parseHeader(const std::string& headers, std::string* status_line = nullptr, std::string* reason = nullptr); +Cookies parseCookies(curl_slist* raw_cookies); +size_t readUserFunction(char* ptr, size_t size, size_t nitems, const ReadCallback* read); +size_t headerUserFunction(char* ptr, size_t size, size_t nmemb, const HeaderCallback* header); +size_t writeFunction(char* ptr, size_t size, size_t nmemb, std::string* data); +size_t writeFileFunction(char* ptr, size_t size, size_t nmemb, std::ofstream* file); +size_t writeUserFunction(char* ptr, size_t size, size_t nmemb, const WriteCallback* write); + +template +int progressUserFunction(const T* progress, cpr_pf_arg_t dltotal, cpr_pf_arg_t dlnow, cpr_pf_arg_t ultotal, cpr_pf_arg_t ulnow) { + const int cancel_retval{1}; +#ifdef CURL_PROGRESSFUNC_CONTINUE // Not always defined. Ref: https://github.com/libcpr/cpr/issues/932 + static_assert(cancel_retval != CURL_PROGRESSFUNC_CONTINUE); +#endif // CURL_PROGRESSFUNC_CONTINUE + return (*progress)(dltotal, dlnow, ultotal, ulnow) ? 0 : cancel_retval; +} +int debugUserFunction(CURL* handle, curl_infotype type, char* data, size_t size, const DebugCallback* debug); +std::vector split(const std::string& to_split, char delimiter); +std::string urlEncode(const std::string& s); +std::string urlDecode(const std::string& s); + +/** + * Override the content of the provided string to hide sensitive data. The + * string content after invocation is undefined. The string size is reset to zero. + * impl. based on: + * https://github.com/ojeda/secure_clear/blob/master/example-implementation/secure_clear.h + **/ +void secureStringClear(std::string& s); +bool isTrue(const std::string& s); + +/** + * Parses the given std::string into time_t (unix ms). + * This parsing happens time_t size agnostic since time_t does not use the same underlying type on all systems/compilers. + **/ +time_t sTimestampToT(const std::string&); + +} // namespace cpr::util + +#endif diff --git a/3rdparty/cpr/include/cpr/verbose.h b/3rdparty/cpr/include/cpr/verbose.h new file mode 100644 index 0000000000000000000000000000000000000000..2bf0df8f0b741cd150261b6093583e23b9d108d8 --- /dev/null +++ b/3rdparty/cpr/include/cpr/verbose.h @@ -0,0 +1,18 @@ +#ifndef CPR_VERBOSE_H_ +#define CPR_VERBOSE_H_ + +namespace cpr { + +class Verbose { + public: + Verbose() = default; + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Verbose(const bool p_verbose) : verbose{p_verbose} {} + + bool verbose = true; +}; + +} // namespace cpr + + +#endif /* CPR_VERBOSE_H_ */ diff --git a/3rdparty/cpr/nuget/libcpr.nuspec b/3rdparty/cpr/nuget/libcpr.nuspec new file mode 100644 index 0000000000000000000000000000000000000000..1169ebf4e81f903b5d27d791d572ca36fbaa54fa --- /dev/null +++ b/3rdparty/cpr/nuget/libcpr.nuspec @@ -0,0 +1,19 @@ + + + + libcpr + $VERSION$ + C++ Requests: Curl for People + Simon Berger + Fabian Sauter, Kilian Traub, many other libcpr contributors + false + MIT + resources/cpr.png + README.md + https://github.com/libcpr + C++ Requests: Curl for People, a spiritual port of Python Requests. + Native, native + english + + + \ No newline at end of file diff --git a/3rdparty/cpr/nuget/resources/cpr.png b/3rdparty/cpr/nuget/resources/cpr.png new file mode 100644 index 0000000000000000000000000000000000000000..61ad790107c60a623b12ecb33a01f350caae0c4d Binary files /dev/null and b/3rdparty/cpr/nuget/resources/cpr.png differ diff --git a/3rdparty/cpr/package-build/build-package.sh b/3rdparty/cpr/package-build/build-package.sh new file mode 100755 index 0000000000000000000000000000000000000000..eef22da954ba906c3262cd4d4be6a0671cc088b6 --- /dev/null +++ b/3rdparty/cpr/package-build/build-package.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +SRC_DIR=$1 + +LIB='libcpr' + +LIB_DIR="${LIB}_${VERSION}" +DEBIAN_DIR="${LIB_DIR}/debian" + +ARCHIVE_NAME="$LIB_DIR.orig.tar.gz" + +echo -e "Preparing tar archive and directory\n" +cp -r $SRC_DIR $LIB_DIR + +tar --exclude-vcs -czf $ARCHIVE_NAME $LIB_DIR +tar -xzf $ARCHIVE_NAME + +cd $LIB_DIR + +echo -e "\n\nCopying prepared debian files to directory\n" +mkdir debian +cp -r package-build/debian-libcpr/* debian/ +sed -i "s/\%VERSION/$VERSION/g" debian/changelog +sed -i "s/\%DATE/$(date -R)/g" debian/changelog + +echo -e "\n\nCalling debuild\n" +debuild diff --git a/3rdparty/cpr/package-build/debian-libcpr/README.Debian b/3rdparty/cpr/package-build/debian-libcpr/README.Debian new file mode 100644 index 0000000000000000000000000000000000000000..e159461a5b5df93b9d61c8bb5baf4ebc9eef9648 --- /dev/null +++ b/3rdparty/cpr/package-build/debian-libcpr/README.Debian @@ -0,0 +1,5 @@ +libcpr for Debian + +A package of the libcpr library. + + -- Philip Saendig Tue, 24 May 2022 10:37:24 +0200 diff --git a/3rdparty/cpr/package-build/debian-libcpr/changelog b/3rdparty/cpr/package-build/debian-libcpr/changelog new file mode 100644 index 0000000000000000000000000000000000000000..dbe84a94f8b59918450f02a7d2471dbdbbe36733 --- /dev/null +++ b/3rdparty/cpr/package-build/debian-libcpr/changelog @@ -0,0 +1,6 @@ +libcpr (%VERSION-1) UNRELEASED; urgency=low + + [ Philip Saendig ] + * First package of libcpr %VERSION for debian. + + -- Philip Saendig %DATE diff --git a/3rdparty/cpr/package-build/debian-libcpr/control b/3rdparty/cpr/package-build/debian-libcpr/control new file mode 100644 index 0000000000000000000000000000000000000000..e4526b1c56c66a03ab91d8f4fd996d231b61be2f --- /dev/null +++ b/3rdparty/cpr/package-build/debian-libcpr/control @@ -0,0 +1,39 @@ +Source: libcpr +Section: libs +Priority: optional +Maintainer: Philip Saendig +Build-Depends: + debhelper-compat (= 12), + cmake, + libcurl4-openssl-dev, + libssl-dev, +Standards-Version: 4.5.0 +Homepage: https://github.com/libcpr/cpr + +Package: libcpr-dev +Architecture: any +Multi-Arch: same +Pre-Depends: ${misc:Pre-Depends} +Depends: ${misc:Depends}, ${shlibs:Depends}, libcpr1 +Description: C++ wrapper around the libcurl library - development kit + This package contains the header files and development + libraries of cpr, Curl for People. + . + The project is inspried by the Python Request project. + Using the more expressive language facilities of C++11, + it captures the essence of making network calls into a + few concise idioms. + +Package: libcpr1 +Architecture: any +Multi-Arch: same +Pre-Depends: ${misc:Pre-Depends} +Depends: ${misc:Depends}, ${shlibs:Depends}, +Description: C++ wrapper around the libcurl library - runtime library + This package contains the runtime, shared library of cpr, + Curl for People. + . + The project is inspried by the Python Request project. + Using the more expressive language facilities of C++11, + it captures the essence of making network calls into a + few concise idioms. diff --git a/3rdparty/cpr/package-build/debian-libcpr/copyright b/3rdparty/cpr/package-build/debian-libcpr/copyright new file mode 100644 index 0000000000000000000000000000000000000000..33b7ffc230a1e22450dc581f99677a8125a81eec --- /dev/null +++ b/3rdparty/cpr/package-build/debian-libcpr/copyright @@ -0,0 +1,52 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: libcpr +Source: https://github.com/libcpr/cpr + +Files: .clang-format + .clang-tidy + .github/* + CMakeLists.txt + CODE_OF_CONDUCT.md + CONTRIBUTING.md + CppCheckSuppressions.txt + README.md + cmake/* + cpr-config.cmake + cpr/* + include/* + nuget/* + package-build/* + debian/* +Copyright: 2017-2021 Huu Nguyen + 2022 libcpr and many other contributors +License: Expat + MIT License + . + Copyright (c) 2017-2021 Huu Nguyen + Copyright (c) 2022 libcpr and many other contributors + . + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +Files: test/* +Copyright: 2022 libcpr and many other contributors +License: GPL-3 + On Debian systems, the full text of the GNU General Public + License version 3 can be found in the file + `/usr/share/common-licenses/GPL-3'. + diff --git a/3rdparty/cpr/package-build/debian-libcpr/libcpr-dev.install b/3rdparty/cpr/package-build/debian-libcpr/libcpr-dev.install new file mode 100644 index 0000000000000000000000000000000000000000..bb722673ac60df5bc155f6865e8e993f3399d50b --- /dev/null +++ b/3rdparty/cpr/package-build/debian-libcpr/libcpr-dev.install @@ -0,0 +1,3 @@ +usr/include +usr/lib/*/*.so +usr/lib/*/cmake diff --git a/3rdparty/cpr/package-build/debian-libcpr/libcpr1.install b/3rdparty/cpr/package-build/debian-libcpr/libcpr1.install new file mode 100644 index 0000000000000000000000000000000000000000..3de3b10a49844edfe7e12e2fde3eb1357306fbfc --- /dev/null +++ b/3rdparty/cpr/package-build/debian-libcpr/libcpr1.install @@ -0,0 +1 @@ +usr/lib/*/*.so.* diff --git a/3rdparty/cpr/package-build/debian-libcpr/rules b/3rdparty/cpr/package-build/debian-libcpr/rules new file mode 100755 index 0000000000000000000000000000000000000000..11b4eb1c20752bfa46c2ddf3dfbac9cd309246ef --- /dev/null +++ b/3rdparty/cpr/package-build/debian-libcpr/rules @@ -0,0 +1,14 @@ +#!/usr/bin/make -f +export DH_VERBOSE = 1 +export DEB_BUILD_MAINT_OPTIONS = hardening=+all +export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic +export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed + +%: + dh $@ + +override_dh_auto_configure: + dh_auto_configure -- \ + -DCMAKE_LIBRARY_ARCHITECTURE="$(DEB_TARGET_MULTIARCH)" -DCMAKE_BUILD_TYPE=Release \ + -DCPR_USE_SYSTEM_CURL=ON -DCURL_ZLIB=OFF -DBUILD_SHARED_LIBS=ON + diff --git a/3rdparty/cpr/package-build/debian-libcpr/source/format b/3rdparty/cpr/package-build/debian-libcpr/source/format new file mode 100644 index 0000000000000000000000000000000000000000..163aaf8d82b6c54f23c45f32895dbdfdcc27b047 --- /dev/null +++ b/3rdparty/cpr/package-build/debian-libcpr/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/3rdparty/cpr/scripts/check_clang_format.sh b/3rdparty/cpr/scripts/check_clang_format.sh new file mode 100755 index 0000000000000000000000000000000000000000..488c45e7989203803e5b3696b384db03950c086d --- /dev/null +++ b/3rdparty/cpr/scripts/check_clang_format.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Based on: https://gist.github.com/leilee/1d0915a583f8f29414cc21cd86e7151b +# Checks if all files are formatted based on the clang-format formatting rules. +# Execute as follows: +# ./scripts/check_clang_format.sh tests src + +printf "📑 Checking if your code fulfills all clang-format rules...\n" + +RET_CODE=0 + +function format() { + for f in $(find $@ -name '*.h' -or -name '*.hpp' -or -name '*.c' -or -name '*.cpp'); do + clang-format -i --dry-run --Werror --style=file ${f}; + ret=$? + if [ $ret -ne 0 ]; then + RET_CODE=$ret + fi + done + + echo "~~~ $@ directory checked ~~~"; +} + +# Check all of the arguments first to make sure they're all directories +for dir in "$@"; do + if [ ! -d "${dir}" ]; then + echo "${dir} is not a directory"; + else + format ${dir}; + fi +done + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +if [ $RET_CODE -eq 0 ]; then + printf "✅ ${GREEN}Everything up to standard :party: ${NC}\n" +else + printf "❌ ${RED}Not up to formatting standard :sad_face: ${NC}\n" + echo "Try running run_clang_format.sh to format all files." +fi + +exit $RET_CODE \ No newline at end of file diff --git a/3rdparty/cpr/scripts/delete_build_dir.sh b/3rdparty/cpr/scripts/delete_build_dir.sh new file mode 100755 index 0000000000000000000000000000000000000000..52d0215684deeba1e20ceb2c70f97303ce6d235c --- /dev/null +++ b/3rdparty/cpr/scripts/delete_build_dir.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +printf "🗑️ Clearing your build directory...\n" +rm -rf build/* + +GREEN='\033[0;32m' +NC='\033[0m' + +printf "✅ ${GREEN}Done. Build directory deleted.${NC}\n" \ No newline at end of file diff --git a/3rdparty/cpr/scripts/run_clang_format.sh b/3rdparty/cpr/scripts/run_clang_format.sh new file mode 100755 index 0000000000000000000000000000000000000000..edf11659959737691981a7c30ff05473440b0362 --- /dev/null +++ b/3rdparty/cpr/scripts/run_clang_format.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Based on: https://gist.github.com/leilee/1d0915a583f8f29414cc21cd86e7151b +# Run from the project root directory as follows: +# ./scripts/run_clang_format.sh tests src + +printf "📝 Running clang-format...\n" + +function format() { + for f in $(find $@ -name '*.h' -or -name '*.hpp' -or -name '*.c' -or -name '*.cpp'); do + echo "format ${f}"; + clang-format -i --style=file ${f}; + done + + echo "~~~ $@ directory formatted ~~~"; +} + +# Check all of the arguments first to make sure they're all directories +for dir in "$@"; do + if [ ! -d "${dir}" ]; then + echo "${dir} is not a directory"; + else + format ${dir}; + fi +done + +GREEN='\033[0;32m' +NC='\033[0m' + +printf "✅ ${GREEN}Done. All files were formatted (if required).${NC}\n" diff --git a/3rdparty/cpr/test/CMakeLists.txt b/3rdparty/cpr/test/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..d78ff8229aa91c0a758789ab335ac48a506efb6a --- /dev/null +++ b/3rdparty/cpr/test/CMakeLists.txt @@ -0,0 +1,91 @@ +cmake_minimum_required(VERSION 3.15) + +find_package(Threads REQUIRED) + +if (ENABLE_SSL_TESTS) + add_library(test_server STATIC + abstractServer.cpp + httpServer.cpp + httpsServer.cpp) +else () + add_library(test_server STATIC + abstractServer.cpp + httpServer.cpp) +endif() +if(WIN32) + target_link_libraries(test_server PRIVATE Threads::Threads cpr::cpr GTest::GTest + PUBLIC mongoose ws2_32 wsock32) +else() + target_link_libraries(test_server PRIVATE Threads::Threads cpr::cpr GTest::GTest + PUBLIC mongoose) +endif() + +macro(add_cpr_test _TEST_NAME) + add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests.cpp) + target_link_libraries(${_TEST_NAME}_tests PRIVATE + test_server + GTest::GTest + cpr::cpr + ${CURL_LIB}) + add_test(NAME cpr_${_TEST_NAME}_tests COMMAND ${_TEST_NAME}_tests) + # Group under the "tests" project folder in IDEs such as Visual Studio. + set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") + if(WIN32 AND BUILD_SHARED_LIBS) + add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ $) + add_custom_command(TARGET ${_TEST_NAME}_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ $) + endif() +endmacro() + +add_cpr_test(get) +add_cpr_test(post) +add_cpr_test(session) +add_cpr_test(prepare) +add_cpr_test(async) +if(CPR_BUILD_TESTS_PROXY) + add_cpr_test(proxy) + add_cpr_test(proxy_auth) +endif() +add_cpr_test(head) +add_cpr_test(delete) +add_cpr_test(put) +add_cpr_test(callback) +add_cpr_test(raw_body) +add_cpr_test(options) +add_cpr_test(patch) +add_cpr_test(error) +add_cpr_test(alternating) +add_cpr_test(util) +add_cpr_test(structures) +add_cpr_test(encoded_auth) +add_cpr_test(version) +add_cpr_test(download) +add_cpr_test(interceptor) +add_cpr_test(interceptor_multi) +add_cpr_test(multiperform) +add_cpr_test(resolve) +add_cpr_test(multiasync) +add_cpr_test(file_upload) +add_cpr_test(singleton) +add_cpr_test(threadpool) + +if (ENABLE_SSL_TESTS) + add_cpr_test(ssl) + + # Install all ssl keys and certs. Explicit copy for each file to prevent issues when copying on Windows. + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory $/data/certificates $/data/keys) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/certificates/client.crt $/data/certificates/client.crt) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/certificates/root-ca.crt $/data/certificates/root-ca.crt) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/certificates/sub-ca.crt $/data/certificates/sub-ca.crt) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/certificates/ca-bundle.crt $/data/certificates/ca-bundle.crt) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/certificates/server.crt $/data/certificates/server.crt) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/keys/client.key $/data/keys/client.key) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/keys/root-ca.key $/data/keys/root-ca.key) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/keys/server.key $/data/keys/server.key) + add_custom_command(TARGET ssl_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/keys/server.pub $/data/keys/server.pub) +endif() + +add_custom_command(TARGET file_upload_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/test_file_hello_äüöp_2585_你好.txt $/data/test_file_hello_äüöp_2585_你好.txt) +add_custom_command(TARGET file_upload_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/test_file_hello_äüöp_2585.txt $/data/test_file_hello_äüöp_2585.txt) +add_custom_command(TARGET file_upload_tests POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/data/test_file.txt $/data/test_file.txt) + +file(INSTALL data DESTINATION data) diff --git a/3rdparty/cpr/test/LICENSE b/3rdparty/cpr/test/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..4d188aa7fde3a48a216e5c281693bafbca808b4e --- /dev/null +++ b/3rdparty/cpr/test/LICENSE @@ -0,0 +1,677 @@ +This license applies to everything inside this directory and all +subdirectories. + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/3rdparty/cpr/test/abstractServer.cpp b/3rdparty/cpr/test/abstractServer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e32394b5a37b98f990fdf3a6358adbf24d9155aa --- /dev/null +++ b/3rdparty/cpr/test/abstractServer.cpp @@ -0,0 +1,143 @@ +#include "abstractServer.hpp" + +namespace cpr { +void AbstractServer::SetUp() { + Start(); +} + +void AbstractServer::TearDown() { + Stop(); +} + +void AbstractServer::Start() { + should_run = true; + serverThread = std::make_shared(&AbstractServer::Run, this); + serverThread->detach(); + std::unique_lock server_lock(server_mutex); + server_start_cv.wait(server_lock); +} + +void AbstractServer::Stop() { + should_run = false; + std::unique_lock server_lock(server_mutex); + server_stop_cv.wait(server_lock); +} + +static void EventHandler(mg_connection* conn, int event, void* event_data, void* context) { + switch (event) { + case MG_EV_READ: + case MG_EV_WRITE: + /** Do nothing. Just for housekeeping. **/ + break; + case MG_EV_POLL: + /** Do nothing. Just for housekeeping. **/ + break; + case MG_EV_CLOSE: + /** Do nothing. Just for housekeeping. **/ + break; + case MG_EV_ACCEPT: + /* Initialize HTTPS connection if Server is an HTTPS Server */ + static_cast(context)->acceptConnection(conn); + break; + case MG_EV_CONNECT: + /** Do nothing. Just for housekeeping. **/ + break; + + case MG_EV_HTTP_CHUNK: { + /** Do nothing. Just for housekeeping. **/ + } break; + + case MG_EV_HTTP_MSG: { + AbstractServer* server = static_cast(context); + server->OnRequest(conn, static_cast(event_data)); + } break; + + default: + break; + } +} + +void AbstractServer::Run() { + // Setup a new mongoose http server. + memset(&mgr, 0, sizeof(mg_mgr)); + initServer(&mgr, EventHandler); + + // Notify the main thread that the server is up and runing: + server_start_cv.notify_all(); + + // Main server loop: + while (should_run) { + // NOLINTNEXTLINE (cppcoreguidelines-avoid-magic-numbers) + mg_mgr_poll(&mgr, 100); + } + + // Shutdown and cleanup: + timer_args.clear(); + mg_mgr_free(&mgr); + + // Notify the main thread that we have shut down everything: + server_stop_cv.notify_all(); +} + +static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; +/** + * Decodes the given BASE64 string to a normal string. + * Source: https://gist.github.com/williamdes/308b95ac9ef1ee89ae0143529c361d37 + **/ +std::string AbstractServer::Base64Decode(const std::string& in) { + std::string out; + + std::vector T(256, -1); + for (size_t i = 0; i < 64; i++) + T[base64_chars[i]] = static_cast(i); + + int val = 0; + int valb = -8; + for (unsigned char c : in) { + if (T[c] == -1) { + break; + } + val = (val << 6) + T[c]; + valb += 6; + if (valb >= 0) { + out.push_back(char((val >> valb) & 0xFF)); + valb -= 8; + } + } + return out; +} + +// Sends error similar like in mongoose 6 method mg_http_send_error +// https://github.com/cesanta/mongoose/blob/6.18/mongoose.c#L7081-L7089 +void AbstractServer::SendError(mg_connection* conn, int code, std::string& reason) { + std::string headers{"Content-Type: text/plain\r\nConnection: close\r\n"}; + mg_http_reply(conn, code, headers.c_str(), reason.c_str()); +} + +// Checks whether a pointer to a connection is still managed by a mg_mgr. +// This check tells whether it is still possible to send a message via the given connection +// Note that it is still possible that the pointer of an old connection object may be reused by mongoose. +// In this case, the active connection might refer to a different connection than the one the caller refers to +bool AbstractServer::IsConnectionActive(mg_mgr* mgr, mg_connection* conn) { + mg_connection* c{mgr->conns}; + while (c) { + if (c == conn) { + return true; + } + c = c->next; + } + return false; +} + +uint16_t AbstractServer::GetRemotePort(const mg_connection* conn) { + return (conn->rem.port >> 8) | (conn->rem.port << 8); +} + +uint16_t AbstractServer::GetLocalPort(const mg_connection* conn) { + return (conn->loc.port >> 8) | (conn->loc.port << 8); +} + +} // namespace cpr diff --git a/3rdparty/cpr/test/abstractServer.hpp b/3rdparty/cpr/test/abstractServer.hpp new file mode 100644 index 0000000000000000000000000000000000000000..d2daec26d5dd72f92b02b5674c0da8cc223c3f3b --- /dev/null +++ b/3rdparty/cpr/test/abstractServer.hpp @@ -0,0 +1,70 @@ +#ifndef CPR_TEST_ABSTRACT_SERVER_SERVER_H +#define CPR_TEST_ABSTRACT_SERVER_SERVER_H + +#include +#include +#include +#include +#include +#include + +#include "cpr/cpr.h" +#include "mongoose.h" + +namespace cpr { + +// Helper struct for functions using timers to simulate slow connections +struct TimerArg { + mg_mgr* mgr; + mg_connection* connection; + unsigned long connection_id; + mg_timer timer; + unsigned counter; + + explicit TimerArg(mg_mgr* m, mg_connection* c, mg_timer&& t) : mgr{m}, connection{c}, connection_id{0}, timer{t}, counter{0} {} + + ~TimerArg() { + mg_timer_free(&mgr->timers, &timer); + } +}; + +class AbstractServer : public testing::Environment { + public: + ~AbstractServer() override = default; + + void SetUp() override; + void TearDown() override; + + void Start(); + void Stop(); + + virtual std::string GetBaseUrl() = 0; + virtual uint16_t GetPort() = 0; + + virtual void acceptConnection(mg_connection* conn) = 0; + virtual void OnRequest(mg_connection* conn, mg_http_message* msg) = 0; + + private: + std::shared_ptr serverThread{nullptr}; + std::mutex server_mutex; + std::condition_variable server_start_cv; + std::condition_variable server_stop_cv; + std::atomic should_run{false}; + + void Run(); + + protected: + mg_mgr mgr{}; + std::vector> timer_args{}; + virtual mg_connection* initServer(mg_mgr* mgr, mg_event_handler_t event_handler) = 0; + + static std::string Base64Decode(const std::string& in); + static void SendError(mg_connection* conn, int code, std::string& reason); + static bool IsConnectionActive(mg_mgr* mgr, mg_connection* conn); + + static uint16_t GetRemotePort(const mg_connection* conn); + static uint16_t GetLocalPort(const mg_connection* conn); +}; +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/test/alternating_tests.cpp b/3rdparty/cpr/test/alternating_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d6b54775bf8ba1fe1d229cc34fb3f7a4f37621b5 --- /dev/null +++ b/3rdparty/cpr/test/alternating_tests.cpp @@ -0,0 +1,163 @@ +#include + +#include + +#include "cpr/cpr.h" + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(AlternatingTests, PutGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Header reflect PUT"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Response response = cpr::Get(url); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(AlternatingTests, PutGetPutGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Header reflect PUT"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Response response = cpr::Get(url); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Header reflect PUT"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Response response = cpr::Get(url); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(AlternatingTests, HeadGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + + { + // Head shouldn't return a body + Response response = cpr::Head(url); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Response response = cpr::Get(url); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(AlternatingTests, PutHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Header reflect PUT"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + // Head shouldn't return a body + Response response = cpr::Head(url); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(AlternatingTests, PutPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Header reflect PUT"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + + { + Payload payload{{"x", "5"}}; + Response response = cpr::Post(url, payload); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/async_tests.cpp b/3rdparty/cpr/test/async_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..de819ea99146c3a0b3d5cfd219503bb3cc008a09 --- /dev/null +++ b/3rdparty/cpr/test/async_tests.cpp @@ -0,0 +1,81 @@ +#include + +#include +#include + +#include "cpr/cpr.h" +#include "cpr/filesystem.h" + +#include "cpr/api.h" +#include "cpr/response.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +bool write_data(const std::string_view& /*data*/, intptr_t /*userdata*/) { + return true; +} + +TEST(AsyncTests, AsyncGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + cpr::AsyncResponse future = cpr::GetAsync(url); + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); +} + +TEST(AsyncTests, AsyncGetMultipleTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::GetAsync(url)); + } + for (cpr::AsyncResponse& future : responses) { + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + } +} + +TEST(AsyncTests, AsyncGetMultipleReflectTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::vector responses; + for (size_t i = 0; i < 100; ++i) { + Parameters p{{"key", std::to_string(i)}}; + responses.emplace_back(cpr::GetAsync(url, p)); + } + int i = 0; + for (cpr::AsyncResponse& future : responses) { + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + Url expected_url{url + "?key=" + std::to_string(i)}; + EXPECT_EQ(expected_url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + ++i; + } +} + +TEST(AsyncTests, AsyncDownloadTest) { + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::AsyncResponse future = cpr::DownloadAsync(fs::path{"/tmp/aync_download"}, url, cpr::Header{{"Accept-Encoding", "gzip"}}, cpr::WriteCallback{write_data, 0}); + cpr::Response response = future.get(); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/callback_tests.cpp b/3rdparty/cpr/test/callback_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1d35ceff445ffafcaf76e9f3190266521dbefb24 --- /dev/null +++ b/3rdparty/cpr/test/callback_tests.cpp @@ -0,0 +1,934 @@ +#include +#include + +#include +#include +#include +#include + +#include "cpr/cpr.h" + +#include "cpr/cprtypes.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); +std::chrono::milliseconds sleep_time{50}; +std::chrono::seconds zero{0}; + +int status_callback(int& status_code, Response r) { + status_code = r.status_code; + return r.status_code; +} + +int status_callback_ref(int& status_code, const Response& r) { + status_code = r.status_code; + return r.status_code; +} + +std::string text_callback(std::string& expected_text, Response r) { + expected_text = r.text; + return r.text; +} + +std::string text_callback_ref(std::string& expected_text, const Response& r) { + expected_text = r.text; + return r.text; +} + +TEST(CallbackGetTests, CallbackGetLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::GetCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackGetTests, CallbackGetLambdaTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::GetCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackGetTests, CallbackGetLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::GetCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackGetTests, CallbackGetLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::GetCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackGetTests, CallbackGetFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::GetCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackGetTests, CallbackGetFunctionTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::GetCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackGetTests, CallbackGetFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::GetCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackGetTests, CallbackGetFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::GetCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + int status_code = 0; + auto future = cpr::DeleteCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteLambdaTextTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::string expected_text{}; + auto future = cpr::DeleteCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + int status_code = 0; + auto future = cpr::DeleteCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::string expected_text{}; + auto future = cpr::DeleteCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::DeleteCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteFunctionTextTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::DeleteCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::DeleteCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackDeleteTests, CallbackDeleteFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::DeleteCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::HeadCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadLambdaTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::HeadCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::HeadCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::HeadCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::HeadCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadFunctionTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::HeadCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::HeadCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackHeadTests, CallbackHeadFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::HeadCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPostTests, CallbackPostLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + // NOLINTNEXTLINE(google-runtime-int) + long status_code = 0; + auto future = cpr::PostCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPostTests, CallbackPostLambdaTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PostCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPostTests, CallbackPostLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + // NOLINTNEXTLINE(google-runtime-int) + long status_code = 0; + auto future = cpr::PostCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPostTests, CallbackPostLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PostCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPostTests, CallbackPostFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PostCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPostTests, CallbackPostFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PostCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPostTests, CallbackPostFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PostCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPostTests, CallbackPostFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PostCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPutTests, CallbackPutLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto future = cpr::PutCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPutTests, CallbackPutLambdaTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PutCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPutTests, CallbackPutLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto future = cpr::PutCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPutTests, CallbackPutLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PutCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPutTests, CallbackPutFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PutCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPutTests, CallbackPutFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PutCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPutTests, CallbackPutFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PutCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPutTests, CallbackPutFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PutCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::OptionsCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsLambdaTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::OptionsCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto future = cpr::OptionsCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto future = cpr::OptionsCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::OptionsCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsFunctionTextTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::OptionsCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::OptionsCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackOptionsTests, CallbackOptionsFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::OptionsCallback(callback, url); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchLambdaStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto future = cpr::PatchCallback( + [&status_code](Response r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchLambdaTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PatchCallback( + [&expected_text](Response r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchLambdaStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto future = cpr::PatchCallback( + [&status_code](const Response& r) { + status_code = r.status_code; + return r.status_code; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchLambdaTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto future = cpr::PatchCallback( + [&expected_text](const Response& r) { + expected_text = r.text; + return r.text; + }, + url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchFunctionStatusTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PatchCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PatchCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchFunctionStatusReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + int status_code = 0; + auto callback = std::function(std::bind(status_callback_ref, std::ref(status_code), std::placeholders::_1)); + auto future = cpr::PatchCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(status_code, future.get()); +} + +TEST(CallbackPatchTests, CallbackPatchFunctionTextReferenceTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::string expected_text{}; + auto callback = std::function(std::bind(text_callback_ref, std::ref(expected_text), std::placeholders::_1)); + auto future = cpr::PatchCallback(callback, url, payload); + std::this_thread::sleep_for(sleep_time); + EXPECT_EQ(future.wait_for(zero), std::future_status::ready); + EXPECT_EQ(expected_text, future.get()); +} + +TEST(CallbackDataTests, CallbackReadFunctionCancelTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, cpr::ReadCallback([](char* /*buffer*/, size_t& /*size*/, intptr_t /*userdata*/) -> size_t { return false; })); + EXPECT_TRUE((response.error.code == ErrorCode::ABORTED_BY_CALLBACK) || (response.error.code == ErrorCode::WRITE_ERROR)); +} + +TEST(CallbackDataTests, CallbackReadFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + unsigned count = 0; + Response response = cpr::Post(url, cpr::ReadCallback{3, [&](char* buffer, size_t& size, intptr_t /*userdata*/) -> size_t { + std::string data; + ++count; + switch (count) { + case 1: + data = "x="; + break; + case 2: + data = "5"; + break; + default: + return false; + } + std::copy(data.begin(), data.end(), buffer); + size = data.size(); + return true; + }}); + EXPECT_EQ(2, count); + EXPECT_EQ(expected_text, response.text); +} + +TEST(CallbackDataTests, CallbackReadFunctionTextTestPut) { + Url url{server->GetBaseUrl() + "/put.html"}; + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + unsigned count = 0; + Response response = cpr::Put(url, cpr::ReadCallback{3, [&](char* buffer, size_t& size, intptr_t /*userdata*/) -> size_t { + std::string data; + ++count; + switch (count) { + case 1: + data = "x="; + break; + case 2: + data = "5"; + break; + default: + return false; + } + std::copy(data.begin(), data.end(), buffer); + size = data.size(); + return true; + }}); + EXPECT_EQ(2, count); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +/** + * Checks if the "Transfer-Encoding" header will be kept when using headers and a read callback. + * Issue: https://github.com/whoshuu/cpr/issues/517 + **/ +TEST(CallbackDataTests, CallbackReadFunctionHeaderTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::string data = "Test"; + Response response = cpr::Post(url, + cpr::ReadCallback{-1, + [&](char* /*buffer*/, size_t& size, intptr_t /*userdata*/) -> size_t { + size = 0; + return true; + }}, + Header{{"TestHeader", "42"}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + + // Check Header: + EXPECT_EQ(std::string{"42"}, response.header["TestHeader"]); // Set by us + EXPECT_TRUE(response.header.find("TestHeader") != response.header.end()); + EXPECT_EQ(std::string{"chunked"}, response.header["Transfer-Encoding"]); // Set by the read callback + EXPECT_TRUE(response.header.find("Transfer-Encoding") != response.header.end()); +} + +/* cesanta mongoose doesn't support chunked requests yet +TEST(CallbackDataTests, CallbackReadFunctionChunkedTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + unsigned count = 0; + Response response = cpr::Post(url, cpr::ReadCallback{[&count](char* buffer, size_t & size) -> size_t { + std::string data; + ++ count; + switch (count) { + case 1: + data = "x="; + break; + case 2: + data = "5"; + break; + default: + data = ""; + break; + } + std::copy(data.begin(), data.end(), buffer); + size = data.size(); + return true; + }}); + EXPECT_EQ(3, count); + EXPECT_EQ(expected_text, response.text); +} +*/ + +TEST(CallbackDataTests, CallbackHeaderFunctionCancelTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = Post(url, HeaderCallback{[](const std::string_view& /*header*/, intptr_t /*userdata*/) -> bool { return false; }}); + EXPECT_TRUE((response.error.code == ErrorCode::ABORTED_BY_CALLBACK) || (response.error.code == ErrorCode::WRITE_ERROR)); +} + +TEST(CallbackDataTests, CallbackHeaderFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::vector expected_headers{"HTTP/1.1 201 Created\r\n", "Content-Type: application/json\r\n", "\r\n"}; + std::set response_headers; + Post(url, HeaderCallback{[&response_headers](const std::string_view& header, intptr_t /*userdata*/) -> bool { + response_headers.insert(std::string{header}); + return true; + }}); + for (std::string& header : expected_headers) { + std::cout << header << '\n'; + EXPECT_TRUE(response_headers.count(header)); + } +} + +TEST(CallbackDataTests, CallbackWriteFunctionCancelTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = Post(url, WriteCallback{[](const std::string_view& /*header*/, intptr_t /*userdata*/) -> bool { return false; }}); + EXPECT_TRUE((response.error.code == ErrorCode::ABORTED_BY_CALLBACK) || (response.error.code == ErrorCode::WRITE_ERROR)); +} + +TEST(CallbackDataTests, CallbackWriteFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + std::string response_text; + Post(url, Payload{{"x", "5"}}, WriteCallback{[&response_text](const std::string_view& header, intptr_t /*userdata*/) -> bool { + response_text.append(header); + return true; + }}); + EXPECT_EQ(expected_text, response_text); +} + +TEST(CallbackDataTests, CallbackProgressFunctionCancelTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = Post(url, ProgressCallback{[](size_t /*downloadTotal*/, size_t /*downloadNow*/, size_t /*uploadTotal*/, size_t /*uploadNow*/, intptr_t /*userdata*/) -> bool { return false; }}); + EXPECT_TRUE((response.error.code == ErrorCode::ABORTED_BY_CALLBACK) || (response.error.code == ErrorCode::WRITE_ERROR)); +} + +TEST(CallbackDataTests, CallbackProgressFunctionTotalTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Body body{"x=5"}; + size_t response_upload = 0; + size_t response_download = 0; + Response response = Post(url, body, ProgressCallback{[&](size_t downloadTotal, size_t /*downloadNow*/, size_t uploadTotal, size_t /*uploadNow*/, intptr_t /*userdata*/) -> bool { + response_upload = uploadTotal; + response_download = downloadTotal; + return true; + }}); + EXPECT_EQ(body.str().length(), response_upload); + EXPECT_EQ(response.text.length(), response_download); +} + +TEST(CallbackDataTests, CallbackDebugFunctionTextTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Body body{"x=5"}; + std::string debug_body; + Response response = Post(url, body, DebugCallback{[&](DebugCallback::InfoType type, const std::string& data, intptr_t /*userdata*/) { + if (type == DebugCallback::InfoType::DATA_OUT) { + debug_body = data; + } + }}); + EXPECT_EQ(body.str(), debug_body); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/data/certificates/ca-bundle.crt b/3rdparty/cpr/test/data/certificates/ca-bundle.crt new file mode 100644 index 0000000000000000000000000000000000000000..1a5417704c7d098aa5a3ee4dd929c0cb4c60a3ef --- /dev/null +++ b/3rdparty/cpr/test/data/certificates/ca-bundle.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIBrjCCAWCgAwIBAgIRAKy+/CzeW5ALVVSDllVnZdMwBQYDK2VwMDExCzAJBgNV +BAYTAkdCMRAwDgYDVQQKDAdFeGFtcGxlMRAwDgYDVQQDDAdSb290IENBMB4XDTI0 +MDUwNzEwMTgyMloXDTM0MDUwNTEwMTgyMlowMDELMAkGA1UEBhMCR0IxEDAOBgNV +BAoMB0V4YW1wbGUxDzANBgNVBAMMBlN1YiBDQTAqMAUGAytlcAMhAL9vKw+Jb0jc +THPJj/0HKRBIusX9D0Xj4qZEvK3kqXX+o4GNMIGKMA8GA1UdEwEB/wQFMAMBAf8w +DgYDVR0PAQH/BAQDAgIEMB0GA1UdDgQWBBSbsZshYdxmKzqt7YTxBbbOmYLB/DBI +BgNVHR4EQTA/oD0wC4IJbG9jYWxob3N0MAqHCH8AAAH/AAAAMCKHIAAAAAAAAAAA +AAAAAAAAAAH/////////////////////MAUGAytlcANBACspVj23xQ46wvlIWimf +ofVcl0Nlj1rW1CoTOoA4butJGfJJQoYMzW8Ui/sVokzPoTw7vdOw9u3Knps26c0T +Ygk= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBrzCCAWGgAwIBAgIRAKy+/CzeW5ALVVSDllVnZdIwBQYDK2VwMDExCzAJBgNV +BAYTAkdCMRAwDgYDVQQKDAdFeGFtcGxlMRAwDgYDVQQDDAdSb290IENBMB4XDTI0 +MDUwNzEwMTgyMloXDTM0MDUwNTEwMTgyMlowMTELMAkGA1UEBhMCR0IxEDAOBgNV +BAoMB0V4YW1wbGUxEDAOBgNVBAMMB1Jvb3QgQ0EwKjAFBgMrZXADIQDI4HsQNDKN +xwtOvL2FI7Q+VIoqWLHmsoLaOe1L+JvbyKOBjTCBijAPBgNVHRMBAf8EBTADAQH/ +MA4GA1UdDwEB/wQEAwICBDAdBgNVHQ4EFgQUvMDvOfNgjMd7lZ6iDa/JCJcVLwkw +SAYDVR0eBEEwP6A9MAuCCWxvY2FsaG9zdDAKhwh/AAAB/wAAADAihyAAAAAAAAAA +AAAAAAAAAAAB/////////////////////zAFBgMrZXADQQBCMm6k6vanrNUO3vlc +vsecQTSUVxsnl+bD6ANYhs10cuGafZ/lFRh1z4yBxz50b7EIePDeLP2pZlLmz8bm +sN8M +-----END CERTIFICATE----- diff --git a/3rdparty/cpr/test/data/certificates/client.crt b/3rdparty/cpr/test/data/certificates/client.crt new file mode 100644 index 0000000000000000000000000000000000000000..d04541e3aa20a3e38cb795ede7d5e475c0e4e55b --- /dev/null +++ b/3rdparty/cpr/test/data/certificates/client.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBejCCASygAwIBAgIRAKy+/CzeW5ALVVSDllVnZdUwBQYDK2VwMDAxCzAJBgNV +BAYTAkdCMRAwDgYDVQQKDAdFeGFtcGxlMQ8wDQYDVQQDDAZTdWIgQ0EwHhcNMjQw +NTA3MTAxODIyWhcNMjkwNTA2MTAxODIyWjAWMRQwEgYDVQQDDAt0ZXN0LWNsaWVu +dDAqMAUGAytlcAMhAPU88C62SwhVGcJhYDp97gf1x5pHjcVD/AZ3PqoQfU68o3Uw +czAfBgNVHSMEGDAWgBSbsZshYdxmKzqt7YTxBbbOmYLB/DAMBgNVHRMBAf8EAjAA +MBMGA1UdJQQMMAoGCCsGAQUFBwMCMA4GA1UdDwEB/wQEAwIHgDAdBgNVHQ4EFgQU +TU53uUDblDe4iFsDIV77hIwigPswBQYDK2VwA0EAX0aM10AEe8HxQNXcL2Qf1ryh +StldRyLog/s1ZuGidfxwdr7xoZes0yjYaZYhkKLDIf+CR3BwEWik2ppNXE1bDw== +-----END CERTIFICATE----- diff --git a/3rdparty/cpr/test/data/certificates/root-ca.crt b/3rdparty/cpr/test/data/certificates/root-ca.crt new file mode 100644 index 0000000000000000000000000000000000000000..2f1bda3bd8d6a9303f1922772ce107daf12dcffa --- /dev/null +++ b/3rdparty/cpr/test/data/certificates/root-ca.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBrzCCAWGgAwIBAgIRAKy+/CzeW5ALVVSDllVnZdIwBQYDK2VwMDExCzAJBgNV +BAYTAkdCMRAwDgYDVQQKDAdFeGFtcGxlMRAwDgYDVQQDDAdSb290IENBMB4XDTI0 +MDUwNzEwMTgyMloXDTM0MDUwNTEwMTgyMlowMTELMAkGA1UEBhMCR0IxEDAOBgNV +BAoMB0V4YW1wbGUxEDAOBgNVBAMMB1Jvb3QgQ0EwKjAFBgMrZXADIQDI4HsQNDKN +xwtOvL2FI7Q+VIoqWLHmsoLaOe1L+JvbyKOBjTCBijAPBgNVHRMBAf8EBTADAQH/ +MA4GA1UdDwEB/wQEAwICBDAdBgNVHQ4EFgQUvMDvOfNgjMd7lZ6iDa/JCJcVLwkw +SAYDVR0eBEEwP6A9MAuCCWxvY2FsaG9zdDAKhwh/AAAB/wAAADAihyAAAAAAAAAA +AAAAAAAAAAAB/////////////////////zAFBgMrZXADQQBCMm6k6vanrNUO3vlc +vsecQTSUVxsnl+bD6ANYhs10cuGafZ/lFRh1z4yBxz50b7EIePDeLP2pZlLmz8bm +sN8M +-----END CERTIFICATE----- diff --git a/3rdparty/cpr/test/data/certificates/server.crt b/3rdparty/cpr/test/data/certificates/server.crt new file mode 100644 index 0000000000000000000000000000000000000000..f6502a0627e4192484862f57edc7fa25ff4e59d1 --- /dev/null +++ b/3rdparty/cpr/test/data/certificates/server.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBtDCCAWagAwIBAgIRAKy+/CzeW5ALVVSDllVnZdQwBQYDK2VwMDAxCzAJBgNV +BAYTAkdCMRAwDgYDVQQKDAdFeGFtcGxlMQ8wDQYDVQQDDAZTdWIgQ0EwHhcNMjQw +NTA3MTAxODIyWhcNMjkwNTA2MTAxODIyWjAWMRQwEgYDVQQDDAt0ZXN0LXNlcnZl +cjAqMAUGAytlcAMhACdLUqJFSyspgGKJiXNlnOLU2dO/TLV+b8aIZNAX7EuVo4Gu +MIGrMB8GA1UdIwQYMBaAFJuxmyFh3GYrOq3thPEFts6ZgsH8MAwGA1UdEwEB/wQC +MAAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIF +oDAdBgNVHQ4EFgQUZkdU+CWXVppSVjW0p1JgDOdPMwkwLAYDVR0RBCUwI4IJbG9j +YWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMAUGAytlcANBAG1j2RGjm8ef +tiMSJ+k04KGjIL7734D+UwidjOSCQnbCVRPofIaDMwuan5IqP97pMnjAsbw/QukX ++Z9sFTWjAQk= +-----END CERTIFICATE----- diff --git a/3rdparty/cpr/test/data/certificates/sub-ca.crt b/3rdparty/cpr/test/data/certificates/sub-ca.crt new file mode 100644 index 0000000000000000000000000000000000000000..f56692e390cfc1927e92ca574f8694d7c4a5a4c5 --- /dev/null +++ b/3rdparty/cpr/test/data/certificates/sub-ca.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBrjCCAWCgAwIBAgIRAKy+/CzeW5ALVVSDllVnZdMwBQYDK2VwMDExCzAJBgNV +BAYTAkdCMRAwDgYDVQQKDAdFeGFtcGxlMRAwDgYDVQQDDAdSb290IENBMB4XDTI0 +MDUwNzEwMTgyMloXDTM0MDUwNTEwMTgyMlowMDELMAkGA1UEBhMCR0IxEDAOBgNV +BAoMB0V4YW1wbGUxDzANBgNVBAMMBlN1YiBDQTAqMAUGAytlcAMhAL9vKw+Jb0jc +THPJj/0HKRBIusX9D0Xj4qZEvK3kqXX+o4GNMIGKMA8GA1UdEwEB/wQFMAMBAf8w +DgYDVR0PAQH/BAQDAgIEMB0GA1UdDgQWBBSbsZshYdxmKzqt7YTxBbbOmYLB/DBI +BgNVHR4EQTA/oD0wC4IJbG9jYWxob3N0MAqHCH8AAAH/AAAAMCKHIAAAAAAAAAAA +AAAAAAAAAAH/////////////////////MAUGAytlcANBACspVj23xQ46wvlIWimf +ofVcl0Nlj1rW1CoTOoA4butJGfJJQoYMzW8Ui/sVokzPoTw7vdOw9u3Knps26c0T +Ygk= +-----END CERTIFICATE----- diff --git a/3rdparty/cpr/test/data/client.cnf b/3rdparty/cpr/test/data/client.cnf new file mode 100644 index 0000000000000000000000000000000000000000..d387d39deae46b07bad005e64cc979cee6e5a620 --- /dev/null +++ b/3rdparty/cpr/test/data/client.cnf @@ -0,0 +1,8 @@ +# Based on https://www.feistyduck.com/library/openssl-cookbook/online/openssl-command-line/private-ca-create-subordinate.html +[req] +prompt = no +distinguished_name = dn + +[dn] +CN = test-client + diff --git a/3rdparty/cpr/test/data/generate-certificates.sh b/3rdparty/cpr/test/data/generate-certificates.sh new file mode 100755 index 0000000000000000000000000000000000000000..7f127daee515487ca88e109a20ff03661525f252 --- /dev/null +++ b/3rdparty/cpr/test/data/generate-certificates.sh @@ -0,0 +1,88 @@ +#!/bin/sh + +# Generate a CA with a self-signed root certificate that then signs the server certificate +# Based on the OpenSSL Cookbook by Ivan Ristic: +# https://www.feistyduck.com/library/openssl-cookbook/online/ +# +# Especially, see chapter 1.5. Creating a private Certification Authority: +# https://www.feistyduck.com/library/openssl-cookbook/online/openssl-command-line/private-ca.html + +export KEY_PATH=keys +export CRT_PATH=certificates +export CA_PATH=ca + +# Create environment. +# $CA_PATH is deleted in the end. +# If new certificates need to be issued, this needs to be done before the cleanup in the end. +mkdir -p $KEY_PATH $CRT_PATH $CA_PATH/db $CA_PATH/private $CA_PATH/certificates +touch $CA_PATH/db/index +openssl rand -hex 16 > $CA_PATH/db/serial + + +# Generate all private keys +openssl genpkey -algorithm ed25519 -out $KEY_PATH/root-ca.key +openssl genpkey -algorithm ed25519 -out $KEY_PATH/sub-ca.key +openssl genpkey -algorithm ed25519 -out $KEY_PATH/server.key +openssl genpkey -algorithm ed25519 -out $KEY_PATH/client.key + +# For the server, we also need the public key +openssl pkey -in $KEY_PATH/server.key -pubout -out $KEY_PATH/server.pub + + +# Generate a Certificate Signing Request for the Root CA based on a config file +openssl req -new \ + -config root-ca.cnf -out root-ca.csr \ + -key $KEY_PATH/root-ca.key + +# Self-sign the root certificate +openssl ca -batch \ + -selfsign -config root-ca.cnf \ + -extensions ca_ext \ + -in root-ca.csr -out $CRT_PATH/root-ca.crt -notext + +# Create a Certificate Signing request for the Sub CA +openssl req -new \ + -config sub-ca.cnf -out sub-ca.csr \ + -key $KEY_PATH/sub-ca.key + +# Issue the Sub CA +openssl ca -batch \ + -config root-ca.cnf \ + -extensions ca_ext \ + -in sub-ca.csr -out $CRT_PATH/sub-ca.crt -notext + +# Create a Certificate Signing request for the server certificate +openssl req -new \ + -config server.cnf -out server.csr \ + -key $KEY_PATH/server.key +openssl req -text -in server.csr -noout + +# Issue the server certificate +openssl ca -batch \ + -config root-ca.cnf \ + -name sub_ca \ + -extensions server_ext \ + -in server.csr -out $CRT_PATH/server.crt -notext \ + -days 1825 + +# Create a Certificate Signing request for the client certificate +openssl req -new \ + -config client.cnf -out client.csr \ + -key $KEY_PATH/client.key + +# Issue the client certificate +openssl ca -batch \ + -config root-ca.cnf \ + -name sub_ca \ + -extensions client_ext \ + -in client.csr -out $CRT_PATH/client.crt -notext \ + -days 1825 + +cp $CRT_PATH/sub-ca.crt $CRT_PATH/ca-bundle.crt +cat $CRT_PATH/root-ca.crt >> $CRT_PATH/ca-bundle.crt + +# Clean up +# IMPORTANT: If new certificates should be issued, $CA_PATH and its files MUST NOT be deleted! +# New certificates can be created in this script before cleaning up. +rm -rf *.csr $CA_PATH + diff --git a/3rdparty/cpr/test/data/keys/client.key b/3rdparty/cpr/test/data/keys/client.key new file mode 100644 index 0000000000000000000000000000000000000000..d38e1eed09733928ea6b3cb827784240b45c11bb --- /dev/null +++ b/3rdparty/cpr/test/data/keys/client.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIIK4CYIlr3jGta1aSNICikX8V4CXv/i6IJTmj68CUQOU +-----END PRIVATE KEY----- diff --git a/3rdparty/cpr/test/data/keys/root-ca.key b/3rdparty/cpr/test/data/keys/root-ca.key new file mode 100644 index 0000000000000000000000000000000000000000..cb395bf18369eef061f38cd8672215269fa96354 --- /dev/null +++ b/3rdparty/cpr/test/data/keys/root-ca.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEICJbx2nPwG8L2S/EKvCHI2q4InmAFAaNVBqdVq13ZpJz +-----END PRIVATE KEY----- diff --git a/3rdparty/cpr/test/data/keys/server.key b/3rdparty/cpr/test/data/keys/server.key new file mode 100644 index 0000000000000000000000000000000000000000..4562df47291236e8b0a1295c7c89a21e2c33c916 --- /dev/null +++ b/3rdparty/cpr/test/data/keys/server.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIGqt/stoQYkwb24d3EUC0LpH2QwKuh+0tftML+wk/N1P +-----END PRIVATE KEY----- diff --git a/3rdparty/cpr/test/data/keys/server.pub b/3rdparty/cpr/test/data/keys/server.pub new file mode 100644 index 0000000000000000000000000000000000000000..4286bfdb2448d0bc55f586c6af4850c2bc4933f9 --- /dev/null +++ b/3rdparty/cpr/test/data/keys/server.pub @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAJ0tSokVLKymAYomJc2Wc4tTZ079MtX5vxohk0BfsS5U= +-----END PUBLIC KEY----- diff --git a/3rdparty/cpr/test/data/keys/sub-ca.key b/3rdparty/cpr/test/data/keys/sub-ca.key new file mode 100644 index 0000000000000000000000000000000000000000..954f634de527cf384afcb1b002d62d8570bbb69f --- /dev/null +++ b/3rdparty/cpr/test/data/keys/sub-ca.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIASqWiXeb8UOEbwjVVq/2j49JvbBX2aLAiqjUtHQK2qV +-----END PRIVATE KEY----- diff --git a/3rdparty/cpr/test/data/root-ca.cnf b/3rdparty/cpr/test/data/root-ca.cnf new file mode 100644 index 0000000000000000000000000000000000000000..dd7cacc505c67162fd752f17255c585c7103f084 --- /dev/null +++ b/3rdparty/cpr/test/data/root-ca.cnf @@ -0,0 +1,85 @@ +# Based on: https://www.feistyduck.com/library/openssl-cookbook/online/openssl-command-line/private-ca-creating-root.html +[default] +name = root-ca +default_ca = ca_default +name_opt = utf8,esc_ctrl,multiline,lname,align + +[ca_dn] +countryName = "GB" +organizationName = "Example" +commonName = "Root CA" + +[ca_default] +home = ./${ENV::CA_PATH} +database = $home/db/index +serial = $home/db/serial +certificate = ./${ENV::CRT_PATH}/$name.crt +private_key = ./${ENV::KEY_PATH}/$name.key +RANDFILE = $home/private/random +new_certs_dir = $home/certificates +unique_subject = no +copy_extensions = none +default_days = 3650 +default_md = sha256 +policy = policy_cn_supplied + +[sub_ca] +name = sub-ca +name_opt = utf8,esc_ctrl,multiline,lname,align +home = ./${ENV::CA_PATH} +database = $home/db/index +serial = $home/db/serial +certificate = ./${ENV::CRT_PATH}/$name.crt +private_key = ./${ENV::KEY_PATH}/$name.key +RANDFILE = $home/private/random +new_certs_dir = $home/certificates +unique_subject = no +copy_extensions = none +default_days = 3650 +default_md = sha256 +policy = policy_cn_supplied + +[policy_cn_supplied] +countryName = optional +stateOrProvinceName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[req] +default_bits = 4096 +encrypt_key = yes +default_md = sha256 +utf8 = yes +string_mask = utf8only +prompt = no +distinguished_name = ca_dn +req_extensions = ca_ext + +[ca_ext] +basicConstraints = critical,CA:true +keyUsage = critical,keyCertSign +subjectKeyIdentifier = hash +nameConstraints = @name_constraints + +[server_ext] +authorityKeyIdentifier = keyid:always +basicConstraints = critical,CA:false +extendedKeyUsage = clientAuth,serverAuth +keyUsage = critical,digitalSignature,keyEncipherment +subjectKeyIdentifier = hash +subjectAltName = DNS:localhost,IP:127.0.0.1,IP:::1 + +[client_ext] +authorityKeyIdentifier = keyid:always +basicConstraints = critical,CA:false +extendedKeyUsage = clientAuth +keyUsage = critical,digitalSignature +subjectKeyIdentifier = hash + +[name_constraints] +permitted;DNS.0=localhost +permitted;IP.0=127.0.0.1/255.0.0.0 +permitted;IP.1=::1/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + diff --git a/3rdparty/cpr/test/data/server.cnf b/3rdparty/cpr/test/data/server.cnf new file mode 100644 index 0000000000000000000000000000000000000000..a67fe34ab1752ed50f80c9b66ae867d5f10677ba --- /dev/null +++ b/3rdparty/cpr/test/data/server.cnf @@ -0,0 +1,12 @@ +# Based on https://www.feistyduck.com/library/openssl-cookbook/online/openssl-command-line/private-ca-create-subordinate.html +[req] +prompt = no +distinguished_name = dn +req_extensions = ext + +[dn] +CN = test-server + +[ext] +subjectAltName = DNS:localhost,IP:127.0.0.1,IP:::1 + diff --git a/3rdparty/cpr/test/data/sub-ca.cnf b/3rdparty/cpr/test/data/sub-ca.cnf new file mode 100644 index 0000000000000000000000000000000000000000..3e691fccc42e53ea5cf96fc066d0cf3d0d7b6b24 --- /dev/null +++ b/3rdparty/cpr/test/data/sub-ca.cnf @@ -0,0 +1,25 @@ +[req] +default_bits = 4096 +encrypt_key = yes +default_md = sha256 +utf8 = yes +string_mask = utf8only +prompt = no +distinguished_name = sub_ca_dn +req_extensions = sub_ca_ext + +[sub_ca_dn] +countryName = "GB" +organizationName = "Example" +commonName = "Sub CA" + +[sub_ca_ext] +basicConstraints = critical,CA:true +keyUsage = critical,keyCertSign +subjectKeyIdentifier = hash +nameConstraints = @name_constraints + +[name_constraints] +permitted;DNS.0=localhost +permitted;IP.0=127.0.0.1/255.0.0.0 +permitted;IP.1=::1/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff diff --git a/3rdparty/cpr/test/data/test_file.txt b/3rdparty/cpr/test/data/test_file.txt new file mode 100644 index 0000000000000000000000000000000000000000..b67d8e82d99ab5d311143e73ed633b305e83f259 --- /dev/null +++ b/3rdparty/cpr/test/data/test_file.txt @@ -0,0 +1 @@ +test content: hello_äüöp_2585_你好 \ No newline at end of file diff --git "a/3rdparty/cpr/test/data/test_file_hello_\303\244\303\274\303\266p_2585.txt" "b/3rdparty/cpr/test/data/test_file_hello_\303\244\303\274\303\266p_2585.txt" new file mode 100644 index 0000000000000000000000000000000000000000..b67d8e82d99ab5d311143e73ed633b305e83f259 --- /dev/null +++ "b/3rdparty/cpr/test/data/test_file_hello_\303\244\303\274\303\266p_2585.txt" @@ -0,0 +1 @@ +test content: hello_äüöp_2585_你好 \ No newline at end of file diff --git "a/3rdparty/cpr/test/data/test_file_hello_\303\244\303\274\303\266p_2585_\344\275\240\345\245\275.txt" "b/3rdparty/cpr/test/data/test_file_hello_\303\244\303\274\303\266p_2585_\344\275\240\345\245\275.txt" new file mode 100644 index 0000000000000000000000000000000000000000..b67d8e82d99ab5d311143e73ed633b305e83f259 --- /dev/null +++ "b/3rdparty/cpr/test/data/test_file_hello_\303\244\303\274\303\266p_2585_\344\275\240\345\245\275.txt" @@ -0,0 +1 @@ +test content: hello_äüöp_2585_你好 \ No newline at end of file diff --git a/3rdparty/cpr/test/delete_tests.cpp b/3rdparty/cpr/test/delete_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f067eff6f2e7d0a5a145cbb3aa3a83f5447fb36a --- /dev/null +++ b/3rdparty/cpr/test/delete_tests.cpp @@ -0,0 +1,259 @@ +#include + +#include + +#include "cpr/cpr.h" + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(DeleteTests, DeleteTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + Response response = cpr::Delete(url); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, DeleteUnallowedTest) { + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + Response response = cpr::Delete(url); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, DeleteJsonBodyTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + Response response = cpr::Delete(url, cpr::Body{"'foo': 'bar'"}, cpr::Header{{"Content-Type", "application/json"}}); + std::string expected_text{"'foo': 'bar'"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + Session session; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteUnallowedTest) { + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + Session session; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteJsonBodyTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Content-Type", "application/json"}}); + session.SetBody(cpr::Body{"{'foo': 'bar'}"}); + Response response = session.Delete(); + std::string expected_text{"{'foo': 'bar'}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/delete.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteUnallowedAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/delete.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteUnallowedAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, SessionDeleteUnallowedAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, AsyncDeleteTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + cpr::AsyncResponse future_response = cpr::DeleteAsync(url); + cpr::Response response = future_response.get(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, AsyncDeleteUnallowedTest) { + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + cpr::AsyncResponse future_response = cpr::DeleteAsync(url); + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DeleteTests, AsyncMultipleDeleteTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::DeleteAsync(url)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(DeleteTests, AsyncMultipleDeleteUnallowedTest) { + Url url{server->GetBaseUrl() + "/delete_unallowed.html"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::DeleteAsync(url)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/download_tests.cpp b/3rdparty/cpr/test/download_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b9bd5d9fd801c9c8208fd02deb2ba14942efaf53 --- /dev/null +++ b/3rdparty/cpr/test/download_tests.cpp @@ -0,0 +1,152 @@ +#include +#include + +#include + +#include "cpr/accept_encoding.h" +#include "cpr/cpr.h" + +#include "cpr/api.h" +#include "cpr/callback.h" +#include "cpr/cprtypes.h" +#include "cpr/session.h" +#include "httpServer.hpp" + + +static cpr::HttpServer* server = new cpr::HttpServer(); + +bool write_data(const std::string_view& /*data*/, intptr_t /*userdata*/) { + return true; +} + +TEST(DownloadTests, DownloadHeaderGzip) { + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetUrl(url); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +TEST(DownloadTests, DownloadAcceptEncodingGzip) { + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetAcceptEncoding(cpr::AcceptEncoding{cpr::AcceptEncodingMethods::gzip}); + session.SetUrl(url); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +TEST(DownloadTests, RangeTestWholeFile) { + const int64_t download_size = 9; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetRange(cpr::Range{std::nullopt, std::nullopt}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +TEST(DownloadTests, RangeTestLowerLimit) { + const int64_t download_size = 8; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetRange(cpr::Range{1, std::nullopt}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(206, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +TEST(DownloadTests, RangeTestUpperLimit) { + const int64_t download_size = 6; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetRange(cpr::Range{std::nullopt, download_size - 1}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(206, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +TEST(DownloadTests, RangeTestLowerAndUpperLimit) { + const int64_t download_size = 2; + const int64_t start_from = 2; + const int64_t finish_at = start_from + download_size - 1; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetRange(cpr::Range{start_from, finish_at}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(206, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +TEST(DownloadTests, RangeTestMultipleRangesSet) { + const int64_t num_parts = 2; + const int64_t download_size = num_parts * (26 /*content range*/ + 4 /*\n*/) + ((num_parts + 1) * 15 /*boundary*/) + 2 /*--*/ + 6 /*data*/; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetMultiRange(cpr::MultiRange{cpr::Range{std::nullopt, 3}, cpr::Range{5, 6}}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(206, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +TEST(DownloadTests, RangeTestMultipleRangesOption) { + const int64_t num_parts = 3; + const int64_t download_size = num_parts * (26 /*content range*/ + 4 /*\n*/) + ((num_parts + 1) * 15 /*boundary*/) + 2 /*--*/ + 7 /*data*/; + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetOption(cpr::MultiRange{cpr::Range{std::nullopt, 2}, cpr::Range{4, 5}, cpr::Range{7, 8}}); + cpr::Response response = session.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(206, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(download_size, response.downloaded_bytes); +} + +bool real_write_data(const std::string_view& data, intptr_t userdata) { + // NOLINTNEXTLINE (cppcoreguidelines-pro-type-reinterpret-cast) + std::string* dst = reinterpret_cast(userdata); + *dst += data; + return true; +} + +TEST(DownloadTests, GetDownloadFileLength) { + cpr::Url url{server->GetBaseUrl() + "/get_download_file_length.html"}; + cpr::Session session; + session.SetUrl(url); + auto len = session.GetDownloadFileLength(); + EXPECT_EQ(len, -1); + + std::string strFileData; + cpr::Response response = session.Download(cpr::WriteCallback{real_write_data, (intptr_t) &strFileData}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); + EXPECT_EQ(strFileData, "this is a file content."); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/encoded_auth_tests.cpp b/3rdparty/cpr/test/encoded_auth_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c43c93bb425c90b474a7380f62bf4a3693aef42e --- /dev/null +++ b/3rdparty/cpr/test/encoded_auth_tests.cpp @@ -0,0 +1,20 @@ +#include + +#include + +#include "cpr/cpr.h" + +using namespace cpr; + +TEST(EncodedAuthenticationTests, UnicodeEncoderTest) { + std::string user = "一二三"; + std::string pass = "Hello World!"; + EncodedAuthentication pa{user, pass}; + EXPECT_EQ(pa.GetUsername(), "%E4%B8%80%E4%BA%8C%E4%B8%89"); + EXPECT_EQ(pa.GetPassword(), "Hello%20World%21"); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/error_tests.cpp b/3rdparty/cpr/test/error_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..23b52c83b6cad878243ca5023760a3d3fc5f0696 --- /dev/null +++ b/3rdparty/cpr/test/error_tests.cpp @@ -0,0 +1,97 @@ +#include + +#include +#include + +#include "cpr/cpr.h" +#include + +#include "httpServer.hpp" +#include "httpsServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(ErrorTests, UnsupportedProtocolFailure) { + Url url{"urk://wat.is.this"}; + Response response = cpr::Get(url); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::UNSUPPORTED_PROTOCOL, response.error.code); +} + +TEST(ErrorTests, InvalidURLFailure) { + Url url{"???"}; + Response response = cpr::Get(url); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::URL_MALFORMAT, response.error.code); +} + +TEST(ErrorTests, TimeoutFailure) { + Url url{server->GetBaseUrl() + "/timeout.html"}; + Response response = cpr::Get(url, cpr::Timeout{1}); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(ErrorTests, ChronoTimeoutFailure) { + Url url{server->GetBaseUrl() + "/timeout.html"}; + Response response = cpr::Get(url, cpr::Timeout{std::chrono::milliseconds{1}}); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(ErrorTests, ConnectTimeoutFailure) { + Url url{"http://localhost:67"}; + Response response = cpr::Get(url, cpr::ConnectTimeout{1}); + EXPECT_EQ(0, response.status_code); + // Sometimes a COULDNT_CONNECT happens before the OPERATION_TIMEDOUT: + EXPECT_TRUE(response.error.code == ErrorCode::OPERATION_TIMEDOUT || response.error.code == ErrorCode::COULDNT_CONNECT); +} + +TEST(ErrorTests, ChronoConnectTimeoutFailure) { + Url url{"http://localhost:67"}; + Response response = cpr::Get(url, cpr::ConnectTimeout{std::chrono::milliseconds{1}}); + EXPECT_EQ(0, response.status_code); + // Sometimes a COULDNT_CONNECT happens before the OPERATION_TIMEDOUT: + EXPECT_TRUE(response.error.code == ErrorCode::OPERATION_TIMEDOUT || response.error.code == ErrorCode::COULDNT_CONNECT); +} + +TEST(ErrorTests, LowSpeedTimeFailure) { + Url url{server->GetBaseUrl() + "/low_speed.html"}; + Response response = cpr::Get(url, cpr::LowSpeed{1000, 1}); + // Do not check for the HTTP status code, since libcurl always provides the status code of the header if it was received + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(ErrorTests, LowSpeedBytesFailure) { + Url url{server->GetBaseUrl() + "/low_speed_bytes.html"}; + Response response = cpr::Get(url, cpr::LowSpeed{1000, 1}); + // Do not check for the HTTP status code, since libcurl always provides the status code of the header if it was received + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(ErrorTests, ProxyFailure) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, cpr::Proxies{{"http", "http://bad_host.libcpr.org"}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::COULDNT_RESOLVE_PROXY, response.error.code); +} + +TEST(ErrorTests, BoolFalseTest) { + Error error; + EXPECT_FALSE(error); +} + +TEST(ErrorTests, BoolTrueTest) { + Error error; + error.code = ErrorCode::UNSUPPORTED_PROTOCOL; + EXPECT_TRUE(error); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/file_upload_tests.cpp b/3rdparty/cpr/test/file_upload_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..22e710f622cc88dbad5d47630dc21b59ba64ea00 --- /dev/null +++ b/3rdparty/cpr/test/file_upload_tests.cpp @@ -0,0 +1,100 @@ +#include + +#include +#include +#include + +#include +#include + +#include "cpr/api.h" +#include "cpr/file.h" +#include "cpr/multipart.h" +#include "httpServer.hpp" + +namespace { +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables, cert-err58-cpp) +cpr::HttpServer* server = new cpr::HttpServer(); +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::optional baseDirPath{std::nullopt}; +} // namespace + +cpr::fs::path GetBasePath(const std::string& execPath) { + return cpr::fs::path(cpr::fs::path{execPath}.parent_path().string() + "/").make_preferred(); +} + + +TEST(FileUploadTests, AsciiFileName) { + // Ensure 'baseDirPath' has been set + EXPECT_NE(baseDirPath, std::nullopt); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + cpr::fs::path filePath = *baseDirPath / "test_file.txt"; + + cpr::Multipart mp{{cpr::Part("file_name", cpr::File(filePath.string()))}}; + cpr::Url url{server->GetBaseUrl() + "/post_file_upload.html"}; + cpr::Response response = cpr::Post(url, mp); + + // Expected file content + std::ifstream ifs(filePath.string()); + std::string expected_text = "{\n \"file_name\": \"" + filePath.filename().string() + "=" + std::string((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())) + "\"\n}"; + + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(cpr::Url{server->GetBaseUrl() + "/post_file_upload.html"}, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +TEST(FileUploadTests, NonAsciiFileName) { + // Ensure 'baseDirPath' has been set + EXPECT_NE(baseDirPath, std::nullopt); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + cpr::fs::path filePath = *baseDirPath / "test_file_hello_äüöp_2585.txt"; + + cpr::Multipart mp{{cpr::Part("file_name", cpr::File(filePath.string()))}}; + cpr::Url url{server->GetBaseUrl() + "/post_file_upload.html"}; + cpr::Response response = cpr::Post(url, mp); + + // Expected file content + std::ifstream ifs(filePath.string()); + std::string expected_text = "{\n \"file_name\": \"" + filePath.filename().string() + "=" + std::string((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())) + "\"\n}"; + + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(cpr::Url{server->GetBaseUrl() + "/post_file_upload.html"}, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +TEST(FileUploadTests, ChineseFileName) { + // Ensure 'baseDirPath' has been set + EXPECT_NE(baseDirPath, std::nullopt); + + // NOLINTNEXTLINE(bugprone-unchecked-optional-access) + cpr::fs::path filePath = *baseDirPath / "test_file_hello_äüöp_2585.txt"; + + cpr::Multipart mp{{cpr::Part("file_name", cpr::File(filePath.string()))}}; + cpr::Url url{server->GetBaseUrl() + "/post_file_upload.html"}; + cpr::Response response = cpr::Post(url, mp); + + // Expected file content + std::ifstream ifs(filePath.string()); + std::string expected_text = "{\n \"file_name\": \"" + filePath.filename().string() + "=" + std::string((std::istreambuf_iterator(ifs)), (std::istreambuf_iterator())) + "\"\n}"; + + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(cpr::Url{server->GetBaseUrl() + "/post_file_upload.html"}, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + baseDirPath = std::make_optional(cpr::fs::path{GetBasePath(argv[0]).string() + "data/"}); + + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/get_tests.cpp b/3rdparty/cpr/test/get_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9a7e8261161be16458692f653173ab7f1ec629c4 --- /dev/null +++ b/3rdparty/cpr/test/get_tests.cpp @@ -0,0 +1,1329 @@ +#include +#include + +#include +#include + +#include "cpr/cpr.h" +#include "cpr/cprtypes.h" +#include "cpr/redirect.h" +#include "cpr/session.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(BasicTests, XXXTest) { + Url url{"https://getsolara.dev/api/endpoint.json"}; + Response response = cpr::Get(url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, HelloWorldTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, HelloWorldStringViewUrlTest) { + Url url{static_cast(server->GetBaseUrl() + "/hello.html")}; + Response response = cpr::Get(url); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, HelloWorldNoInterfaceTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Interface iface{""}; // Do not specify any specific interface + Response response = cpr::Get(url, iface); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, HelloWorldNoInterfaceStringViewTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Interface iface{std::string_view{}}; // Do not specify any specific interface + Response response = cpr::Get(url, iface); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, TimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, Timeout{0L}); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, BasicJsonTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Response response = cpr::Get(url); + std::string expected_text{ + "[\n" + " {\n" + " \"first_key\": \"first_value\",\n" + " \"second_key\": \"second_value\"\n" + " }\n" + "]"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, ResourceNotFoundTest) { + Url url{server->GetBaseUrl() + "/error.html"}; + Response response = cpr::Get(url); + std::string expected_text{"Not Found"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(404, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, BadHostTest) { + Url url{"http://bad_host/"}; + Response response = cpr::Get(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::COULDNT_RESOLVE_HOST, response.error.code); +} + +TEST(CookiesTests, BasicCookiesTest) { + Url url{server->GetBaseUrl() + "/basic_cookies.html"}; + Response response = cpr::Get(url); + cpr::Cookies res_cookies{response.cookies}; + std::string expected_text{"Basic Cookies"}; + cpr::Cookies expectedCookies{ + {"SID", "31d4d96e407aad42", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + {"lang", "en-US", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + }; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + for (auto cookie = res_cookies.begin(), expectedCookie = expectedCookies.begin(); cookie != res_cookies.end() && expectedCookie != expectedCookies.end(); cookie++, expectedCookie++) { + EXPECT_EQ(expectedCookie->GetName(), cookie->GetName()); + EXPECT_EQ(expectedCookie->GetValue(), cookie->GetValue()); + EXPECT_EQ(expectedCookie->GetDomain(), cookie->GetDomain()); + EXPECT_EQ(expectedCookie->IsIncludingSubdomains(), cookie->IsIncludingSubdomains()); + EXPECT_EQ(expectedCookie->GetPath(), cookie->GetPath()); + EXPECT_EQ(expectedCookie->IsHttpsOnly(), cookie->IsHttpsOnly()); + EXPECT_EQ(expectedCookie->GetExpires(), cookie->GetExpires()); + } +} + +TEST(CookiesTests, EmptyCookieTest) { + Url url{server->GetBaseUrl() + "/empty_cookies.html"}; + Response response = cpr::Get(url); + cpr::Cookies res_cookies{response.cookies}; + std::string expected_text{"Empty Cookies"}; + cpr::Cookies expectedCookies{ + {"SID", "", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + {"lang", "", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + }; + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + for (auto cookie = res_cookies.begin(), expectedCookie = expectedCookies.begin(); cookie != res_cookies.end() && expectedCookie != expectedCookies.end(); cookie++, expectedCookie++) { + EXPECT_EQ(expectedCookie->GetName(), cookie->GetName()); + EXPECT_EQ(expectedCookie->GetValue(), cookie->GetValue()); + EXPECT_EQ(expectedCookie->GetDomain(), cookie->GetDomain()); + EXPECT_EQ(expectedCookie->IsIncludingSubdomains(), cookie->IsIncludingSubdomains()); + EXPECT_EQ(expectedCookie->GetPath(), cookie->GetPath()); + EXPECT_EQ(expectedCookie->IsHttpsOnly(), cookie->IsHttpsOnly()); + EXPECT_EQ(expectedCookie->GetExpires(), cookie->GetExpires()); + } +} + +TEST(CookiesTests, ClientSetCookiesTest) { + Url url{server->GetBaseUrl() + "/cookies_reflect.html"}; + Cookies cookies{ + {"SID", "31d4d96e407aad42", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + {"lang", "en-US", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + }; + Response response = cpr::Get(url, cookies); + std::string expected_text{"SID=31d4d96e407aad42; lang=en-US;"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CookiesTests, UnencodedCookiesTest) { + Url url{server->GetBaseUrl() + "/cookies_reflect.html"}; + Cookies cookies{ + {"SID", "31d4d %$ 96e407aad42", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + {"lang", "en-US", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + }; + cookies.encode = false; + Response response = cpr::Get(url, cookies); + std::string expected_text{"SID=31d4d %$ 96e407aad42; lang=en-US;"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterTests, SingleParameterTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Parameters parameters{{"key", "value"}}; + Response response = cpr::Get(url, parameters); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterTests, SingleParameterOnlyKeyTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Parameters parameters{{"key", ""}}; + Response response = cpr::Get(url, parameters); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); +} + +TEST(ParameterTests, MultipleParametersTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, Parameters{{"key", "value"}, {"hello", "world"}, {"test", "case"}}); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value&hello=world&test=case"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterTests, MultipleDynamicParametersTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Parameters parameters{{"key", "value"}}; + parameters.Add({"hello", "world"}); + parameters.Add({"test", "case"}); + Response response = cpr::Get(url, parameters); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value&hello=world&test=case"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationTests, BasicAuthenticationSuccessTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationTests, BasicBearerSuccessTest) { + Url url{server->GetBaseUrl() + "/bearer_token.html"}; +#if CPR_LIBCURL_VERSION_NUM >= 0x073D00 // 7.61.0 + Response response = cpr::Get(url, Bearer{"the_token"}); +#else + Response response = cpr::Get(url, Header{{"Authorization", "Bearer the_token"}}); +#endif + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationTests, BasicDigestSuccessTest) { + Url url{server->GetBaseUrl() + "/digest_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::DIGEST}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAthenticationParameterTests, BasicAuthenticationSuccessSingleParameterTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterTests, BasicAuthenticationSuccessMultipleParametersTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"key", "value"}, {"hello", "world"}, {"test", "case"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value&hello=world&test=case"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterTests, BasicAuthenticationSuccessSingleParameterReverseTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterTests, BasicAuthenticationSuccessMultipleParametersReverseTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"key", "value"}, {"hello", "world"}, {"test", "case"}}, Authentication{"user", "password", AuthMode::BASIC}); + + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value&hello=world&test=case"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationHeaderTests, BasicAuthenticationSuccessSingleHeaderTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationHeaderTests, BasicAuthenticationSuccessMultipleHeadersTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{{"key", "value"}, {"hello", "world"}, {"test", "case"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(std::string{"case"}, response.header["test"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationHeaderTests, BasicAuthenticationSuccessSingleHeaderReverseTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationHeaderTests, BasicAuthenticationSuccessMultipleHeadersReverseTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"key", "value"}, {"hello", "world"}, {"test", "case"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(std::string{"case"}, response.header["test"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationTests, BasicAuthenticationNullFailureTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationTests, BasicAuthenticationFailureTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterTests, BasicAuthenticationFailureSingleParameterTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"hello", "world"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterTests, BasicAuthenticationFailureMultipleParametersTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"key", "value"}, {"hello", "world"}, {"test", "case"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?key=value&hello=world&test=case"}, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderJsonTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Response response = cpr::Get(url, Header{{"content-type", "application/json"}}); + std::string expected_text{ + "[\n" + " {\n" + " \"first_key\": \"first_value\",\n" + " \"second_key\": \"second_value\"\n" + " }\n" + "]"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectNoneTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectUpdateHeaderAddSessionTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetHeader(Header{{"Header1", "Value1"}}); + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"Value1"}, response.header["Header1"]); + EXPECT_EQ(std::string{}, response.header["Header2"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + + session.UpdateHeader(Header{{"Header2", "Value2"}}); + response = session.Get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"Value1"}, response.header["Header1"]); + EXPECT_EQ(std::string{"Value2"}, response.header["Header2"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +/** + * Test case for #532 + * https://github.com/whoshuu/cpr/issues/532 + **/ +TEST(HeaderTests, SessionHeaderReflectTest) { + std::unique_ptr session(new cpr::Session()); + session->SetUrl({server->GetBaseUrl() + "/header_reflect.html"}); + session->SetBody("Some Body to post"); + session->SetHeader({{"Content-Type", "application/json"}}); + cpr::Response response = session->Post(); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(std::string{"Header reflect POST"}, response.text); + EXPECT_EQ(std::string{"application/json"}, response.header["Content-Type"]); +} + +TEST(HeaderTests, HeaderReflectUpdateHeaderUpdateSessionTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetHeader(Header{{"Header1", "Value1"}}); + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"Value1"}, response.header["Header1"]); + EXPECT_EQ(std::string{}, response.header["Header2"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + + session.UpdateHeader(Header{{"Header1", "Value2"}}); + response = session.Get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"Value2"}, response.header["Header1"]); + EXPECT_EQ(std::string{}, response.header["Header2"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectEmptyTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectSingleTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectMultipleTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}, {"key", "value"}, {"test", "case"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(std::string{"case"}, response.header["test"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, HeaderReflectCaseInsensitiveTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"HeLlO", "wOrLd"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hello"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["HELLO"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hElLo"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeaderTests, SetEmptyHeaderTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"hello", ""}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectNoneParametersTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectEmptyParametersTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{}, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectSingleParametersTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectMultipleParametersTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}, {"key", "value"}, {"test", "case"}}, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(std::string{"case"}, response.header["test"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectCaseInsensitiveParametersTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Header{{"HeLlO", "wOrLd"}}, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hello"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["HELLO"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hElLo"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectEmptyParametersReverseTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectSingleParametersReverseTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectMultipleParametersReverseTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}, Header{{"hello", "world"}, {"key", "value"}, {"test", "case"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(std::string{"case"}, response.header["test"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterHeaderTests, HeaderReflectCaseInsensitiveParametersReverseTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}, {"three", "four"}, {"five", "six"}}, Header{{"HeLlO", "wOrLd"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two&three=four&five=six"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hello"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["HELLO"]); + EXPECT_EQ(std::string{"wOrLd"}, response.header["hElLo"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderAATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{}, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderABTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{}, Header{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderACTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"one", "two"}}, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderADTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"one", "two"}}, Header{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderAETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderAFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{}, Header{{"hello", "world"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderAGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"one", "two"}}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderAHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"one", "two"}}, Header{{"hello", "world"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Header{}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBBTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Header{}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBCTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Header{}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBDTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Header{}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Header{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Header{{"hello", "world"}}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Header{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderBHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Header{{"hello", "world"}}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Authentication{"user", "password", AuthMode::BASIC}, Parameters{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCBTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCCTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"one", "two"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCDTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"one", "two"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}, Parameters{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Authentication{"user", "password", AuthMode::BASIC}, Parameters{{"one", "two"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderCHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Authentication{"user", "bad_password", AuthMode::BASIC}, Parameters{{"one", "two"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{}, Parameters{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDBTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{}, Parameters{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDCTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{}, Parameters{{"one", "two"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDDTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{}, Parameters{{"one", "two"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{{"hello", "world"}}, Parameters{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{{"hello", "world"}}, Parameters{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{{"hello", "world"}}, Parameters{{"one", "two"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderDHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{{"hello", "world"}}, Parameters{{"one", "two"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Parameters{}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEBTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Parameters{}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderECTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Parameters{{"one", "two"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEDTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{}, Parameters{{"one", "two"}}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Parameters{}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Parameters{}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Parameters{{"one", "two"}}, Authentication{"user", "password", AuthMode::BASIC}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderEHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Header{{"hello", "world"}}, Parameters{{"one", "two"}}, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFATest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Authentication{"user", "password", AuthMode::BASIC}, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFBTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFCTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Authentication{"user", "password", AuthMode::BASIC}, Header{}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFDTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFETest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Authentication{"user", "password", AuthMode::BASIC}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFFTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{}, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{{"hello", "world"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFGTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Authentication{"user", "password", AuthMode::BASIC}, Header{{"hello", "world"}}); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicAuthenticationParameterHeaderTests, BasicAuthenticationParameterHeaderFHTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Get(url, Parameters{{"one", "two"}}, Authentication{"user", "bad_password", AuthMode::BASIC}, Header{{"hello", "world"}}); + EXPECT_EQ("Unauthorized", response.text); + EXPECT_EQ(Url{url + "?one=two"}, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(GetRedirectTests, RedirectTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Response response = cpr::Get(url, Redirect(false)); + std::string expected_text{"Moved Temporarily"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(302, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(GetRedirectTests, ZeroMaxRedirectsSuccessTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, Redirect(0L)); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(GetRedirectTests, ZeroMaxRedirectsFailureTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Response response = cpr::Get(url, Redirect(0L)); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(301, response.status_code); + EXPECT_EQ(ErrorCode::TOO_MANY_REDIRECTS, response.error.code); +} + +TEST(GetRedirectTests, BasicAuthenticationRedirectSuccessTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Response response = cpr::Get(url, Authentication{"user", "password", AuthMode::BASIC}, Header{{"RedirectLocation", "basic_auth.html"}}, Redirect(true, true)); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + std::string resultUrl = "http://user:password@127.0.0.1:" + std::to_string(server->GetPort()) + "/basic_auth.html"; + EXPECT_EQ(response.url, resultUrl); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, RequestBodyTest) { + Url url{server->GetBaseUrl() + "/body_get.html"}; + Body body{"message=abc123"}; + Response response = cpr::Get(url, body); + std::string expected_text{"abc123"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BasicTests, RequestBodyStringViewTest) { + Url url{server->GetBaseUrl() + "/body_get.html"}; + Body body{static_cast("message=abc123")}; + Response response = cpr::Get(url, body); + std::string expected_text{"abc123"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(LimitRateTests, HelloWorldTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, LimitRate(1024, 1024)); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/head_tests.cpp b/3rdparty/cpr/test/head_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6e3c00f66df9c104a161631658da6f2b7a9043b9 --- /dev/null +++ b/3rdparty/cpr/test/head_tests.cpp @@ -0,0 +1,226 @@ +#include +#include + +#include + +#include "cpr/cpr.h" + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(HeadTests, BasicHeadTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, ComplexHeadTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, ResourceNotFoundHeadTest) { + Url url{server->GetBaseUrl() + "/error.html"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(404, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, BadHostHeadTest) { + Url url{"http://bad_host/"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::COULDNT_RESOLVE_HOST, response.error.code); +} + +TEST(HeadTests, CookieHeadTest) { + Url url{server->GetBaseUrl() + "/basic_cookies.html"}; + Response response = cpr::Head(url); + cpr::Cookies expectedCookies{ + {"SID", "31d4d96e407aad42", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + {"lang", "en-US", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + }; + cpr::Cookies res_cookies{response.cookies}; + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + for (auto cookie = res_cookies.begin(), expectedCookie = expectedCookies.begin(); cookie != res_cookies.end() && expectedCookie != expectedCookies.end(); cookie++, expectedCookie++) { + EXPECT_EQ(expectedCookie->GetName(), cookie->GetName()); + EXPECT_EQ(expectedCookie->GetValue(), cookie->GetValue()); + EXPECT_EQ(expectedCookie->GetDomain(), cookie->GetDomain()); + EXPECT_EQ(expectedCookie->IsIncludingSubdomains(), cookie->IsIncludingSubdomains()); + EXPECT_EQ(expectedCookie->GetPath(), cookie->GetPath()); + EXPECT_EQ(expectedCookie->IsHttpsOnly(), cookie->IsHttpsOnly()); + EXPECT_EQ(expectedCookie->GetExpires(), cookie->GetExpires()); + } +} + +TEST(HeadTests, ParameterHeadTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Parameters parameters{{"key", "value"}}; + Response response = cpr::Head(url, parameters); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(Url{url + "?key=value"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, AuthenticationSuccessHeadTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Head(url, Authentication{"user", "password", AuthMode::BASIC}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, AuthenticationNullFailureHeadTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, AuthenticationFailureHeadTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Response response = cpr::Head(url, Authentication{"user", "bad_password", AuthMode::BASIC}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ("text/plain", response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, BearerSuccessHeadTest) { + Url url{server->GetBaseUrl() + "/bearer_token.html"}; +#if CPR_LIBCURL_VERSION_NUM >= 0x073D00 // 7.61.0 + Response response = cpr::Get(url, Bearer{"the_token"}); +#else + Response response = cpr::Get(url, Header{{"Authorization", "Bearer the_token"}}); +#endif + EXPECT_EQ(std::string{"Header reflect GET"}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, DigestSuccessHeadTest) { + Url url{server->GetBaseUrl() + "/digest_auth.html"}; + Response response = cpr::Head(url, Authentication{"user", "password", AuthMode::DIGEST}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, HeaderReflectNoneHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Head(url); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, HeaderReflectEmptyHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Head(url, Header{}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, HeaderReflectHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Head(url, Header{{"hello", "world"}}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, SetEmptyHeaderHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Head(url, Header{{"hello", ""}}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, RedirectHeadTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Response response = cpr::Head(url, Redirect(false)); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(302, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, ZeroMaxRedirectsHeadTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Head(url, Redirect(0L)); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(HeadTests, BasicHeadAsyncTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::HeadAsync(url)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/httpServer.cpp b/3rdparty/cpr/test/httpServer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..249ce76dbfc7dd86545b45ea57af33b42417ebfa --- /dev/null +++ b/3rdparty/cpr/test/httpServer.cpp @@ -0,0 +1,944 @@ +#include "httpServer.hpp" +#include +#include +#include +#include +#include + +namespace cpr { + +std::string HttpServer::GetBaseUrl() { + return "http://127.0.0.1:" + std::to_string(GetPort()); +} + +uint16_t HttpServer::GetPort() { + // Unassigned port number in the ephemeral range + return 61936; +} + +mg_connection* HttpServer::initServer(mg_mgr* mgr, mg_event_handler_t event_handler) { + // Based on: https://mongoose.ws/tutorials/http-server/ + mg_mgr_init(mgr); + std::string port = std::to_string(GetPort()); + mg_connection* c = mg_http_listen(mgr, GetBaseUrl().c_str(), event_handler, this); + if (!c) { + throw std::system_error(errno, std::system_category(), "Failed to listen at port " + port); + } + return c; +} + +void HttpServer::acceptConnection(mg_connection* /* conn */) {} + +void HttpServer::OnRequestHello(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"OPTIONS"}) { + OnRequestOptions(conn, msg); + } else { + std::string response{"Hello world!"}; + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequestRoot(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"OPTIONS"}) { + OnRequestOptions(conn, msg); + } else { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } +} + +void HttpServer::OnRequestNotFound(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"OPTIONS"}) { + OnRequestOptions(conn, msg); + } else { + std::string errorMessage{"Not Found"}; + SendError(conn, 404, errorMessage); + } +} + +void HttpServer::OnRequestOptions(mg_connection* conn, mg_http_message* /*msg*/) { + std::string headers = + "Content-Type: text/plain\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Credentials: true\r\n" + "Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS\r\n" + "Access-Control-Max-Age: 3600\r\n"; + + std::string response; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestTimeout(mg_connection* conn, mg_http_message* msg) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + OnRequestHello(conn, msg); +} + +void HttpServer::OnRequestLongTimeout(mg_connection* conn, mg_http_message* msg) { + std::this_thread::sleep_for(std::chrono::seconds(2)); + OnRequestHello(conn, msg); +} + +// Send the header, then send "Hello world!" every 100ms +// For this, we use a mongoose timer +void HttpServer::OnRequestLowSpeedTimeout(mg_connection* conn, mg_http_message* /* msg */, TimerArg* timer_arg) { + std::string response{"Hello world!"}; + mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n", response.length() * 20); + timer_arg->connection_id = conn->id; + mg_timer_init( + &timer_arg->mgr->timers, &timer_arg->timer, 100, MG_TIMER_REPEAT, + // The following lambda function gets executed each time the timer is called. + // It sends "Hello world!" to the client each 100ms at most 20 times. + [](void* arg) { + TimerArg* timer_arg = static_cast(arg); + if (timer_arg->counter < 20 && IsConnectionActive(timer_arg->mgr, timer_arg->connection) && timer_arg->connection->id == timer_arg->connection_id) { + std::string response{"Hello world!"}; + mg_send(timer_arg->connection, response.c_str(), response.length()); + ++timer_arg->counter; + } else { + timer_arg->counter = 20; // Make sure that this timer is never called again + } + }, + timer_arg); +} + +// Before and after calling an endpoint that calls this method, the test needs to wait until all previous connections are closed +// The nested call to mg_mgr_poll can lead to problems otherwise +void HttpServer::OnRequestLowSpeed(mg_connection* conn, mg_http_message* /*msg*/, mg_mgr* mgr) { + std::string response{"Hello world!"}; + mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n", response.length()); + mg_timer_add( + mgr, 2000, MG_TIMER_ONCE, + [](void* connection) { + std::string response{"Hello world!"}; + mg_send(static_cast(connection), response.c_str(), response.length()); + }, + conn); +} + +// Before and after calling an endpoint that calls this method, the test needs to wait until all previous connections are closed +// The nested call to mg_mgr_poll can lead to problems otherwise +void HttpServer::OnRequestLowSpeedBytes(mg_connection* conn, mg_http_message* /*msg*/, TimerArg* timer_arg) { + std::string response{'a'}; + mg_printf(conn, "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: %d\r\n\r\n", response.length() * 20); + + mg_timer_init( + &timer_arg->mgr->timers, &timer_arg->timer, 100, MG_TIMER_REPEAT, + // The following lambda function gets executed each time the timer is called. + // It first waits for 2 seconds, then sends "a" to the client each 100ms at most 20 times. + [](void* arg) { + TimerArg* timer_arg = static_cast(arg); + if (timer_arg->counter == 0) { + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + if (timer_arg->counter < 20 && IsConnectionActive(timer_arg->mgr, timer_arg->connection) && timer_arg->connection->id == timer_arg->connection_id) { + std::string response{'a'}; + mg_send(timer_arg->connection, response.c_str(), response.length()); + ++timer_arg->counter; + } else { + timer_arg->counter = 20; // Make sure that this timer is never called again + } + }, + timer_arg); +} + +void HttpServer::OnRequestBasicCookies(mg_connection* conn, mg_http_message* /*msg*/) { + const std::string expires = "Wed, 30 Sep 2093 03:18:00 GMT"; + + std::string cookie1{"SID=31d4d96e407aad42; Expires=" + expires + "; Secure"}; + std::string cookie2{"lang=en-US; Expires=" + expires + "; Secure"}; + std::string headers = + "Content-Type: text/html\r\n" + "Set-Cookie: " + + cookie1 + + "\r\n" + "Set-Cookie: " + + cookie2 + "\r\n"; + std::string response{"Basic Cookies"}; + + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestEmptyCookies(mg_connection* conn, mg_http_message* /*msg*/) { + const std::string expires = "Wed, 30 Sep 2093 03:18:00 GMT"; + + std::string cookie1{"SID=; Expires=" + expires + "; Secure"}; + std::string cookie2{"lang=; Expires=" + expires + "; Secure"}; + std::string headers = + "Content-Type: text/html\r\n" + "Set-Cookie: " + + cookie1 + + "\r\n" + "Set-Cookie: " + + cookie2 + "\r\n"; + std::string response{"Empty Cookies"}; + + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestCookiesReflect(mg_connection* conn, mg_http_message* msg) { + mg_str* request_cookies{nullptr}; + if ((request_cookies = mg_http_get_header(msg, "Cookie")) == nullptr) { + std::string errorMessage{"Cookie not found"}; + SendError(conn, 400, errorMessage); + return; + } + std::string cookie_str{request_cookies->ptr, request_cookies->len}; + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), cookie_str.c_str()); +} + +void HttpServer::OnRequestRedirectionWithChangingCookies(mg_connection* conn, mg_http_message* msg) { + const std::string expires = "Wed, 30 Sep 2093 03:18:00 GMT"; + + mg_str* request_cookies{nullptr}; + std::string cookie_str; + if ((request_cookies = mg_http_get_header(msg, "Cookie")) != nullptr) { + cookie_str = std::string{request_cookies->ptr, request_cookies->len}; + } + + if (cookie_str.find("SID=31d4d96e407aad42") == std::string::npos) { + std::string cookie1{"SID=31d4d96e407aad42; Expires=" + expires + "; Secure"}; + std::string cookie2{"lang=en-US; Expires=" + expires + "; Secure"}; + std::string headers = + "Content-Type: text/html\r\n" + "Location: http://127.0.0.1:61936/redirection_with_changing_cookies.html\r\n" + "Set-Cookie: " + + cookie1 + + "\r\n" + "Set-Cookie: " + + cookie2 + "\r\n"; + + mg_http_reply(conn, 302, headers.c_str(), ""); + } else { + cookie_str = "Received cookies are: " + cookie_str; + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), cookie_str.c_str()); + } +} + +void HttpServer::OnRequestBasicAuth(mg_connection* conn, mg_http_message* msg) { + mg_str* requested_auth; + std::string auth{"Basic"}; + if ((requested_auth = mg_http_get_header(msg, "Authorization")) == nullptr || mg_ncasecmp(requested_auth->ptr, auth.c_str(), auth.length()) != 0) { + std::string errorMessage{"Unauthorized"}; + SendError(conn, 401, errorMessage); + return; + } + std::string auth_string{requested_auth->ptr, requested_auth->len}; + size_t basic_token = auth_string.find(' ') + 1; + auth_string = auth_string.substr(basic_token, auth_string.length() - basic_token); + auth_string = Base64Decode(auth_string); + size_t colon = auth_string.find(':'); + std::string username = auth_string.substr(0, colon); + std::string password = auth_string.substr(colon + 1, auth_string.length() - colon - 1); + if (username == "user" && password == "password") { + OnRequestHeaderReflect(conn, msg); + } else { + std::string errorMessage{"Unauthorized"}; + SendError(conn, 401, errorMessage); + } +} + +void HttpServer::OnRequestBearerAuth(mg_connection* conn, mg_http_message* msg) { + mg_str* requested_auth; + std::string auth{"Bearer"}; + if ((requested_auth = mg_http_get_header(msg, "Authorization")) == nullptr || mg_ncasecmp(requested_auth->ptr, auth.c_str(), auth.length()) != 0) { + std::string errorMessage{"Unauthorized"}; + SendError(conn, 401, errorMessage); + return; + } + std::string auth_string{requested_auth->ptr, requested_auth->len}; + size_t basic_token = auth_string.find(' ') + 1; + auth_string = auth_string.substr(basic_token, auth_string.length() - basic_token); + if (auth_string == "the_token") { + OnRequestHeaderReflect(conn, msg); + } else { + std::string errorMessage{"Unauthorized"}; + SendError(conn, 401, errorMessage); + } +} + +void HttpServer::OnRequestBasicJson(mg_connection* conn, mg_http_message* /*msg*/) { + std::string response = + "[\n" + " {\n" + " \"first_key\": \"first_value\",\n" + " \"second_key\": \"second_value\"\n" + " }\n" + "]"; + std::string headers = "Content-Type: application/json\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestHeaderReflect(mg_connection* conn, mg_http_message* msg) { + std::string response = "Header reflect " + std::string{msg->method.ptr, msg->method.len}; + std::string headers; + bool hasContentTypeHeader = false; + for (const mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"Content-Type"} == name) { + hasContentTypeHeader = true; + } + + if (std::string{"Host"} != name && std::string{"Accept"} != name) { + if (header.value.ptr) { + headers.append(name + ": " + std::string(header.value.ptr, header.value.len) + "\r\n"); + } + } + } + + if (!hasContentTypeHeader) { + headers.append("Content-Type: text/html\r\n"); + } + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestTempRedirect(mg_connection* conn, mg_http_message* msg) { + // Get the requested target location: + std::string location; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"RedirectLocation"} == name) { + location = std::string(header.value.ptr, header.value.len); + break; + } + } + + // Check if the request contains a valid location, else default to 'hello.html': + if (location.empty()) { + location = "hello.html"; + } + std::string headers = "Location: " + location + "\r\n"; + std::string response = "Moved Temporarily"; + mg_http_reply(conn, 302, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestPermRedirect(mg_connection* conn, mg_http_message* msg) { + // Get the requested target location: + std::string location; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"RedirectLocation"} == name) { + location = std::string(header.value.ptr, header.value.len); + break; + } + } + + // Check if the request contains a valid location, else default to 'hello.html': + if (location.empty()) { + location = "hello.html"; + } + std::string headers = "Location: " + location + "\r\n"; + std::string response = "Moved Permanently"; + + mg_http_reply(conn, 301, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestResolvePermRedirect(mg_connection* conn, mg_http_message* msg) { + // Get the requested target location: + std::string location; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"RedirectLocation"} == name) { + location = std::string(header.value.ptr, header.value.len); + break; + } + } + + if (location.empty()) { + std::string errorMessage{"Redirect location missing"}; + SendError(conn, 405, errorMessage); + return; + } + + std::string headers = "Location: " + location + "\r\n"; + std::string response = "Moved Permanently"; + + mg_http_reply(conn, 301, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestTwoRedirects(mg_connection* conn, mg_http_message* /*msg*/) { + std::string response = "Moved Permanently"; + std::string headers = "Location: permanent_redirect.html\r\n"; + mg_http_reply(conn, 301, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestUrlPost(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} != std::string{"POST"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + return; + } + + std::string headers = "Content-Type: application/json\r\n"; + + std::array x{0}; + std::array y{0}; + mg_http_get_var(&(msg->body), "x", x.data(), x.size()); + mg_http_get_var(&(msg->body), "y", y.data(), y.size()); + std::string x_string{x.data()}; + std::string y_string{y.data()}; + std::string response; + if (y_string.empty()) { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + "\n" + "}"}; + } else { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + ",\n" + " \"y\": " + + y_string + + ",\n" + " \"sum\": " + + std::to_string(atoi(x.data()) + atoi(y.data())) + + "\n" + "}"}; + } + mg_http_reply(conn, 201, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestBodyGet(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} != std::string{"GET"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + return; + } + std::array message{}; + mg_http_get_var(&(msg->body), "message", message.data(), message.size()); + if (msg->body.len <= 0) { + std::string errorMessage{"No Content"}; + SendError(conn, 405, errorMessage); + return; + } + std::string response = message.data(); + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestJsonPost(mg_connection* conn, mg_http_message* msg) { + mg_str* content_type{nullptr}; + if ((content_type = mg_http_get_header(msg, "Content-Type")) == nullptr || std::string{content_type->ptr, content_type->len} != "application/json") { + std::string errorMessage{"Unsupported Media Type"}; + SendError(conn, 415, errorMessage); + return; + } + + std::string headers = "Content-Type: application/json\r\n"; + mg_http_reply(conn, 201, headers.c_str(), msg->body.ptr); +} + +void HttpServer::OnRequestFormPost(mg_connection* conn, mg_http_message* msg) { + size_t pos{0}; + mg_http_part part{}; + + std::string headers = "Content-Type: application/json\r\n"; + std::string response{}; + response += "{\n"; + while ((pos = mg_http_next_multipart(msg->body, pos, &part)) > 0) { + response += " \"" + std::string(part.name.ptr, part.name.len) + "\": \""; + if (!std::string(part.filename.ptr, part.filename.len).empty()) { + response += std::string(part.filename.ptr, part.filename.len) + "="; + } + response += std::string(part.body.ptr, part.body.len) + "\",\n"; + } + response.erase(response.find_last_not_of(",\n") + 1); + response += "\n}"; + + mg_http_reply(conn, 201, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestFileUploadPost(mg_connection* conn, mg_http_message* msg) { + size_t pos{0}; + mg_http_part part{}; + + std::string headers = "Content-Type: application/json\r\n"; + std::string response{}; + response += "{\n"; + while ((pos = mg_http_next_multipart(msg->body, pos, &part)) > 0) { + response += " \"" + std::string(part.name.ptr, part.name.len) + "\": \""; + if (!std::string(part.filename.ptr, part.filename.len).empty()) { + response += std::string(part.filename.ptr, part.filename.len) + "="; + } + response += std::string(part.body.ptr, part.body.len) + "\",\n"; + } + response.erase(response.find_last_not_of(",\n") + 1); + response += "\n}"; + + mg_http_reply(conn, 201, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestDelete(mg_connection* conn, mg_http_message* msg) { + bool has_json_header = false; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + std::string value = std::string(header.value.ptr, header.value.len); + if (std::string{"Content-Type"} == name && std::string{"application/json"} == value) { + has_json_header = true; + break; + } + } + if (std::string{msg->method.ptr, msg->method.len} == std::string{"DELETE"}) { + std::string headers; + std::string response = "Patch success"; + if (!has_json_header) { + headers = "Content-Type: text/html\r\n"; + response = "Delete success"; + } else { + headers = "Content-Type: application/json\r\n"; + response = std::string{msg->body.ptr, msg->body.len}; + } + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } else { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } +} + +void HttpServer::OnRequestDeleteNotAllowed(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"DELETE"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } else { + std::string headers = "Content-Type: text/html\r\n"; + std::string response = "Delete success"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequestPut(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"PUT"}) { + std::array x{0}; + std::array y{0}; + mg_http_get_var(&(msg->body), "x", x.data(), x.size()); + mg_http_get_var(&(msg->body), "y", y.data(), y.size()); + std::string x_string{x.data()}; + std::string y_string{y.data()}; + std::string headers = "Content-Type: application/json\r\n"; + std::string response; + if (y_string.empty()) { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + "\n" + "}"}; + } else { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + ",\n" + " \"y\": " + + y_string + + ",\n" + " \"sum\": " + + std::to_string(atoi(x.data()) + atoi(y.data())) + + "\n" + "}"}; + } + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } else { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } +} + +void HttpServer::OnRequestPostReflect(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} != std::string{"POST"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } + + std::string response = std::string{msg->body.ptr, msg->body.len}; + std::string headers; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name{header.name.ptr, header.name.len}; + if (std::string{"Host"} != name && std::string{"Accept"} != name) { + if (header.value.ptr) { + headers.append(name + ": " + std::string(header.value.ptr, header.value.len) + "\r\n"); + } + } + } + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestPutNotAllowed(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"PUT"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } else { + std::string headers = "Content-Type: text/html\r\n"; + std::string response = "Delete success"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequestPatch(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"PATCH"}) { + std::array x{0}; + std::array y{0}; + mg_http_get_var(&(msg->body), "x", x.data(), x.size()); + mg_http_get_var(&(msg->body), "y", y.data(), y.size()); + std::string x_string{x.data()}; + std::string y_string{y.data()}; + std::string headers = "Content-Type: application/json\r\n"; + std::string response; + if (y_string.empty()) { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + "\n" + "}"}; + } else { + response = std::string{ + "{\n" + " \"x\": " + + x_string + + ",\n" + " \"y\": " + + y_string + + ",\n" + " \"sum\": " + + std::to_string(atoi(x.data()) + atoi(y.data())) + + "\n" + "}"}; + } + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } else { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } +} + +void HttpServer::OnRequestPatchNotAllowed(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"PATCH"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } else { + std::string headers = "Content-Type: text/html\r\n"; + std::string response = "Delete success"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequestDownloadGzip(mg_connection* conn, mg_http_message* msg) { + if (std::string{msg->method.ptr, msg->method.len} == std::string{"DOWNLOAD"}) { + std::string errorMessage{"Method Not Allowed"}; + SendError(conn, 405, errorMessage); + } else { + std::string encoding; + std::string range; + std::vector> ranges; + + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"Accept-Encoding"} == name) { + encoding = std::string(header.value.ptr, header.value.len); + } else if (std::string{"Range"} == name) { + range = std::string(header.value.ptr, header.value.len); + } + } + if (encoding.find("gzip") == std::string::npos) { + std::string errorMessage{"Invalid encoding: " + encoding}; + SendError(conn, 405, errorMessage); + return; + } + if (!range.empty()) { + std::string::size_type eq_pos = range.find('='); + if (eq_pos == std::string::npos) { + std::string errorMessage{"Invalid range header: " + range}; + SendError(conn, 405, errorMessage); + return; + } + + int64_t current_start_index = eq_pos + 1; + int64_t current_end_index; + std::string::size_type range_len = range.length(); + std::string::size_type com_pos; + std::string::size_type sep_pos; + bool more_ranges_exists; + + do { + com_pos = range.find(',', current_start_index); + if (com_pos < range_len) { + current_end_index = com_pos - 1; + } else { + current_end_index = range_len - 1; + } + + std::pair current_range{0, -1}; + + sep_pos = range.find('-', current_start_index); + if (sep_pos == std::string::npos) { + std::string errorMessage{"Invalid range format " + range.substr(current_start_index, current_end_index)}; + SendError(conn, 405, errorMessage); + return; + } + if (sep_pos == eq_pos + 1) { + std::string errorMessage{"Suffix ranage not supported: " + range.substr(current_start_index, current_end_index)}; + SendError(conn, 405, errorMessage); + return; + } + + current_range.first = std::strtoll(range.substr(current_start_index, sep_pos - 1).c_str(), nullptr, 10); + if (current_range.first == LLONG_MAX || current_range.first == LLONG_MIN) { + std::string errorMessage{"Start range is invalid number: " + range.substr(current_start_index, current_end_index)}; + SendError(conn, 405, errorMessage); + return; + } + + std::string er_str = range.substr(sep_pos + 1, current_end_index); + if (!er_str.empty()) { + current_range.second = std::strtoll(er_str.c_str(), nullptr, 10); + if (current_range.second == 0 || current_range.second == LLONG_MAX || current_range.second == LLONG_MIN) { + std::string errorMessage{"End range is invalid number: " + range.substr(current_start_index, current_end_index)}; + SendError(conn, 405, errorMessage); + return; + } + } + + ranges.push_back(current_range); + + if (current_end_index >= static_cast(range.length() - 1)) { + more_ranges_exists = false; + } else { + // Multiple ranges are separated by ', ' + more_ranges_exists = true; + current_start_index = current_end_index + 3; + } + } while (more_ranges_exists); + } + + std::string response = "Download!"; + int status_code = 200; + std::string headers; + + if (!ranges.empty()) { + // Create response parts + std::vector responses; + for (std::pair local_range : ranges) { + if (local_range.first >= 0) { + if (local_range.first >= (int64_t) response.length()) { + responses.push_back(""); + } else if (local_range.second == -1 || local_range.second >= (int64_t) response.length()) { + responses.push_back(response.substr(local_range.first)); + } else { + responses.push_back(response.substr(local_range.first, local_range.second - local_range.first + 1)); + } + } + } + + if (responses.size() > 1) { + // Create mime multipart response + std::string boundary = "3d6b6a416f9b5"; + status_code = 206; + response.clear(); + + for (size_t i{0}; i < responses.size(); ++i) { + response += "--" + boundary + "\n"; + response += "Content-Range: bytes " + std::to_string(ranges.at(i).first) + "-"; + if (ranges.at(i).second > 0) { + response += std::to_string(ranges.at(i).second); + } else { + response += std::to_string(responses.at(i).length()); + } + response += "/" + std::to_string(responses.at(i).length()) + "\n\n"; + response += responses.at(i) + "\n"; + } + response += "--" + boundary + "--"; + } else { + if (ranges.at(0).second == -1 || ranges.at(0).second >= (int64_t) response.length()) { + status_code = ranges.at(0).first > 0 ? 206 : 200; + } else { + status_code = 206; + } + response = responses.at(0); + + if (status_code == 206) { + headers = "Content-Range: bytes " + std::to_string(ranges.at(0).first) + "-"; + if (ranges.at(0).second > 0) { + headers += std::to_string(ranges.at(0).second); + } else { + headers += std::to_string(response.length()); + } + headers += "/" + std::to_string(response.length()); + } + } + } + if (!headers.empty()) { + headers += "\r\n"; + } + + mg_http_reply(conn, status_code, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequestCheckAcceptEncoding(mg_connection* conn, mg_http_message* msg) { + std::string response; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"Accept-Encoding"} == name) { + response = std::string(header.value.ptr, header.value.len); + } + } + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestCheckExpect100Continue(mg_connection* conn, mg_http_message* msg) { + std::string response; + for (mg_http_header& header : msg->headers) { + if (!header.name.ptr) { + continue; + } + std::string name = std::string(header.name.ptr, header.name.len); + if (std::string{"Expect"} == name) { + response = std::string(header.value.ptr, header.value.len); + } + } + std::string headers = "Content-Type: text/html\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +void HttpServer::OnRequestGetDownloadFileLength(mg_connection* conn, mg_http_message* msg) { + auto method = std::string{msg->method.ptr, msg->method.len}; + if (method == std::string{"HEAD"}) { + mg_http_reply(conn, 405, nullptr, ""); + } else { + std::string response("this is a file content."); + std::string headers = "Content-Type: text/plain\r\n"; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); + } +} + +void HttpServer::OnRequest(mg_connection* conn, mg_http_message* msg) { + std::string uri = std::string(msg->uri.ptr, msg->uri.len); + if (uri == "/") { + OnRequestRoot(conn, msg); + } else if (uri == "/hello.html") { + OnRequestHello(conn, msg); + } else if (uri == "/timeout.html") { + OnRequestTimeout(conn, msg); + } else if (uri == "/long_timeout.html") { + OnRequestLongTimeout(conn, msg); + } else if (uri == "/low_speed_timeout.html") { + timer_args.emplace_back(std::make_unique(&mgr, conn, mg_timer{})); + OnRequestLowSpeedTimeout(conn, msg, timer_args.back().get()); + } else if (uri == "/low_speed.html") { + OnRequestLowSpeed(conn, msg, &mgr); + } else if (uri == "/low_speed_bytes.html") { + timer_args.emplace_back(std::make_unique(&mgr, conn, mg_timer{})); + OnRequestLowSpeedBytes(conn, msg, timer_args.back().get()); + } else if (uri == "/basic_cookies.html") { + OnRequestBasicCookies(conn, msg); + } else if (uri == "/empty_cookies.html") { + OnRequestEmptyCookies(conn, msg); + } else if (uri == "/cookies_reflect.html") { + OnRequestCookiesReflect(conn, msg); + } else if (uri == "/redirection_with_changing_cookies.html") { + OnRequestRedirectionWithChangingCookies(conn, msg); + } else if (uri == "/basic_auth.html") { + OnRequestBasicAuth(conn, msg); + } else if (uri == "/bearer_token.html") { + OnRequestBearerAuth(conn, msg); + } else if (uri == "/digest_auth.html") { + OnRequestHeaderReflect(conn, msg); + } else if (uri == "/basic.json") { + OnRequestBasicJson(conn, msg); + } else if (uri == "/header_reflect.html") { + OnRequestHeaderReflect(conn, msg); + } else if (uri == "/temporary_redirect.html") { + OnRequestTempRedirect(conn, msg); + } else if (uri == "/permanent_redirect.html") { + OnRequestPermRedirect(conn, msg); + } else if (uri == "/resolve_permanent_redirect.html") { + OnRequestResolvePermRedirect(conn, msg); + } else if (uri == "/two_redirects.html") { + OnRequestTwoRedirects(conn, msg); + } else if (uri == "/url_post.html") { + OnRequestUrlPost(conn, msg); + } else if (uri == "/body_get.html") { + OnRequestBodyGet(conn, msg); + } else if (uri == "/json_post.html") { + OnRequestJsonPost(conn, msg); + } else if (uri == "/form_post.html") { + OnRequestFormPost(conn, msg); + } else if (uri == "/post_file_upload.html") { + OnRequestFileUploadPost(conn, msg); + } else if (uri == "/post_reflect.html") { + OnRequestPostReflect(conn, msg); + } else if (uri == "/delete.html") { + OnRequestDelete(conn, msg); + } else if (uri == "/delete_unallowed.html") { + OnRequestDeleteNotAllowed(conn, msg); + } else if (uri == "/put.html") { + OnRequestPut(conn, msg); + } else if (uri == "/put_unallowed.html") { + OnRequestPutNotAllowed(conn, msg); + } else if (uri == "/patch.html") { + OnRequestPatch(conn, msg); + } else if (uri == "/patch_unallowed.html") { + OnRequestPatchNotAllowed(conn, msg); + } else if (uri == "/download_gzip.html") { + OnRequestDownloadGzip(conn, msg); + } else if (uri == "/local_port.html") { + OnRequestLocalPort(conn, msg); + } else if (uri == "/check_accept_encoding.html") { + OnRequestCheckAcceptEncoding(conn, msg); + } else if (uri == "/check_expect_100_continue.html") { + OnRequestCheckExpect100Continue(conn, msg); + } else if (uri == "/get_download_file_length.html") { + OnRequestGetDownloadFileLength(conn, msg); + } else { + OnRequestNotFound(conn, msg); + } +} + +void HttpServer::OnRequestLocalPort(mg_connection* conn, mg_http_message* /*msg*/) { + // send source port number as response for checking SetLocalPort/SetLocalPortRange + std::string headers = "Content-Type: text/plain\r\n"; + // Convert from big endian to little endian + std::string response = std::to_string(AbstractServer::GetRemotePort(conn)); + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +} // namespace cpr diff --git a/3rdparty/cpr/test/httpServer.hpp b/3rdparty/cpr/test/httpServer.hpp new file mode 100644 index 0000000000000000000000000000000000000000..7941f0f29b6fd754fa824a222fd5e70eca1a2f14 --- /dev/null +++ b/3rdparty/cpr/test/httpServer.hpp @@ -0,0 +1,67 @@ +#ifndef CPR_TEST_HTTP_SERVER_H +#define CPR_TEST_HTTP_SERVER_H + +#include +#include + +#include "abstractServer.hpp" +#include "cpr/cpr.h" +#include "mongoose.h" + +namespace cpr { +class HttpServer : public AbstractServer { + public: + ~HttpServer() override = default; + + std::string GetBaseUrl() override; + uint16_t GetPort() override; + + void OnRequest(mg_connection* conn, mg_http_message* msg) override; + + private: + static void OnRequestHello(mg_connection* conn, mg_http_message* msg); + static void OnRequestRoot(mg_connection* conn, mg_http_message* msg); + static void OnRequestOptions(mg_connection* conn, mg_http_message* msg); + static void OnRequestNotFound(mg_connection* conn, mg_http_message* msg); + static void OnRequestTimeout(mg_connection* conn, mg_http_message* msg); + static void OnRequestLongTimeout(mg_connection* conn, mg_http_message* msg); + static void OnRequestLowSpeedTimeout(mg_connection* conn, mg_http_message* msg, TimerArg* arg); + static void OnRequestLowSpeed(mg_connection* conn, mg_http_message* msg, mg_mgr* mgr); + static void OnRequestLowSpeedBytes(mg_connection* conn, mg_http_message* msg, TimerArg* arg); + static void OnRequestBasicCookies(mg_connection* conn, mg_http_message* msg); + static void OnRequestEmptyCookies(mg_connection* conn, mg_http_message* msg); + static void OnRequestCookiesReflect(mg_connection* conn, mg_http_message* msg); + static void OnRequestRedirectionWithChangingCookies(mg_connection* conn, mg_http_message* msg); + static void OnRequestBasicAuth(mg_connection* conn, mg_http_message* msg); + static void OnRequestBearerAuth(mg_connection* conn, mg_http_message* msg); + static void OnRequestBasicJson(mg_connection* conn, mg_http_message* msg); + static void OnRequestHeaderReflect(mg_connection* conn, mg_http_message* msg); + static void OnRequestTempRedirect(mg_connection* conn, mg_http_message* msg); + static void OnRequestPermRedirect(mg_connection* conn, mg_http_message* msg); + static void OnRequestResolvePermRedirect(mg_connection* conn, mg_http_message* msg); + static void OnRequestTwoRedirects(mg_connection* conn, mg_http_message* msg); + static void OnRequestUrlPost(mg_connection* conn, mg_http_message* msg); + static void OnRequestPostReflect(mg_connection* conn, mg_http_message* msg); + static void OnRequestBodyGet(mg_connection* conn, mg_http_message* msg); + static void OnRequestJsonPost(mg_connection* conn, mg_http_message* msg); + static void OnRequestFormPost(mg_connection* conn, mg_http_message* msg); + static void OnRequestFileUploadPost(mg_connection* conn, mg_http_message* msg); + static void OnRequestDelete(mg_connection* conn, mg_http_message* msg); + static void OnRequestDeleteNotAllowed(mg_connection* conn, mg_http_message* msg); + static void OnRequestPut(mg_connection* conn, mg_http_message* msg); + static void OnRequestPutNotAllowed(mg_connection* conn, mg_http_message* msg); + static void OnRequestPatch(mg_connection* conn, mg_http_message* msg); + static void OnRequestPatchNotAllowed(mg_connection* conn, mg_http_message* msg); + static void OnRequestDownloadGzip(mg_connection* conn, mg_http_message* msg); + static void OnRequestLocalPort(mg_connection* conn, mg_http_message* msg); + static void OnRequestCheckAcceptEncoding(mg_connection* conn, mg_http_message* msg); + static void OnRequestCheckExpect100Continue(mg_connection* conn, mg_http_message* msg); + static void OnRequestGetDownloadFileLength(mg_connection* conn, mg_http_message* msg); + + protected: + mg_connection* initServer(mg_mgr* mgr, mg_event_handler_t event_handler) override; + void acceptConnection(mg_connection* conn) override; +}; +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/test/httpsServer.cpp b/3rdparty/cpr/test/httpsServer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..401a47032b3830ec8d0ba764ab8ca3411ac30f9e --- /dev/null +++ b/3rdparty/cpr/test/httpsServer.cpp @@ -0,0 +1,65 @@ +#include "httpsServer.hpp" +#include + +namespace cpr { +HttpsServer::HttpsServer(fs::path&& baseDirPath, fs::path&& sslCertFileName, fs::path&& sslKeyFileName) : baseDirPath(baseDirPath.make_preferred().string()), sslCertFileName(sslCertFileName.make_preferred().string()), sslKeyFileName(sslKeyFileName.make_preferred().string()) { + // See https://mongoose.ws/tutorials/tls/ + memset(static_cast(&tlsOpts), 0, sizeof(tlsOpts)); + tlsOpts.cert = this->sslCertFileName.c_str(); + tlsOpts.certkey = this->sslKeyFileName.c_str(); +} + +std::string HttpsServer::GetBaseUrl() { + return "https://127.0.0.1:" + std::to_string(GetPort()); +} + +uint16_t HttpsServer::GetPort() { + // Unassigned port in the ephemeral range + return 61937; +} + +mg_connection* HttpsServer::initServer(mg_mgr* mgr, mg_event_handler_t event_handler) { + mg_mgr_init(mgr); + + std::string port = std::to_string(GetPort()); + mg_connection* c = mg_http_listen(mgr, GetBaseUrl().c_str(), event_handler, this); + return c; +} + +void HttpsServer::acceptConnection(mg_connection* conn) { + // See https://mongoose.ws/tutorials/tls/ + mg_tls_init(conn, &tlsOpts); +} + +void HttpsServer::OnRequest(mg_connection* conn, mg_http_message* msg) { + std::string uri = std::string(msg->uri.ptr, msg->uri.len); + if (uri == "/hello.html") { + OnRequestHello(conn, msg); + } else { + OnRequestNotFound(conn, msg); + } +} + +void HttpsServer::OnRequestNotFound(mg_connection* conn, mg_http_message* /*msg*/) { + mg_http_reply(conn, 404, nullptr, "Not Found"); +} + +void HttpsServer::OnRequestHello(mg_connection* conn, mg_http_message* /*msg*/) { + std::string response{"Hello world!"}; + std::string headers{"Content-Type: text/html\r\n"}; + mg_http_reply(conn, 200, headers.c_str(), response.c_str()); +} + +const std::string& HttpsServer::getBaseDirPath() const { + return baseDirPath; +} + +const std::string& HttpsServer::getSslCertFileName() const { + return sslCertFileName; +} + +const std::string& HttpsServer::getSslKeyFileName() const { + return sslKeyFileName; +} + +} // namespace cpr diff --git a/3rdparty/cpr/test/httpsServer.hpp b/3rdparty/cpr/test/httpsServer.hpp new file mode 100644 index 0000000000000000000000000000000000000000..a670113f7311008f3393474927cb54794551223d --- /dev/null +++ b/3rdparty/cpr/test/httpsServer.hpp @@ -0,0 +1,42 @@ +#ifndef CPR_TEST_HTTPS_SERVER_H +#define CPR_TEST_HTTPS_SERVER_H + +#include +#include + +#include "abstractServer.hpp" +#include "cpr/cpr.h" +#include "cpr/filesystem.h" +#include "mongoose.h" + +namespace cpr { +class HttpsServer : public AbstractServer { + private: + // We don't use fs::path here, as this leads to problems using windows + const std::string baseDirPath; + const std::string sslCertFileName; + const std::string sslKeyFileName; + struct mg_tls_opts tlsOpts; + + public: + explicit HttpsServer(fs::path&& baseDirPath, fs::path&& sslCertFileName, fs::path&& sslKeyFileName); + ~HttpsServer() override = default; + + std::string GetBaseUrl() override; + uint16_t GetPort() override; + + void OnRequest(mg_connection* conn, mg_http_message* msg) override; + static void OnRequestHello(mg_connection* conn, mg_http_message* msg); + static void OnRequestNotFound(mg_connection* conn, mg_http_message* msg); + + const std::string& getBaseDirPath() const; + const std::string& getSslCertFileName() const; + const std::string& getSslKeyFileName() const; + + protected: + mg_connection* initServer(mg_mgr* mgr, mg_event_handler_t event_handler) override; + void acceptConnection(mg_connection* conn) override; +}; +} // namespace cpr + +#endif diff --git a/3rdparty/cpr/test/interceptor_multi_tests.cpp b/3rdparty/cpr/test/interceptor_multi_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9dfd2dbd39d761b45c7ce442ddc76e85cc100a49 --- /dev/null +++ b/3rdparty/cpr/test/interceptor_multi_tests.cpp @@ -0,0 +1,470 @@ +#include +#include +#include + +#include "cpr/cpr.h" +#include "cpr/interceptor.h" +#include "cpr/response.h" +#include "cpr/session.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +class HiddenHelloWorldRedirectInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + std::shared_ptr session = multi.GetSessions().front().first; + + // Save original url + Url old_url = session->GetFullRequestUrl(); + + // Rewrite the url + Url url{server->GetBaseUrl() + "/hello.html"}; + session->SetUrl(url); + + // Proceed the chain + std::vector response = proceed(multi); + EXPECT_FALSE(response.empty()); + + // Restore the url again + response.front().url = old_url; + return response; + } +}; + +class ChangeStatusCodeInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + std::shared_ptr session = multi.GetSessions().front().first; + + // Proceed the chain + std::vector response = proceed(multi); + EXPECT_FALSE(response.empty()); + + // Change the status code + response.front().status_code = 12345; + return response; + } +}; + +class SetBasicAuthInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + std::shared_ptr session = multi.GetSessions().front().first; + + // Set authentication + session->SetAuth(Authentication{"user", "password", AuthMode::BASIC}); + + // Proceed the chain + return proceed(multi); + } +}; + +class SetUnsupportedProtocolErrorInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + std::shared_ptr session = multi.GetSessions().front().first; + + // Proceed the chain + std::vector response = proceed(multi); + EXPECT_FALSE(response.empty()); + + // Set unsupported protocol error + response.front().error = Error{CURLE_UNSUPPORTED_PROTOCOL, "SetErrorInterceptorMulti"}; + + // Return response + return response; + } +}; + +class ChangeRequestMethodToGetInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::GET_REQUEST; + + return proceed(multi); + } +}; + +class ChangeRequestMethodToPostInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::POST_REQUEST; + multi.GetSessions().front().first->SetOption(Payload{{"x", "5"}}); + return proceed(multi); + } +}; + +class ChangeRequestMethodToPutInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::PUT_REQUEST; + multi.GetSessions().front().first->SetOption(Payload{{"x", "5"}}); + return proceed(multi); + } +}; + +class ChangeRequestMethodToDeleteInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::DELETE_REQUEST; + return proceed(multi); + } +}; + +bool write_data(const std::string_view& /*data*/, intptr_t /*userdata*/) { + return true; +} + +class ChangeRequestMethodToDownloadCallbackInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::DOWNLOAD_REQUEST; + PrepareDownloadSession(multi, 0, WriteCallback{write_data, 0}); + return proceed(multi); + } +}; + +class ChangeRequestMethodToHeadInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::HEAD_REQUEST; + return proceed(multi); + } +}; + +class ChangeRequestMethodToOptionsInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::OPTIONS_REQUEST; + return proceed(multi); + } +}; + +class ChangeRequestMethodToPatchInterceptorMulti : public InterceptorMulti { + public: + std::vector intercept(MultiPerform& multi) override { + EXPECT_FALSE(multi.GetSessions().empty()); + multi.GetSessions().front().second = MultiPerform::HttpMethod::PATCH_REQUEST; + multi.GetSessions().front().first->SetOption(Payload{{"x", "5"}}); + return proceed(multi); + } +}; + +TEST(InterceptorMultiTest, HiddenUrlRewriteInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeStatusCodeInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + long expected_status_code{12345}; + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(expected_status_code, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, DownloadChangeStatusCodeInterceptorMultiTest) { + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Download(cpr::WriteCallback{write_data, 0}); + EXPECT_EQ(response.size(), 1); + + long expected_status_code{12345}; + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(expected_status_code, response.front().status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, SetBasicAuthInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"text/html"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, SetUnsupportedProtocolErrorInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_error_message{"SetErrorInterceptorMulti"}; + ErrorCode expected_error_code{ErrorCode::UNSUPPORTED_PROTOCOL}; + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"text/html"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(expected_error_message, response.front().error.message); + EXPECT_EQ(expected_error_code, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToGetInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Head(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"text/html"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToPostInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Head(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"application/json"}, response.front().header["content-type"]); + EXPECT_EQ(201, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToPutInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"application/json"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToPatchInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"application/json"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToOptionsInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.front().header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToHeadInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + EXPECT_EQ(std::string{}, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"text/html"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToDownloadCallbackInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/download_gzip.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session->SetTimeout(Timeout{2000}); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Put(); + EXPECT_EQ(response.size(), 1); + + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToDownloadCallbackInterceptorMultiMixTest) { + Url url{server->GetBaseUrl() + "/download_gzip.html"}; + std::shared_ptr session1 = std::make_shared(); + session1->SetUrl(url); + session1->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session1->SetTimeout(Timeout{2000}); + + std::shared_ptr session2 = std::make_shared(); + session2->SetUrl(url); + session2->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session2->SetTimeout(Timeout{2000}); + + MultiPerform multi; + multi.AddSession(session1); + multi.AddSession(session2); + // Changes only one of two sessions to download, so it is expected to throw an exception here since we can not mix them. + multi.AddInterceptor(std::make_shared()); + EXPECT_THROW(multi.Put(), std::invalid_argument); +} + +TEST(InterceptorMultiTest, ChangeRequestMethodToDeleteInterceptorMultiTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(std::string{"text/html"}, response.front().header["content-type"]); + EXPECT_EQ(200, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, TwoInterceptorMultisTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Hello world!"}; + long expected_status_code{12345}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(expected_status_code, response.front().status_code); + EXPECT_EQ(ErrorCode::OK, response.front().error.code); +} + +TEST(InterceptorMultiTest, ThreeInterceptorMultisTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multi; + multi.AddSession(session); + multi.AddInterceptor(std::make_shared()); + multi.AddInterceptor(std::make_shared()); + multi.AddInterceptor(std::make_shared()); + std::vector response = multi.Get(); + EXPECT_EQ(response.size(), 1); + + std::string expected_text{"Hello world!"}; + long expected_status_code{12345}; + std::string expected_error_message{"SetErrorInterceptorMulti"}; + ErrorCode expected_error_code{ErrorCode::UNSUPPORTED_PROTOCOL}; + EXPECT_EQ(expected_text, response.front().text); + EXPECT_EQ(url, response.front().url); + EXPECT_EQ(expected_status_code, response.front().status_code); + EXPECT_EQ(expected_error_message, response.front().error.message); + EXPECT_EQ(expected_error_code, response.front().error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/3rdparty/cpr/test/interceptor_tests.cpp b/3rdparty/cpr/test/interceptor_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c3261054c26d22baef77f1152e393e70a6e2572d --- /dev/null +++ b/3rdparty/cpr/test/interceptor_tests.cpp @@ -0,0 +1,408 @@ +#include +#include + +#include "cpr/cpr.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +class HiddenHelloWorldRedirectInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + // Save original url + Url old_url = session.GetFullRequestUrl(); + + // Rewrite the url + Url url{server->GetBaseUrl() + "/hello.html"}; + session.SetUrl(url); + + // Proceed the chain + Response response = proceed(session); + + // Restore the url again + response.url = old_url; + return response; + } +}; + +class ChangeStatusCodeInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + // Proceed the chain + Response response = proceed(session); + + // Change the status code + response.status_code = 12345; + return response; + } +}; + +class SetBasicAuthInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + // Set authentication + session.SetAuth(Authentication{"user", "password", AuthMode::BASIC}); + + // Proceed the chain + return proceed(session); + } +}; + +class SetUnsupportedProtocolErrorInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + // Proceed the chain + Response response = proceed(session); + + // Set unsupported protocol error + response.error = Error{CURLE_UNSUPPORTED_PROTOCOL, "SetErrorInterceptor"}; + + // Return response + return response; + } +}; + +class ChangeRequestMethodToGetInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + return proceed(session, Interceptor::ProceedHttpMethod::GET_REQUEST); + } +}; + +class ChangeRequestMethodToPostInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + session.SetOption(Payload{{"x", "5"}}); + return proceed(session, Interceptor::ProceedHttpMethod::POST_REQUEST); + } +}; + +class ChangeRequestMethodToPutInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + session.SetOption(Payload{{"x", "5"}}); + return proceed(session, Interceptor::ProceedHttpMethod::PUT_REQUEST); + } +}; + +class ChangeRequestMethodToDeleteInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + return proceed(session, Interceptor::ProceedHttpMethod::DELETE_REQUEST); + } +}; + +bool write_data(const std::string_view& /*data*/, intptr_t /*userdata*/) { + return true; +} + +class ChangeRequestMethodToDownloadCallbackInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + return proceed(session, Interceptor::ProceedHttpMethod::DOWNLOAD_CALLBACK_REQUEST, WriteCallback{write_data, 0}); + } +}; + +class ChangeRequestMethodToHeadInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + return proceed(session, Interceptor::ProceedHttpMethod::HEAD_REQUEST); + } +}; + +class ChangeRequestMethodToOptionsInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + return proceed(session, Interceptor::ProceedHttpMethod::OPTIONS_REQUEST); + } +}; + +class ChangeRequestMethodToPatchInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + session.SetOption(Payload{{"x", "5"}}); + return proceed(session, Interceptor::ProceedHttpMethod::PATCH_REQUEST); + } +}; + +class RetryInterceptor : public Interceptor { + public: + Response intercept(Session& session) override { + // Proceed the chain + Response response = proceed(session); + + // retried request + response = proceed(session); + + return response; + } +}; + +TEST(InterceptorTest, HiddenUrlRewriteInterceptorTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeStatusCodeInterceptorTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + long expected_status_code{12345}; + EXPECT_EQ(url, response.url); + EXPECT_EQ(expected_status_code, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + + // second request + response = session.Get(); + EXPECT_EQ(url, response.url); + EXPECT_EQ(expected_status_code, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, RetryInterceptorTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + long expected_status_code{12345}; + EXPECT_EQ(url, response.url); + EXPECT_EQ(expected_status_code, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + + // second request + response = session.Get(); + EXPECT_EQ(url, response.url); + EXPECT_EQ(expected_status_code, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, DownloadChangeStatusCodeInterceptorTest) { + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + cpr::Session session; + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Download(cpr::WriteCallback{write_data, 0}); + + long expected_status_code{12345}; + EXPECT_EQ(url, response.url); + EXPECT_EQ(expected_status_code, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, SetBasicAuthInterceptorTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, SetUnsupportedProtocolErrorInterceptorTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + + Response response = session.Get(); + std::string expected_error_message{"SetErrorInterceptor"}; + ErrorCode expected_error_code{ErrorCode::UNSUPPORTED_PROTOCOL}; + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(expected_error_message, response.error.message); + EXPECT_EQ(expected_error_code, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToGetInterceptorTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Head(); + + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToPostInterceptorTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Head(); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToPutInterceptorTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToPatchInterceptorTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToOptionsInterceptorTest) { + Url url{server->GetBaseUrl() + "/"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToHeadInterceptorTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToDownloadCallbackInterceptorTest) { + Url url{server->GetBaseUrl() + "/download_gzip.html"}; + Session session; + session.SetUrl(url); + session.SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + session.SetTimeout(Timeout{2000}); + session.AddInterceptor(std::make_shared()); + Response response = session.Put(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ChangeRequestMethodToDeleteInterceptorTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, TwoInterceptorsTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{"Hello world!"}; + long expected_status_code{12345}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(expected_status_code, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(InterceptorTest, ThreeInterceptorsTest) { + Url url{server->GetBaseUrl() + "/basic.json"}; + Session session; + session.SetUrl(url); + session.AddInterceptor(std::make_shared()); + session.AddInterceptor(std::make_shared()); + session.AddInterceptor(std::make_shared()); + Response response = session.Get(); + + std::string expected_text{"Hello world!"}; + long expected_status_code{12345}; + std::string expected_error_message{"SetErrorInterceptor"}; + ErrorCode expected_error_code{ErrorCode::UNSUPPORTED_PROTOCOL}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(expected_status_code, response.status_code); + EXPECT_EQ(expected_error_message, response.error.message); + EXPECT_EQ(expected_error_code, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/multiasync_tests.cpp b/3rdparty/cpr/test/multiasync_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0454d2dbebbee808e95c541110118c41d7c4b6c1 --- /dev/null +++ b/3rdparty/cpr/test/multiasync_tests.cpp @@ -0,0 +1,438 @@ +#include +#include +#include + +#include "cpr/cpr.h" + +#include "httpServer.hpp" +#include "multiasync_tests.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +// A cancellable AsyncResponse +using AsyncResponseC = AsyncWrapper; + +/** This property is tested at compile-time, so if compilation succeeds, it has already been verified. It is, however, useful to structure it as a test for semantic purposes. + */ +TEST(AsyncWrapperTests, TestConstructorDeductions) { + auto wrapper_non_cancellable{AsyncWrapper{std::future{}}}; + auto wrapper_cancellable{AsyncWrapper{std::future{}, std::make_shared(false)}}; + + static_assert(std::is_same_v, decltype(wrapper_non_cancellable)>); + static_assert(std::is_same_v, decltype(wrapper_cancellable)>); + SUCCEED(); +} + +/** These tests aim to set a point of reference for AsyncWrapper behavior. + * Those functions that replicate std::future member functions should behave in a way that is in all ways compatible. + * Others should behave as expected by the below test set. + */ +TEST(AsyncWrapperNonCancellableTests, TestGetNoError) { + const Url hello_url{server->GetBaseUrl() + "/hello.html"}; + const std::string expected_hello{"Hello world!"}; + const Response resp{GetAsync(hello_url).get()}; + EXPECT_EQ(expected_hello, resp.text); +} + +TEST(AsyncWrapperNonCancellableTests, TestExceptionsNoSharedState) { + const std::chrono::duration five_secs{std::chrono::seconds(1)}; + const std::chrono::time_point in_five_s{std::chrono::steady_clock::now() + five_secs}; + + // We create an AsyncWrapper for a future without a shared state (default-initialized) + AsyncWrapper test_wrapper{std::future{}}; + + + ASSERT_FALSE(test_wrapper.valid()); + ASSERT_FALSE(test_wrapper.IsCancelled()); + + // Trying to get or wait for a future that doesn't have a shared state should result to an exception + // It should be noted that there is a divergence from std::future behavior here: calling wait* on the original std::future is undefined behaviour, according to cppreference.com . We find it preferrable to throw an exception. + EXPECT_THROW(std::ignore = test_wrapper.get(), std::exception); + EXPECT_THROW(test_wrapper.wait(), std::exception); + EXPECT_THROW(test_wrapper.wait_for(five_secs), std::exception); + EXPECT_THROW(test_wrapper.wait_until(in_five_s), std::exception); +} + +TEST(AsyncWrapperCancellableTests, TestExceptionsNoSharedState) { + const std::chrono::duration five_secs{std::chrono::seconds(5)}; + const std::chrono::time_point in_five_s{std::chrono::steady_clock::now() + five_secs}; + + AsyncWrapper test_wrapper{std::future{}, std::make_shared(false)}; + + static_assert(std::is_same, decltype(test_wrapper)>::value); + + ASSERT_FALSE(test_wrapper.valid()); + ASSERT_FALSE(test_wrapper.IsCancelled()); + + EXPECT_THROW(std::ignore = test_wrapper.get(), std::exception); + EXPECT_THROW(test_wrapper.wait(), std::exception); + EXPECT_THROW(test_wrapper.wait_for(five_secs), std::exception); + EXPECT_THROW(test_wrapper.wait_until(in_five_s), std::exception); +} + +TEST(AsyncWrapperCancellableTests, TestExceptionsCancelledRequest) { + const Url call_url{server->GetBaseUrl() + "/low_speed_bytes.html"}; + const std::chrono::duration five_secs{std::chrono::seconds(5)}; + const std::chrono::time_point in_five_s{std::chrono::steady_clock::now() + five_secs}; + + AsyncResponseC test_wrapper{std::move(MultiGetAsync(std::tuple{call_url}).at(0))}; + EXPECT_EQ(CancellationResult::success, test_wrapper.Cancel()); + EXPECT_EQ(CancellationResult::invalid_operation, test_wrapper.Cancel()); + ASSERT_TRUE(test_wrapper.IsCancelled()); + + EXPECT_THROW(std::ignore = test_wrapper.get(), std::exception); + EXPECT_THROW(test_wrapper.wait(), std::exception); + EXPECT_THROW(test_wrapper.wait_for(five_secs), std::exception); + EXPECT_THROW(test_wrapper.wait_until(in_five_s), std::exception); +} + +TEST(AsyncWrapperCancellableTests, TestWaitFor) { + constexpr std::chrono::duration wait_for_time{std::chrono::milliseconds(100)}; + constexpr std::chrono::duration teardown_time{std::chrono::milliseconds(10)}; + + const Url call_url{server->GetBaseUrl() + "/low_speed_bytes.html"}; + + AsyncResponseC test_wrapper{std::move(MultiGetAsync(std::tuple{call_url}).at(0))}; + + EXPECT_EQ(std::future_status::timeout, test_wrapper.wait_for(wait_for_time)); + + ASSERT_TRUE(test_wrapper.valid()); + ASSERT_FALSE(test_wrapper.IsCancelled()); + + EXPECT_EQ(CancellationResult::success, test_wrapper.Cancel()); + + std::this_thread::sleep_for(teardown_time); +} +/** The group MultiAsyncBasicTests executes multiple tests from the test sources associated with every Http action in parallel. + * These tests are reproductions of tests from the appropriate test suites, but they guarantee that the multiasync function template produces correctly working instantiations for every Http action. + */ +TEST(MultiAsyncBasicTests, MultiAsyncGetTest) { + const Url hello_url{server->GetBaseUrl() + "/hello.html"}; + const std::string expected_hello{"Hello world!"}; + std::vector resps{MultiGetAsync(std::tuple{hello_url}, std::tuple{hello_url}, std::tuple{hello_url})}; + + for (AsyncResponseC& resp : resps) { + EXPECT_EQ(expected_hello, resp.get().text); + } +} + +TEST(MultiAsyncBasicTests, MultiAsyncDeleteTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url delete_allowed{server_base + "/delete.html"}; + const Url delete_unallowed{server_base + "/delete_unallowed.html"}; + const std::tuple del_json_params{delete_allowed, Body{"'foo':'bar'"}, Header{{"Content-Type", "application/json"}}}; + const std::string expected_text_success{"Delete success"}; + const std::string expected_text_fail{"Method Not Allowed"}; + const std::string expected_text_json{"'foo':'bar'"}; + + std::vector resps{MultiDeleteAsync(std::tuple{delete_allowed}, std::tuple{delete_unallowed}, del_json_params)}; + + Response del_success{resps.at(0).get()}; + Response del_fail{resps.at(1).get()}; + Response del_json{resps.at(2).get()}; + + EXPECT_EQ(expected_text_success, del_success.text); + EXPECT_EQ(delete_allowed, del_success.url); + EXPECT_EQ(std::string{"text/html"}, del_success.header["content-type"]); + EXPECT_EQ(200, del_success.status_code); + EXPECT_EQ(ErrorCode::OK, del_success.error.code); + + EXPECT_EQ(expected_text_fail, del_fail.text); + EXPECT_EQ(delete_unallowed, del_fail.url); + EXPECT_EQ(std::string{"text/plain"}, del_fail.header["content-type"]); + EXPECT_EQ(405, del_fail.status_code); + EXPECT_EQ(ErrorCode::OK, del_fail.error.code); + + EXPECT_EQ(expected_text_json, del_json.text); + EXPECT_EQ(delete_allowed, del_json.url); + EXPECT_EQ(std::string{"application/json"}, del_json.header["content-type"]); + EXPECT_EQ(200, del_json.status_code); + EXPECT_EQ(ErrorCode::OK, del_json.error.code); +} + +TEST(MultiAsyncBasicTests, MultiAsyncHeadTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url hello_url{server_base + "/hello.html"}; + const Url json_url{server_base + "/basic.json"}; + const Url notfound_url{server_base + "/error.html"}; + const Url digest_url{server_base + "/digest_auth.html"}; + const Authentication digest_auth{"user", "password", AuthMode::DIGEST}; + + std::vector resps{MultiHeadAsync(std::tuple{hello_url}, std::tuple{json_url}, std::tuple{notfound_url}, std::tuple{digest_url, digest_auth})}; + Response hello_resp{resps.at(0).get()}; + Response json_resp{resps.at(1).get()}; + Response notfound_resp{resps.at(2).get()}; + Response digest_resp{resps.at(3).get()}; + + EXPECT_EQ(std::string{}, hello_resp.text); + EXPECT_EQ(hello_url, hello_resp.url); + EXPECT_EQ(std::string{"text/html"}, hello_resp.header["content-type"]); + EXPECT_EQ(200, hello_resp.status_code); + EXPECT_EQ(ErrorCode::OK, hello_resp.error.code); + + EXPECT_EQ(std::string{}, json_resp.text); + EXPECT_EQ(json_url, json_resp.url); + EXPECT_EQ(std::string{"application/json"}, json_resp.header["content-type"]); + EXPECT_EQ(200, json_resp.status_code); + EXPECT_EQ(ErrorCode::OK, json_resp.error.code); + + EXPECT_EQ(std::string{}, notfound_resp.text); + EXPECT_EQ(notfound_url, notfound_resp.url); + EXPECT_EQ(std::string{"text/plain"}, notfound_resp.header["content-type"]); + EXPECT_EQ(404, notfound_resp.status_code); + EXPECT_EQ(ErrorCode::OK, notfound_resp.error.code); + + EXPECT_EQ(std::string{}, digest_resp.text); + EXPECT_EQ(digest_url, digest_resp.url); + EXPECT_EQ(std::string{"text/html"}, digest_resp.header["content-type"]); + EXPECT_EQ(200, digest_resp.status_code); + EXPECT_EQ(ErrorCode::OK, digest_resp.error.code); +} + +TEST(MultiAsyncBasicTests, MultiAsyncOptionsTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url root_url{server_base + "/"}; + const Url hello_url{server_base + "/hello.html"}; + + std::vector resps{MultiOptionsAsync(std::tuple{root_url}, std::tuple{hello_url})}; + + Response root_resp{resps.at(0).get()}; + Response hello_resp{resps.at(1).get()}; + + EXPECT_EQ(std::string{}, root_resp.text); + EXPECT_EQ(root_url, root_resp.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, root_resp.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, root_resp.status_code); + EXPECT_EQ(ErrorCode::OK, root_resp.error.code); + + EXPECT_EQ(std::string{}, hello_resp.text); + EXPECT_EQ(hello_url, hello_resp.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, hello_resp.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, hello_resp.status_code); + EXPECT_EQ(ErrorCode::OK, hello_resp.error.code); +} + +TEST(MultiAsyncBasicTests, MultiAsyncPatchTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url patch_url{server_base + "/patch.html"}; + const Url patch_not_allowed_url{server_base + "/patch_unallowed.html"}; + const Payload pl{{"x", "10"}, {"y", "1"}}; + const std::string expected_text{ + "{\n" + " \"x\": 10,\n" + " \"y\": 1,\n" + " \"sum\": 11\n" + "}"}; + const std::string notallowed_text{"Method Not Allowed"}; + std::vector resps{MultiPatchAsync(std::tuple{patch_url, pl}, std::tuple{patch_not_allowed_url, pl})}; + const Response success{resps.at(0).get()}; + const Response fail{resps.at(1).get()}; + EXPECT_EQ(expected_text, success.text); + EXPECT_EQ(200, success.status_code); + EXPECT_EQ(patch_url, success.url); + + EXPECT_EQ(notallowed_text, fail.text); + EXPECT_EQ(405, fail.status_code); + EXPECT_EQ(ErrorCode::OK, fail.error.code); +} + +TEST(MultiAsyncBasicTests, MultiAsyncPostTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url post_url{server_base + "/url_post.html"}; + const Url form_post_url{server_base + "/form_post.html"}; + + const Payload post_data{{"x", "5"}, {"y", "15"}}; + const Multipart form_data{{"x", 5}}; + + const std::string post_text{ + "{\n" + " \"x\": 5,\n" + " \"y\": 15,\n" + " \"sum\": 20\n" + "}"}; + const std::string form_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + + std::vector resps{MultiPostAsync(std::tuple{post_url, post_data}, std::tuple{form_post_url, form_data})}; + + Response post_resp{resps.at(0).get()}; + Response form_resp{resps.at(1).get()}; + + EXPECT_EQ(post_text, post_resp.text); + EXPECT_EQ(post_url, post_resp.url); + EXPECT_EQ(std::string{"application/json"}, post_resp.header["content-type"]); + EXPECT_EQ(201, post_resp.status_code); + EXPECT_EQ(ErrorCode::OK, post_resp.error.code); + + EXPECT_EQ(form_text, form_resp.text); + EXPECT_EQ(form_post_url, form_resp.url); + EXPECT_EQ(std::string{"application/json"}, form_resp.header["content-type"]); + EXPECT_EQ(201, form_resp.status_code); + EXPECT_EQ(ErrorCode::OK, form_resp.error.code); +} + +TEST(MultiAsyncBasicTests, MultiAsyncPutTest) { + const std::string server_base{server->GetBaseUrl()}; + const Url put_url{server_base + "/put.html"}; + const Url put_failure_url{server_base + "/put_unallowed.html"}; + const Payload pl{{"x", "7"}}; + const std::string success_text{ + "{\n" + " \"x\": 7\n" + "}"}; + const std::string failure_text{"Method Not Allowed"}; + + std::vector resps{MultiPutAsync(std::tuple{put_url, pl}, std::tuple{put_failure_url, pl})}; + Response success_resp{resps.at(0).get()}; + Response failure_resp{resps.at(1).get()}; + + EXPECT_EQ(success_text, success_resp.text); + EXPECT_EQ(put_url, success_resp.url); + EXPECT_EQ(std::string{"application/json"}, success_resp.header["content-type"]); + EXPECT_EQ(200, success_resp.status_code); + EXPECT_EQ(ErrorCode::OK, success_resp.error.code); + + EXPECT_EQ(failure_text, failure_resp.text); + EXPECT_EQ(put_failure_url, failure_resp.url); + EXPECT_EQ(std::string{"text/plain"}, failure_resp.header["content-type"]); + EXPECT_EQ(405, failure_resp.status_code); + EXPECT_EQ(ErrorCode::OK, failure_resp.error.code); +} + +static TestSynchronizationEnv* synchro_env = new TestSynchronizationEnv(); + +/** + * We test that cancellation on queue, works, ie libcurl does not get engaged at all + * To do this, we plant an observer function in the progress call sequence, which + * will set an atomic boolean to true. The objective is to verify that within 500ms, + * the function is never called. + */ +TEST(MultiAsyncCancelTests, CancellationOnQueue) { + synchro_env->Reset(); + const Url hello_url{server->GetBaseUrl() + "/hello.html"}; + const std::function observer_fn{[](cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, intptr_t) -> bool { + synchro_env->fn_called.store(true); + return true; + }}; + + GlobalThreadPool::GetInstance()->Pause(); + std::vector resps{MultiGetAsync(std::tuple{hello_url, ProgressCallback{observer_fn}})}; + EXPECT_EQ(CancellationResult::success, resps.at(0).Cancel()); + GlobalThreadPool::GetInstance()->Resume(); + const bool was_called{synchro_env->fn_called}; + EXPECT_EQ(false, was_called); +} + +/** + * We test that cancellation works as intended while the request is being processed by the server. + * To achieve this we use a condition variable to ensure that the observer function, wrapped in a + * cpr::ProgressCallback, is called at least once, and then no further calls are made for half a + * second after cancellation. + * + * The usage of the condition variable and mutex to synchronize this procedure is analogous to the section "Example" in https://en.cppreference.com/w/cpp/thread/condition_variable + * We use the condition variable in our synchronization environment to ensure that the transfer has + * started at the time of cancellation, ie the observer function has been called at least once. + */ +TEST(MultiAsyncCancelTests, TestCancellationInTransit) { + const Url call_url{server->GetBaseUrl() + "/low_speed_bytes.html"}; + synchro_env->Reset(); + + // 1. Thread running the test acquires the condition variable's mutex + std::unique_lock setup_lock{synchro_env->test_cv_mutex}; + const std::function observer_fn{[](cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, intptr_t) -> bool { + if (synchro_env->counter == 0) { + // 3. in Threadpool, the cv mutex is obtained by the worker thread + const std::unique_lock l{synchro_env->test_cv_mutex}; + synchro_env->counter++; + // 4. the cv is notified + synchro_env->test_cv.notify_all(); + } else { + synchro_env->counter++; + } + return true; + }}; + std::vector res{cpr::MultiGetAsync(std::tuple{call_url, cpr::ProgressCallback{observer_fn}})}; + // 2. cv mutex is released, thread waits for notification on cv + // see https://en.cppreference.com/w/cpp/thread/condition_variable/wait + synchro_env->test_cv.wait(setup_lock); + // 5. execution continues after notification + const size_t init_calls{synchro_env->counter}; + EXPECT_LT(0, init_calls); + EXPECT_EQ(cpr::CancellationResult::success, res.at(0).Cancel()); + const size_t calls{synchro_env->counter}; + std::this_thread::sleep_for(std::chrono::milliseconds{101}); + const size_t calls_post{synchro_env->counter}; + EXPECT_LT(calls_post, calls + 2); +} + +/** Checks that the request is cancelled when the corresponding AsyncResponseC is desturcted + */ +TEST(MultiAsyncCancelTests, TestCancellationOnResponseWrapperDestruction) { + const Url call_url{server->GetBaseUrl() + "/hello.html"}; + synchro_env->Reset(); + std::unique_lock setup_lock{synchro_env->test_cv_mutex}; + const std::function observer_fn{[](cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, intptr_t) -> bool { + const std::unique_lock l{synchro_env->test_cv_mutex}; + synchro_env->counter++; + synchro_env->test_cv.notify_all(); + return true; + }}; + + // We construct a Request that will not terminate, wait until it is being processed by a thread, and destruct the AsyncResponseC + { + AsyncResponseC resp{std::move(MultiGetAsync(std::tuple{call_url, ProgressCallback{observer_fn}}).at(0))}; + synchro_env->test_cv.wait(setup_lock); + const size_t init_calls{synchro_env->counter}; + EXPECT_LT(0, init_calls); + } + + const size_t calls{synchro_env->counter}; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + const size_t post_calls{synchro_env->counter}; + EXPECT_EQ(calls, post_calls); +} + +/** + * This test checks if the interval of calls to the progress function is + * acceptable during a low-speed transaction. The server's low_speed_bytes + * uri sends 1 Byte/second, and we aim to evaluate that 15 calls to the + * progress function happen within 5 seconds. This would indicate that + * the user can realistically expect to have their request cancelled within + * ~1s on a bad case (low network speed). + * INFO this test is not, strictly speaking, deterministic. It depends at the + * least on scheduler behaviour. We have tried, however, to set a boundary that + * is permissive enough to ensure consistency. + */ + +TEST(MultiAsyncCancelTests, TestIntervalOfProgressCallsLowSpeed) { + const Url call_url{server->GetBaseUrl() + "/low_speed_bytes.html"}; + + synchro_env->Reset(); + size_t N{15}; + // This variable will be used to cancel the transaction at the point of the Nth call. + const std::chrono::time_point start{std::chrono::steady_clock::now()}; + + const std::function observer_fn{[N](cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, cpr_pf_arg_t, intptr_t) -> bool { + const size_t current_iteration{++(synchro_env->counter)}; + return current_iteration <= N; + }}; + const ProgressCallback pcall{observer_fn}; + + std::vector resp{MultiGetAsync(std::tuple{call_url, pcall})}; + resp.at(0).wait(); + + const std::chrono::duration elapsed_time{std::chrono::steady_clock::now() - start}; + EXPECT_GT(std::chrono::seconds(N), elapsed_time); + std::this_thread::sleep_for(std::chrono::milliseconds{101}); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + ::testing::AddGlobalTestEnvironment(synchro_env); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/multiasync_tests.hpp b/3rdparty/cpr/test/multiasync_tests.hpp new file mode 100644 index 0000000000000000000000000000000000000000..ee1273d319e66f68dbe3c59ba639e4c43978d31c --- /dev/null +++ b/3rdparty/cpr/test/multiasync_tests.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +class TestSynchronizationEnv : public testing::Environment { + public: + std::atomic_size_t counter{0}; + std::atomic_bool fn_called{false}; + std::condition_variable test_cv{}; + std::mutex test_cv_mutex{}; + + void Reset() { + counter = 0; + fn_called = false; + } +}; diff --git a/3rdparty/cpr/test/multiperform_tests.cpp b/3rdparty/cpr/test/multiperform_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9cf1720fdd109fe4d002d53659afb729152e93a9 --- /dev/null +++ b/3rdparty/cpr/test/multiperform_tests.cpp @@ -0,0 +1,682 @@ +#include +#include + +#include +#include + +#include "cpr/api.h" +#include "cpr/cpr.h" +#include + +#include "httpServer.hpp" +#include "httpsServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +bool write_data(const std::string_view& /*data*/, intptr_t /*userdata*/) { + return true; +} + +TEST(MultiperformAddSessionTests, MultiperformAddSingleSessionTest) { + std::shared_ptr session = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session); + + EXPECT_EQ(2, session.use_count()); +} + +TEST(MultiperformAddSessionTests, MultiperformAddMultipleSessionsTest) { + MultiPerform multiperform; + for (int i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + multiperform.AddSession(session); + EXPECT_EQ(2, session.use_count()); + } +} + +TEST(MultiperformAddSessionTests, MultiperformAddMultipleNonDownloadSessionsTest) { + MultiPerform multiperform; + for (int i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + multiperform.AddSession(session, MultiPerform::HttpMethod::GET_REQUEST); + EXPECT_EQ(2, session.use_count()); + } +} + +TEST(MultiperformAddSessionTests, MultiperformAddMultipleDownloadSessionsTest) { + MultiPerform multiperform; + for (int i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + multiperform.AddSession(session, MultiPerform::HttpMethod::DOWNLOAD_REQUEST); + EXPECT_EQ(2, session.use_count()); + } +} + +TEST(MultiperformAddSessionTests, MultiperformAddTwoMixedTypeSessionsTest) { + std::shared_ptr session_1 = std::make_shared(); + std::shared_ptr session_2 = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session_1, MultiPerform::HttpMethod::GET_REQUEST); + EXPECT_EQ(2, session_1.use_count()); + EXPECT_THROW(multiperform.AddSession(session_2, MultiPerform::HttpMethod::DOWNLOAD_REQUEST), std::invalid_argument); +} + +TEST(MultiperformAddSessionTests, MultiperformAddTwoMixedTypeSessionsReversTest) { + std::shared_ptr session_1 = std::make_shared(); + std::shared_ptr session_2 = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session_1, MultiPerform::HttpMethod::DOWNLOAD_REQUEST); + EXPECT_EQ(2, session_1.use_count()); + EXPECT_THROW(multiperform.AddSession(session_2, MultiPerform::HttpMethod::GET_REQUEST), std::invalid_argument); +} + +TEST(MultiperformRemoveSessionTests, MultiperformRemoveSingleSessionTest) { + std::shared_ptr session = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session); + EXPECT_EQ(2, session.use_count()); + multiperform.RemoveSession(session); + EXPECT_EQ(1, session.use_count()); +} + +TEST(MultiperformRemoveSessionTests, MultiperformRemoveMultipleSessionsTest) { + MultiPerform multiperform; + for (int i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + multiperform.AddSession(session); + EXPECT_EQ(2, session.use_count()); + multiperform.RemoveSession(session); + EXPECT_EQ(1, session.use_count()); + } +} + +TEST(MultiperformRemoveSessionTests, MultiperformRemoveNonExistingSessionEmptyTest) { + std::shared_ptr session = std::make_shared(); + MultiPerform multiperform; + EXPECT_THROW(multiperform.RemoveSession(session), std::invalid_argument); +} + +TEST(MultiperformRemoveSessionTests, MultiperformRemoveNonExistingSessionTest) { + MultiPerform multiperform; + std::shared_ptr session = std::make_shared(); + multiperform.AddSession(session); + + std::shared_ptr session2 = std::make_shared(); + EXPECT_THROW(multiperform.RemoveSession(session2), std::invalid_argument); +} + +TEST(MultiperformGetTests, MultiperformSingleSessionGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Get(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformGetTests, MultiperformTwoSessionsGetTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/hello.html"}); + urls.push_back({server->GetBaseUrl() + "/error.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + multiperform.AddSession(sessions.at(i)); + } + + std::vector responses = multiperform.Get(); + + EXPECT_EQ(responses.size(), sessions.size()); + std::vector expected_texts; + expected_texts.emplace_back("Hello world!"); + expected_texts.emplace_back("Not Found"); + + std::vector expected_content_types; + expected_content_types.emplace_back("text/html"); + expected_content_types.emplace_back("text/plain"); + + std::vector expected_status_codes; + expected_status_codes.push_back(200); + expected_status_codes.push_back(404); + + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(expected_texts.at(i), responses.at(i).text); + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(expected_content_types.at(i), responses.at(i).header["content-type"]); + EXPECT_EQ(expected_status_codes.at(i), responses.at(i).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(i).error.code); + } +} + +TEST(MultiperformGetTests, MultiperformRemoveSessionGetTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/hello.html"}); + urls.push_back({server->GetBaseUrl() + "/hello.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + multiperform.AddSession(sessions.at(i)); + } + + multiperform.RemoveSession(sessions.at(0)); + + std::vector responses = multiperform.Get(); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(urls.at(0), responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +#ifndef __APPLE__ +/** + * This test case is currently disabled for macOS/Apple systems since it fails in an nondeterministic manner. + * It is probably caused by a bug inside curl_multi_perform on macOS. + * Needs further investigation. + * Issue: https://github.com/libcpr/cpr/issues/841 + **/ +TEST(MultiperformGetTests, MultiperformTenSessionsGetTest) { + const size_t sessionCount = 10; + + MultiPerform multiperform; + Url url{server->GetBaseUrl() + "/hello.html"}; + for (size_t i = 0; i < sessionCount; ++i) { + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + multiperform.AddSession(session); + } + + std::vector responses = multiperform.Get(); + + EXPECT_EQ(responses.size(), sessionCount); + for (Response& response : responses) { + EXPECT_EQ(std::string{"Hello world!"}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} +#endif + +TEST(MultiperformDeleteTests, MultiperformSingleSessionDeleteTest) { + Url url{server->GetBaseUrl() + "/delete.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Delete(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformDownloadTests, MultiperformSingleSessionDownloadTest) { + Url url{server->GetBaseUrl() + "/download_gzip.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Download(WriteCallback{write_data, 0}); + + EXPECT_EQ(responses.size(), 1); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(cpr::ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformDownloadTests, MultiperformSingleSessionDownloadNonMatchingArgumentsNumberTest) { + std::shared_ptr session = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session); + EXPECT_THROW(std::vector responses = multiperform.Download(WriteCallback{write_data, 0}, WriteCallback{write_data, 0}), std::invalid_argument); +} + +TEST(MultiperformDownloadTests, MultiperformTwoSessionsDownloadTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/download_gzip.html"}); + urls.push_back({server->GetBaseUrl() + "/download_gzip.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + sessions.at(i)->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + + multiperform.AddSession(sessions.at(i)); + } + + std::vector responses = multiperform.Download(WriteCallback{write_data, 0}, WriteCallback{write_data, 0}); + + EXPECT_EQ(responses.size(), sessions.size()); + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(200, responses.at(i).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(i).error.code); + } +} + +TEST(MultiperformPutTests, MultiperformSingleSessionPutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload(Payload{{"x", "5"}}); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Put(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformHeadTests, MultiperformSingleSessionHeadTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Head(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformOptionsTests, MultiperformSingleSessionOptionsTest) { + Url url{server->GetBaseUrl() + "/"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Options(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, responses.at(0).header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformPatchTests, MultiperformSingleSessionPatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload(Payload{{"x", "5"}}); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Patch(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformPostTests, MultiperformSingleSessionPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload(Payload{{"x", "5"}}); + MultiPerform multiperform; + multiperform.AddSession(session); + std::vector responses = multiperform.Post(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(201, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformPerformTests, MultiperformSingleGetPerformTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + MultiPerform multiperform; + multiperform.AddSession(session, MultiPerform::HttpMethod::GET_REQUEST); + std::vector responses = multiperform.Perform(); + + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformPerformTests, MultiperformTwoGetPerformTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/hello.html"}); + urls.push_back({server->GetBaseUrl() + "/error.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + multiperform.AddSession(sessions.at(i), MultiPerform::HttpMethod::GET_REQUEST); + } + + std::vector responses = multiperform.Perform(); + + EXPECT_EQ(responses.size(), sessions.size()); + std::vector expected_texts; + expected_texts.emplace_back("Hello world!"); + expected_texts.emplace_back("Not Found"); + + std::vector expected_content_types; + expected_content_types.emplace_back("text/html"); + expected_content_types.emplace_back("text/plain"); + + std::vector expected_status_codes; + expected_status_codes.push_back(200); + expected_status_codes.push_back(404); + + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(expected_texts.at(i), responses.at(i).text); + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(expected_content_types.at(i), responses.at(i).header["content-type"]); + EXPECT_EQ(expected_status_codes.at(i), responses.at(i).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(i).error.code); + } +} + +TEST(MultiperformPerformTests, MultiperformMixedMethodsPerformTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/hello.html"}); + urls.push_back({server->GetBaseUrl() + "/delete.html"}); + urls.push_back({server->GetBaseUrl() + "/error.html"}); + urls.push_back({server->GetBaseUrl() + "/url_post.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + std::vector methods; + methods.push_back(MultiPerform::HttpMethod::GET_REQUEST); + methods.push_back(MultiPerform::HttpMethod::DELETE_REQUEST); + methods.push_back(MultiPerform::HttpMethod::GET_REQUEST); + methods.push_back(MultiPerform::HttpMethod::POST_REQUEST); + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + if (methods.at(i) == MultiPerform::HttpMethod::POST_REQUEST) { + sessions.at(i)->SetPayload(Payload{{"x", "5"}}); + } + multiperform.AddSession(sessions.at(i), methods.at(i)); + } + + std::vector responses = multiperform.Perform(); + + EXPECT_EQ(responses.size(), sessions.size()); + + std::vector expected_texts; + expected_texts.emplace_back("Hello world!"); + expected_texts.emplace_back("Delete success"); + expected_texts.emplace_back("Not Found"); + expected_texts.emplace_back( + "{\n" + " \"x\": 5\n" + "}"); + + std::vector expected_content_types; + expected_content_types.emplace_back("text/html"); + expected_content_types.emplace_back("text/html"); + expected_content_types.emplace_back("text/plain"); + expected_content_types.emplace_back("application/json"); + + std::vector expected_status_codes; + expected_status_codes.push_back(200); + expected_status_codes.push_back(200); + expected_status_codes.push_back(404); + expected_status_codes.push_back(201); + + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(expected_texts.at(i), responses.at(i).text); + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(expected_content_types.at(i), responses.at(i).header["content-type"]); + EXPECT_EQ(expected_status_codes.at(i), responses.at(i).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(i).error.code); + } +} + +TEST(MultiperformPerformDownloadTests, MultiperformSinglePerformDownloadTest) { + Url url{server->GetBaseUrl() + "/download_gzip.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + MultiPerform multiperform; + multiperform.AddSession(session, MultiPerform::HttpMethod::DOWNLOAD_REQUEST); + std::vector responses = multiperform.PerformDownload(WriteCallback{write_data, 0}); + + EXPECT_EQ(responses.size(), 1); + EXPECT_EQ(url, responses.at(0).url); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(cpr::ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformDownloadTests, MultiperformSinglePerformDownloadNonMatchingArgumentsNumberTest) { + std::shared_ptr session = std::make_shared(); + MultiPerform multiperform; + multiperform.AddSession(session, MultiPerform::HttpMethod::DOWNLOAD_REQUEST); + EXPECT_THROW(std::vector responses = multiperform.PerformDownload(WriteCallback{write_data, 0}, WriteCallback{write_data, 0}), std::invalid_argument); +} + +TEST(MultiperformPerformDownloadTests, MultiperformTwoPerformDownloadTest) { + MultiPerform multiperform; + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/download_gzip.html"}); + urls.push_back({server->GetBaseUrl() + "/download_gzip.html"}); + + std::vector> sessions; + sessions.push_back(std::make_shared()); + sessions.push_back(std::make_shared()); + + + for (size_t i = 0; i < sessions.size(); ++i) { + sessions.at(i)->SetUrl(urls.at(i)); + sessions.at(i)->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + + multiperform.AddSession(sessions.at(i), MultiPerform::HttpMethod::DOWNLOAD_REQUEST); + } + + std::vector responses = multiperform.PerformDownload(WriteCallback{write_data, 0}, WriteCallback{write_data, 0}); + + EXPECT_EQ(responses.size(), sessions.size()); + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(200, responses.at(i).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(i).error.code); + } +} + +TEST(MultiperformAPITests, MultiperformApiSingleGetTest) { + std::vector responses = MultiGet(std::tuple{Url{server->GetBaseUrl() + "/hello.html"}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiTwoGetsTest) { + std::vector responses = MultiGet(std::tuple{Url{server->GetBaseUrl() + "/long_timeout.html"}, Timeout{1000}}, std::tuple{Url{server->GetBaseUrl() + "/error.html"}}); + + EXPECT_EQ(responses.size(), 2); + std::vector urls; + urls.push_back({server->GetBaseUrl() + "/long_timeout.html"}); + urls.push_back({server->GetBaseUrl() + "/error.html"}); + + std::vector expected_texts; + expected_texts.emplace_back(""); + expected_texts.emplace_back("Not Found"); + + std::vector expected_content_types; + expected_content_types.emplace_back(""); + expected_content_types.emplace_back("text/plain"); + + std::vector expected_status_codes; + expected_status_codes.push_back(0); + expected_status_codes.push_back(404); + + std::vector expected_error_codes; + expected_error_codes.push_back(ErrorCode::OPERATION_TIMEDOUT); + expected_error_codes.push_back(ErrorCode::OK); + + for (size_t i = 0; i < responses.size(); ++i) { + EXPECT_EQ(expected_texts.at(i), responses.at(i).text); + EXPECT_EQ(urls.at(i), responses.at(i).url); + EXPECT_EQ(expected_content_types.at(i), responses.at(i).header["content-type"]); + EXPECT_EQ(expected_status_codes.at(i), responses.at(i).status_code); + EXPECT_EQ(expected_error_codes.at(i), responses.at(i).error.code); + } +} + +TEST(MultiperformAPITests, MultiperformApiSingleDeleteTest) { + std::vector responses = MultiDelete(std::tuple{Url{server->GetBaseUrl() + "/delete.html"}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{"Delete success"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/delete.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiSinglePutTest) { + std::vector responses = MultiPut(std::tuple{Url{server->GetBaseUrl() + "/put.html"}, Payload{{"x", "5"}}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/put.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiSingleHeadTest) { + std::vector responses = MultiHead(std::tuple{Url{server->GetBaseUrl() + "/hello.html"}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"text/html"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiSingleOptionsTest) { + std::vector responses = MultiOptions(std::tuple{Url{server->GetBaseUrl() + "/"}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/"}, responses.at(0).url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, responses.at(0).header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiSinglePatchTest) { + std::vector responses = MultiPatch(std::tuple{Url{server->GetBaseUrl() + "/patch.html"}, Payload{{"x", "5"}}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/patch.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(200, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +TEST(MultiperformAPITests, MultiperformApiSinglePostTest) { + std::vector responses = MultiPost(std::tuple{Url{server->GetBaseUrl() + "/url_post.html"}, Payload{{"x", "5"}}}); + EXPECT_EQ(responses.size(), 1); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, responses.at(0).text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/url_post.html"}, responses.at(0).url); + EXPECT_EQ(std::string{"application/json"}, responses.at(0).header["content-type"]); + EXPECT_EQ(201, responses.at(0).status_code); + EXPECT_EQ(ErrorCode::OK, responses.at(0).error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/options_tests.cpp b/3rdparty/cpr/test/options_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..04312319a9813a3260ad672b6c7f322e198d4022 --- /dev/null +++ b/3rdparty/cpr/test/options_tests.cpp @@ -0,0 +1,73 @@ +#include + +#include + +#include "cpr/cpr.h" + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(OptionsTests, BaseUrlTest) { + Url url{server->GetBaseUrl() + "/"}; + Response response = cpr::Options(url); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(OptionsTests, SpecificUrlTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Options(url); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(OptionsTests, AsyncBaseUrlTest) { + Url url{server->GetBaseUrl() + "/"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::OptionsAsync(url)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(OptionsTests, AsyncSpecificUrlTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::OptionsAsync(url)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/patch_tests.cpp b/3rdparty/cpr/test/patch_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4c0c46cf43d462e1d9febf59c2390c8998fcd803 --- /dev/null +++ b/3rdparty/cpr/test/patch_tests.cpp @@ -0,0 +1,276 @@ +#include + +#include + +#include "cpr/cpr.h" + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(PatchTests, PatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + Response response = cpr::Patch(url, payload); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, PatchUnallowedTest) { + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + Response response = cpr::Patch(url, payload); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + Session session; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchUnallowedTest) { + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + Session session; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchUnallowedAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchUnallowedAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, SessionPatchUnallowedAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Patch(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, AsyncPatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + cpr::AsyncResponse future_response = cpr::PatchAsync(url, payload); + cpr::Response response = future_response.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, AsyncPatchUnallowedTest) { + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + cpr::AsyncResponse future_response = cpr::PatchAsync(url, payload); + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PatchTests, AsyncMultiplePatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::PatchAsync(url, payload)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(PatchTests, AsyncMultiplePatchUnallowedTest) { + Url url{server->GetBaseUrl() + "/patch_unallowed.html"}; + Payload payload{{"x", "5"}}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::PatchAsync(url, payload)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/post_tests.cpp b/3rdparty/cpr/test/post_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8183c932900e3041b0dfd5ed61bec60f71b1377d --- /dev/null +++ b/3rdparty/cpr/test/post_tests.cpp @@ -0,0 +1,766 @@ +#include +#include +#include + +#include +#include +#include +#include + +#include "cpr/cookies.h" +#include "cpr/cpr.h" +#include "cpr/multipart.h" + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(UrlEncodedPostTests, UrlPostSingleTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, UrlPostAddPayloadPair) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "1"}}; + payload.Add({"y", "2"}); + Response response = cpr::Post(url, payload); + std::string expected_text{ + "{\n" + " \"x\": 1,\n" + " \"y\": 2,\n" + " \"sum\": 3\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); +} + +TEST(UrlEncodedPostTests, UrlPostPayloadIteratorTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::vector payloadData; + payloadData.emplace_back("x", "1"); + payloadData.emplace_back("y", "2"); + Response response = cpr::Post(url, Payload(payloadData.begin(), payloadData.end())); + std::string expected_text{ + "{\n" + " \"x\": 1,\n" + " \"y\": 2,\n" + " \"sum\": 3\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); +} + +TEST(UrlEncodedPostTests, UrlPostEncodeTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Payload{{"x", "hello world!!~"}}); + std::string expected_text{ + "{\n" + " \"x\": hello world!!~\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, UrlPostEncodeNoCopyTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "hello world!!~"}}; + // payload lives through the lifetime of Post, so it doesn't need to be copied + Response response = cpr::Post(url, payload); + std::string expected_text{ + "{\n" + " \"x\": hello world!!~\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, UrlPostManyTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}, {"y", "13"}}); + std::string expected_text{ + "{\n" + " \"x\": 5,\n" + " \"y\": 13,\n" + " \"sum\": 18\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, UrlPostBadHostTest) { + Url url{"http://bad_host/"}; + Response response = cpr::Post(url, Payload{{"hello", "world"}}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::COULDNT_RESOLVE_HOST, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostSingleTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", 5}}); + std::string expected_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileTest) { + std::string filename{"test_file"}; + std::string content{"hello world"}; + std::ofstream test_file; + test_file.open(filename); + test_file << content; + test_file.close(); + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", File{filename}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + content + + "\"\n" + "}"}; + std::remove(filename.c_str()); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostMultipleFilesTestLvalue) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + std::string filename1{"file1"}; + std::string content1{"apple"}; + std::ofstream file1; + file1.open(filename1); + file1 << content1; + file1.close(); + std::string filename2{"file2"}; + std::string content2{"banana"}; + std::ofstream file2; + file2.open(filename2); + file2 << content2; + file2.close(); + File singleFile{"file1"}; + File singleFileWithOverridenFilename{"file1", "applefile"}; + Files multipleFiles{"file1", "file2"}; + Files multipleFilesWithOverridenFilename{ + File{"file1", "applefile"}, + File{"file2", "bananafile"}, + }; + { + Response response = cpr::Post(url, Multipart{{"files", singleFile}}); + std::string expected_text{ + "{\n" + " \"files\": \"file1=" + + content1 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"singleFile", singleFileWithOverridenFilename}}); + std::string expected_text{ + "{\n" + " \"singleFile\": \"applefile=" + + content1 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"files", multipleFiles}}); + std::string expected_text{ + "{\n" + " \"files\": \"file1=" + + content1 + + "\",\n" + " \"files\": \"file2=" + + content2 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"files", multipleFilesWithOverridenFilename}}); + std::string expected_text{ + "{\n" + " \"files\": \"applefile=" + + content1 + + "\",\n" + " \"files\": \"bananafile=" + + content2 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + std::remove(filename1.c_str()); + std::remove(filename2.c_str()); +} + +TEST(UrlEncodedPostTests, FormPostMultipleFilesTestRvalue) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + std::string filename1{"file1"}; + std::string content1{"apple"}; + std::ofstream file1; + file1.open(filename1); + file1 << content1; + file1.close(); + std::string filename2{"file2"}; + std::string content2{"banana"}; + std::ofstream file2; + file2.open(filename2); + file2 << content2; + file2.close(); + { + Response response = cpr::Post(url, Multipart{{"files", File{"file1"}}}); + std::string expected_text{ + "{\n" + " \"files\": \"file1=" + + content1 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"files", File{"file1", "applefile"}}}); + std::string expected_text{ + "{\n" + " \"files\": \"applefile=" + + content1 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"files", Files{"file1", "file2"}}}); + std::string expected_text{ + "{\n" + " \"files\": \"file1=" + + content1 + + "\",\n" + " \"files\": \"file2=" + + content2 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = cpr::Post(url, Multipart{{"files", Files{ + File{"file1", "applefile"}, + File{"file2", "bananafile"}, + }}}); + std::string expected_text{ + "{\n" + " \"files\": \"applefile=" + + content1 + + "\",\n" + " \"files\": \"bananafile=" + + content2 + "\"\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + std::remove(filename1.c_str()); + std::remove(filename2.c_str()); +} + +TEST(UrlEncodedPostTests, FormPostFileTestWithOverridenFilename) { + std::string filename{"test_file"}; + std::string overided_filename{"overided_filename"}; + std::string content{"hello world"}; + std::ofstream test_file; + test_file.open(filename); + test_file << content; + test_file.close(); + Url url{server->GetBaseUrl() + "/form_post.html"}; + + Response response = cpr::Post(url, Multipart{{"x", File{filename, overided_filename}}}); + std::string expected_text{ + "{\n" + " \"x\": \"" + + overided_filename + "=" + content + + "\"\n" + "}"}; + std::remove(filename.c_str()); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileNoCopyTest) { + std::string filename{"./test_file"}; + std::string content{"hello world"}; + std::ofstream test_file; + test_file.open(filename); + test_file << content; + test_file.close(); + Url url{server->GetBaseUrl() + "/form_post.html"}; + Multipart multipart{{"x", File{filename}}}; + Response response = cpr::Post(url, multipart); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + content + + "\"\n" + "}"}; + std::remove(filename.c_str()); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileNoCopyTestWithOverridenFilename) { + std::string filename{"test_file"}; + std::string overriden_filename{"overided_filename"}; + std::string content{"hello world"}; + std::ofstream test_file; + test_file.open(filename); + test_file << content; + test_file.close(); + Url url{server->GetBaseUrl() + "/form_post.html"}; + Multipart multipart{{"x", File{filename, overriden_filename}}}; + Response response = cpr::Post(url, multipart); + std::string expected_text{ + "{\n" + " \"x\": \"" + + overriden_filename + "=" + content + + "\"\n" + "}"}; + std::remove(filename.c_str()); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, TimeoutPostTest) { + Url url{server->GetBaseUrl() + "/json_post.html"}; + std::string body{R"({"RegisterObject": {"DeviceID": "65010000005030000001"}})"}; + cpr::Response response = cpr::Post(url, cpr::Header{{"Content-Type", "application/json"}}, cpr::Body{body}, cpr::ConnectTimeout{3000}, cpr::Timeout{3000}); + std::string expected_text{R"({"RegisterObject": {"DeviceID": "65010000005030000001"}})"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferTest) { + std::string content{"hello world"}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", Buffer{content.begin(), content.end(), "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + content + + "\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferNoCopyTest) { + std::string content{"hello world"}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Multipart multipart{{"x", Buffer{content.begin(), content.end(), "test_file"}}}; + Response response = cpr::Post(url, multipart); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + content + + "\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferPointerTest) { + const char* content = "hello world"; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", Buffer{content, 11 + content, "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + std::string(content) + + "\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferArrayTest) { + const char content[] = "hello world"; + Url url{server->GetBaseUrl() + "/form_post.html"}; + // We subtract 1 from std::end() because we don't want to include the terminating null + Response response = cpr::Post(url, Multipart{{"x", Buffer{std::begin(content), std::end(content) - 1, "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=" + + std::string(content) + + "\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferVectorTest) { + std::vector content{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", Buffer{content.begin(), content.end(), "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=hello world\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostFileBufferStdArrayTest) { + std::array content{{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", Buffer{content.begin(), content.end(), "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=hello world\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostBufferRvalueTest) { + std::vector content{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", Buffer{content.begin(), content.end(), "test_file"}}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=hello world\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, ReflectPostBufferLvalueTest) { + std::vector content{'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'}; + Url url{server->GetBaseUrl() + "/form_post.html"}; + Buffer buff{content.begin(), content.end(), "test_file"}; + Response response = cpr::Post(url, Multipart{{"x", buff}}); + std::string expected_text{ + "{\n" + " \"x\": \"test_file=hello world\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostManyTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", 5}, {"y", 13}}); + std::string expected_text{ + "{\n" + " \"x\": \"5\",\n" + " \"y\": \"13\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostManyNoCopyTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Multipart multipart{{"x", 5}, {"y", 13}}; + Response response = cpr::Post(url, multipart); + std::string expected_text{ + "{\n" + " \"x\": \"5\",\n" + " \"y\": \"13\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostContentTypeTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url, Multipart{{"x", 5, "application/number"}}); + std::string expected_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, FormPostContentTypeLValueTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Multipart multipart{{"x", 5, "application/number"}}; + Response response = cpr::Post(url, multipart); + std::string expected_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, UrlPostAsyncSingleTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::PostAsync(url, payload)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(UrlEncodedPostTests, UrlReflectTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UrlEncodedPostTests, PostWithNoBodyTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Response response = cpr::Post(url); + std::string expected_text{"{\n}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +static std::string getTimestamp() { + const std::chrono::system_clock::time_point tp = std::chrono::system_clock::now(); + const time_t timeT = std::chrono::system_clock::to_time_t(tp); + // NOLINTNEXTLINE(concurrency-mt-unsafe) + struct tm* tm = gmtime(&timeT); + + std::string buf; + buf.resize(EXPIRES_STRING_SIZE); + + const size_t s = strftime(buf.data(), buf.size(), "%a, %d %b %Y %H:%M:%S GMT", tm); + EXPECT_GT(s, 0); + buf.resize(s); + return buf; +} + +TEST(UrlEncodedPostTests, PostReflectTest) { + std::string uri = server->GetBaseUrl() + "/post_reflect.html"; + std::string body = R"({"property1": "value1"})"; + std::string contentType = "application/json"; + std::string signature = "x-ms-date: something"; + std::string logType = "LoggingTest"; + std::string date = getTimestamp(); + Response response = cpr::Post(cpr::Url(uri), cpr::Header{{"content-type", contentType}, {"Authorization", signature}, {"log-type", logType}, {"x-ms-date", date}, {"content-length", std::to_string(body.length())}}, cpr::Body(body)); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(body, response.text); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(signature, response.header["Authorization"]); + EXPECT_EQ(logType, response.header["log-type"]); + EXPECT_EQ(date, response.header["x-ms-date"]); + EXPECT_EQ(std::to_string(body.length()), response.header["content-length"]); +} + +TEST(UrlEncodedPostTests, PostReflectPayloadTest) { + std::string uri = server->GetBaseUrl() + "/header_reflect.html"; + cpr::Payload payload = cpr::Payload{{"email", ""}, {"password", ""}, {"devicetoken", ""}}; + cpr::Response response = cpr::Post(cpr::Url(uri), cpr::Timeout{10000}, payload); + + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(200, response.status_code); +} + +TEST(UrlEncodedPostTests, InjectMultipleHeadersTest) { + std::string uri = server->GetBaseUrl() + "/post_reflect.html"; + std::string key_1 = "key_1"; + std::string val_1 = "value_1"; + std::string key_2 = "key_2"; + std::string val_2 = "value_2"; + cpr::Response response = cpr::Post(cpr::Url{uri}, cpr::Header{{key_1, val_1}}, cpr::Header{{key_2, val_2}}); + + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(val_1, response.header[key_1]); + EXPECT_EQ(val_2, response.header[key_2]); +} + +TEST(UrlEncodedPostTests, PostBodyWithFile) { + std::string filename{"test_file"}; + std::string expected_text(R"({"property1": "value1"})"); + std::ofstream test_file; + test_file.open(filename); + test_file << expected_text; + test_file.close(); + Url url{server->GetBaseUrl() + "/post_reflect.html"}; + cpr::Response response = Post(url, cpr::Header({{"Content-Type", "application/octet-stream"}}), cpr::Body(File("test_file"))); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(std::string{"application/octet-stream"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); +} + +TEST(UrlEncodedPostTests, PostBodyWithBuffer) { + Url url{server->GetBaseUrl() + "/post_reflect.html"}; + std::string expected_text(R"({"property1": "value1"})"); + cpr::Response response = Post(url, cpr::Header({{"Content-Type", "application/octet-stream"}}), cpr::Body(Buffer{expected_text.begin(), expected_text.end(), "test_file"})); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/octet-stream"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PostRedirectTests, TempRedirectTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}, Header{{"RedirectLocation", "url_post.html"}}); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(response.url, server->GetBaseUrl() + "/url_post.html"); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PostRedirectTests, TempRedirectNoneTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}, Header{{"RedirectLocation", "url_post.html"}}, Redirect(PostRedirectFlags::NONE)); + EXPECT_EQ(response.url, server->GetBaseUrl() + "/url_post.html"); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PostRedirectTests, PermRedirectTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}, Header{{"RedirectLocation", "url_post.html"}}); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(response.url, server->GetBaseUrl() + "/url_post.html"); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PostRedirectTests, PermRedirectNoneTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Response response = cpr::Post(url, Payload{{"x", "5"}}, Header{{"RedirectLocation", "url_post.html"}}, Redirect(PostRedirectFlags::NONE)); + EXPECT_EQ(response.url, server->GetBaseUrl() + "/url_post.html"); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/prepare_tests.cpp b/3rdparty/cpr/test/prepare_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..722b2815b78ae6c950c7a077b766609f67a461e6 --- /dev/null +++ b/3rdparty/cpr/test/prepare_tests.cpp @@ -0,0 +1,138 @@ +#include + +#include +#include + +#include "cpr/cpr.h" + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(PrepareTests, GetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.PrepareGet(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PrepareTests, OptionsTests) { + Url url{server->GetBaseUrl() + "/"}; + Session session; + session.SetUrl(url); + session.PrepareOptions(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"GET, POST, PUT, DELETE, PATCH, OPTIONS"}, response.header["Access-Control-Allow-Methods"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PrepareTests, PatchTest) { + Url url{server->GetBaseUrl() + "/patch.html"}; + Payload payload{{"x", "5"}}; + Session session; + session.SetUrl(url); + session.SetPayload(payload); + session.PreparePatch(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PrepareTests, MultipleDeleteHeadPutGetPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Url urlPost{server->GetBaseUrl() + "/post_reflect.html"}; + Url urlPut{server->GetBaseUrl() + "/put.html"}; + Session session; + for (size_t i = 0; i < 3; ++i) { + { + session.SetUrl(url); + session.PrepareDelete(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{"Header reflect DELETE"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(urlPost); + std::string expectedBody = "a1b2c3Post"; + session.SetBody(expectedBody); + session.PreparePost(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + EXPECT_EQ(expectedBody, response.text); + EXPECT_EQ(urlPost, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(url); + session.PrepareGet(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(urlPut); + session.SetPayload({{"x", "5"}}); + session.PreparePut(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(urlPut, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(url); + session.PrepareHead(); + CURLcode curl_result = curl_easy_perform(session.GetCurlHolder()->handle); + Response response = session.Complete(curl_result); + std::string expected_text{"Header reflect HEAD"}; + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + } +} + + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/proxy_auth_tests.cpp b/3rdparty/cpr/test/proxy_auth_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f618ba2f8c8361efa2a31579fbc60a52f76c1ef3 --- /dev/null +++ b/3rdparty/cpr/test/proxy_auth_tests.cpp @@ -0,0 +1,94 @@ +#include + +#include + +#include "cpr/cpr.h" +#include "httpServer.hpp" + +// TODO: This requires a local proxy server (squid). This should be replaced with a source +// code implementation. + +#define HTTP_PROXY "127.0.0.1:3128" +#define HTTPS_PROXY "127.0.0.1:3128" +#define PROXY_USER "u$er" +#define PROXY_PASS "p@ss" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +// TODO: These should be fixed after a source code implementation of a proxy +#if defined(false) +TEST(ProxyAuthTests, SingleProxyTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, Proxies{{"http", HTTP_PROXY}}, ProxyAuthentication{{"http", EncodedAuthentication{PROXY_USER, PROXY_PASS}}}); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyAuthTests, MultipleProxyHttpTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Response response = cpr::Get(url, Proxies{{"https", HTTPS_PROXY}, {"http", HTTP_PROXY}}, ProxyAuthentication{{"http", EncodedAuthentication{PROXY_USER, PROXY_PASS}}, {"https", EncodedAuthentication{PROXY_USER, PROXY_PASS}}}); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyAuthTests, CopyProxyTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Proxies proxies{{"http", HTTP_PROXY}}; + ProxyAuthentication proxy_auth{{"http", EncodedAuthentication{PROXY_USER, PROXY_PASS}}}; + Response response = cpr::Get(url, proxies, proxy_auth); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyAuthTests, ProxySessionTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetProxies(Proxies{{"http", HTTP_PROXY}}); + session.SetProxyAuth(ProxyAuthentication{{"http", EncodedAuthentication{PROXY_USER, PROXY_PASS}}}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyAuthTests, ReferenceProxySessionTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Proxies proxies{{"http", HTTP_PROXY}}; + ProxyAuthentication proxy_auth{{"http", EncodedAuthentication{PROXY_USER, PROXY_PASS}}}; + Session session; + session.SetUrl(url); + session.SetProxies(proxies); + session.SetProxyAuth(proxy_auth); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} +#endif + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/proxy_tests.cpp b/3rdparty/cpr/test/proxy_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ce5126dcba6af30e3fd5fa4baf86fbea9353d67d --- /dev/null +++ b/3rdparty/cpr/test/proxy_tests.cpp @@ -0,0 +1,92 @@ +#include + +#include + +#include "cpr/cpr.h" + +// TODO: This uses public servers for proxies and endpoints. This should be replaced with a source +// code implementation inside server.cpp + +#define HTTP_PROXY "51.159.4.98:80" +#define HTTPS_PROXY "51.104.53.182:8000" + +using namespace cpr; + +TEST(ProxyTests, SingleProxyTest) { + Url url{"http://www.httpbin.org/get"}; + Response response = cpr::Get(url, Proxies{{"http", HTTP_PROXY}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyTests, MultipleProxyHttpTest) { + Url url{"http://www.httpbin.org/get"}; + Response response = cpr::Get(url, Proxies{{"http", HTTP_PROXY}, {"https", HTTPS_PROXY}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +// TODO: These should be fixed after a source code implementation of an HTTPS proxy +#if defined(false) +TEST(ProxyTests, ProxyHttpsTest) { + Url url{"https://www.httpbin.org/get"}; + Response response = cpr::Get(url, Proxies{{"https", HTTPS_PROXY}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyTests, MultipleProxyHttpsTest) { + Url url{"https://www.httpbin.org/get"}; + Response response = cpr::Get(url, Proxies{{"http", HTTP_PROXY}, {"https", HTTPS_PROXY}}); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} +#endif + +TEST(ProxyTests, CopyProxyTest) { + Url url{"http://www.httpbin.org/get"}; + Proxies proxies{{"http", HTTP_PROXY}}; + Response response = cpr::Get(url, proxies); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyTests, ProxySessionTest) { + Url url{"http://www.httpbin.org/get"}; + Session session; + session.SetUrl(url); + session.SetProxies(Proxies{{"http", HTTP_PROXY}}); + Response response = session.Get(); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ProxyTests, ReferenceProxySessionTest) { + Url url{"http://www.httpbin.org/get"}; + Proxies proxies{{"http", HTTP_PROXY}}; + Session session; + session.SetUrl(url); + session.SetProxies(proxies); + Response response = session.Get(); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/put_tests.cpp b/3rdparty/cpr/test/put_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b9e1e3abde9249ae79eb8eb348f85ae2f3c3d6de --- /dev/null +++ b/3rdparty/cpr/test/put_tests.cpp @@ -0,0 +1,276 @@ +#include + +#include + +#include "cpr/cpr.h" + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(PutTests, PutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, PutUnallowedTest) { + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + Response response = cpr::Put(url, payload); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + Session session; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutUnallowedTest) { + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + Session session; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutUnallowedAfterGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Get(); + } + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutUnallowedAfterHeadTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/get.html"}; + session.SetUrl(url); + Response response = session.Head(); + } + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, SessionPutUnallowedAfterPostTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + Response response = session.Post(); + } + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + session.SetUrl(url); + session.SetPayload(payload); + Response response = session.Put(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, AsyncPutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + cpr::AsyncResponse future_response = cpr::PutAsync(url, payload); + cpr::Response response = future_response.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, AsyncPutUnallowedTest) { + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + cpr::AsyncResponse future_response = cpr::PutAsync(url, payload); + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PutTests, AsyncMultiplePutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + Payload payload{{"x", "5"}}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::PutAsync(url, payload)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(PutTests, AsyncMultiplePutUnallowedTest) { + Url url{server->GetBaseUrl() + "/put_unallowed.html"}; + Payload payload{{"x", "5"}}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + responses.emplace_back(cpr::PutAsync(url, payload)); + } + for (cpr::AsyncResponse& future_response : responses) { + cpr::Response response = future_response.get(); + std::string expected_text{"Method Not Allowed"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(405, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/raw_body_tests.cpp b/3rdparty/cpr/test/raw_body_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ae0e7925eea7728c92fde440a045a28dd7e10b84 --- /dev/null +++ b/3rdparty/cpr/test/raw_body_tests.cpp @@ -0,0 +1,134 @@ +#include + +#include +#include +#include + +#include "cpr/cpr.h" +#include "cpr/multipart.h" + +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(BodyPostTests, DefaultUrlEncodedPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Body{"x=5"}); + std::string expected_text = "{\n \"x\": 5\n}"; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, TextUrlEncodedPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Body{"x=hello world!!~"}); + std::string expected_text{ + "{\n" + " \"x\": hello world!!~\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, TextUrlEncodedNoCopyPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Body body{"x=hello world!!~"}; + // body lives through the lifetime of Post, so it doesn't need to be copied + Response response = cpr::Post(url, body); + std::string expected_text{ + "{\n" + " \"x\": hello world!!~\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, UrlEncodedManyPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Body{"x=5&y=13"}); + std::string expected_text{ + "{\n" + " \"x\": 5,\n" + " \"y\": 13,\n" + " \"sum\": 18\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, CustomHeaderNumberPostTest) { + Url url{server->GetBaseUrl() + "/json_post.html"}; + Response response = cpr::Post(url, Body{"{\"x\":5}"}, Header{{"Content-Type", "application/json"}}); + std::string expected_text{"{\"x\":5}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, CustomHeaderTextPostTest) { + Url url{server->GetBaseUrl() + "/json_post.html"}; + Response response = cpr::Post(url, Body{"{\"x\":\"hello world!!~\"}"}, Header{{"Content-Type", "application/json"}}); + std::string expected_text{"{\"x\":\"hello world!!~\"}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, CustomWrongHeaderPostTest) { + Url url{server->GetBaseUrl() + "/json_post.html"}; + Response response = cpr::Post(url, Body{"{\"x\":5}"}, Header{{"Content-Type", "text/plain"}}); + std::string expected_text{"Unsupported Media Type"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(415, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyPostTests, UrlPostBadHostTest) { + Url url{"http://bad_host/"}; + Response response = cpr::Post(url, Body{"hello=world"}); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(0, response.status_code); + EXPECT_EQ(ErrorCode::COULDNT_RESOLVE_HOST, response.error.code); +} + +TEST(BodyPostTests, StringMoveBodyTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Response response = cpr::Post(url, Body{std::string{"x=5"}}); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/resolve_tests.cpp b/3rdparty/cpr/test/resolve_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0378a26d59dc6dfa2846a163a6b2a4cbf2a1e453 --- /dev/null +++ b/3rdparty/cpr/test/resolve_tests.cpp @@ -0,0 +1,44 @@ +#include + +#include + +#include "cpr/cpr.h" +#include "httpServer.hpp" + +using namespace cpr; + +static HttpServer* server = new HttpServer(); + +TEST(ResolveTests, HelloWorldTest) { + Url url{"http://www.example.com:" + std::to_string(server->GetPort()) + "/hello.html"}; + Resolve resolve{"www.example.com", "127.0.0.1", {server->GetPort()}}; + Response response = cpr::Get(url, resolve); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ResolveTests, RedirectMultiple) { + Url url1{"http://www.example0.com:" + std::to_string(server->GetPort()) + "/resolve_permanent_redirect.html"}; + Url url2{"http://www.example1.com:" + std::to_string(server->GetPort()) + "/hello.html"}; + Resolve resolve1{"www.example0.com", "127.0.0.1", {server->GetPort()}}; + Resolve resolve2{"www.example1.com", "127.0.0.1", {server->GetPort()}}; + + Response response = cpr::Get(url1, std::vector{resolve1, resolve2}, Header{{"RedirectLocation", url2.str()}}); + + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url2, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/session_tests.cpp b/3rdparty/cpr/test/session_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a7f20880413e08ad510b0c936cc5620106fda3ef --- /dev/null +++ b/3rdparty/cpr/test/session_tests.cpp @@ -0,0 +1,1602 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include "cpr/cpr.h" +#include +#include + +#include "cpr/accept_encoding.h" +#include "httpServer.hpp" + +using namespace cpr; +using namespace std::chrono_literals; + +static HttpServer* server = new HttpServer(); +std::chrono::milliseconds sleep_time{50}; +std::chrono::seconds zero{0}; + +bool write_data(const std::string_view& /*data*/, intptr_t /*userdata*/) { + return true; +} + +TEST(SessionGetTests, GetMultipleTimes) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + std::string expected_text{"Hello world!"}; + + for (size_t i = 0; i < 100; i++) { + Response response = session.Get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(SessionPostTests, PostMultipleTimes) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Session session; + session.SetUrl(url); + session.SetPayload({{"x", "5"}}); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + + for (size_t i = 0; i < 100; i++) { + Response response = session.Post(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(RedirectTests, TemporaryDefaultRedirectTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Session session; + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(RedirectTests, NoTemporaryRedirectTest) { + Url url{server->GetBaseUrl() + "/temporary_redirect.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(false)); + Response response = session.Get(); + std::string expected_text{"Moved Temporarily"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(302, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(RedirectTests, PermanentDefaultRedirectTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Session session; + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(RedirectTests, NoPermanentRedirectTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(false)); + Response response = session.Get(); + std::string expected_text{"Moved Permanently"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(301, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MaxRedirectsTests, ZeroMaxRedirectsSuccessTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(0L)); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MaxRedirectsTests, ZeroMaxRedirectsFailureTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(0L)); + Response response = session.Get(); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(301, response.status_code); + EXPECT_EQ(ErrorCode::TOO_MANY_REDIRECTS, response.error.code); +} + +TEST(MaxRedirectsTests, OneMaxRedirectsSuccessTest) { + Url url{server->GetBaseUrl() + "/permanent_redirect.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(1L)); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MaxRedirectsTests, OneMaxRedirectsFailureTest) { + Url url{server->GetBaseUrl() + "/two_redirects.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(1L)); + Response response = session.Get(); + EXPECT_EQ(std::string{}, response.text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/permanent_redirect.html"}, response.url); + EXPECT_EQ(std::string{}, response.header["content-type"]); + EXPECT_EQ(301, response.status_code); + EXPECT_EQ(ErrorCode::TOO_MANY_REDIRECTS, response.error.code); +} + +TEST(MaxRedirectsTests, TwoMaxRedirectsSuccessTest) { + Url url{server->GetBaseUrl() + "/two_redirects.html"}; + Session session; + session.SetUrl(url); + session.SetRedirect(Redirect(2L)); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{server->GetBaseUrl() + "/hello.html"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MultipleGetTests, BasicMultipleGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + for (size_t i = 0; i < 100; ++i) { + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, UrlChangeMultipleGetTest) { + Session session; + { + Url url{server->GetBaseUrl() + "/hello.html"}; + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Url url{server->GetBaseUrl() + "/basic.json"}; + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{ + "[\n" + " {\n" + " \"first_key\": \"first_value\",\n" + " \"second_key\": \"second_value\"\n" + " }\n" + "]"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, HeaderMultipleGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + session.SetHeader(Header{{"hello", "world"}}); + for (size_t i = 0; i < 100; ++i) { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, HeaderChangeMultipleGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + session.SetHeader(Header{{"hello", "world"}}); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"world"}, response.header["hello"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + session.SetHeader(Header{{"key", "value"}}); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(std::string{"value"}, response.header["key"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, ParameterMultipleGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetParameters({{"hello", "world"}}); + for (size_t i = 0; i < 100; ++i) { + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, ParameterChangeMultipleGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetParameters({{"hello", "world"}}); + { + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + session.SetUrl(url); + session.SetParameters({{"key", "value"}}); + { + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?key=value"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, BasicAuthenticationMultipleGetTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Session session; + session.SetUrl(url); + session.SetAuth(Authentication{"user", "password", AuthMode::BASIC}); + for (size_t i = 0; i < 100; ++i) { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(MultipleGetTests, BasicAuthenticationChangeMultipleGetTest) { + Url url{server->GetBaseUrl() + "/basic_auth.html"}; + Session session; + session.SetUrl(url); + session.SetAuth(Authentication{"user", "password", AuthMode::BASIC}); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + session.SetAuth(Authentication{"user", "bad_password", AuthMode::BASIC}); + { + Response response = session.Get(); + EXPECT_EQ(std::string{"Unauthorized"}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + session.SetAuth(Authentication{"bad_user", "password", AuthMode::BASIC}); + { + Response response = session.Get(); + EXPECT_EQ(std::string{"Unauthorized"}, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/plain"}, response.header["content-type"]); + EXPECT_EQ(401, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(ParameterTests, ParameterSingleTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + Parameters parameters{{"hello", "world"}}; + session.SetParameters(parameters); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ParameterTests, ParameterMultipleTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + Parameters parameters{{"hello", "world"}, {"key", "value"}}; + session.SetParameters(parameters); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(Url{url + "?hello=world&key=value"}, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(FullRequestUrlTest, GetFullRequestUrlNoParametersTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + std::string expected_text{server->GetBaseUrl() + "/hello.html"}; + EXPECT_EQ(expected_text, session.GetFullRequestUrl()); +} + +TEST(FullRequestUrlTest, GetFullRequestUrlOneParameterTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + Parameters parameters{{"hello", "world"}}; + session.SetParameters(parameters); + std::string expected_text{server->GetBaseUrl() + "/hello.html" + "?hello=world"}; + EXPECT_EQ(expected_text, session.GetFullRequestUrl()); +} + +TEST(FullRequestUrlTest, GetFullRequestUrlMultipleParametersTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + Parameters parameters{{"hello", "world"}, {"key", "value"}}; + session.SetParameters(parameters); + std::string expected_text{server->GetBaseUrl() + "/hello.html" + "?hello=world&key=value"}; + EXPECT_EQ(expected_text, session.GetFullRequestUrl()); +} + +TEST(TimeoutTests, SetTimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(0L); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(TimeoutTests, SetTimeoutLongTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(10000L); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(TimeoutTests, SetTimeoutLowSpeed) { + Url url{server->GetBaseUrl() + "/low_speed_timeout.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(1000); + Response response = session.Get(); + EXPECT_EQ(url, response.url); + // Do not check for the HTTP status code, since libcurl always provides the status code of the header if it was received + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(TimeoutTests, SetChronoTimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(std::chrono::milliseconds{0}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(TimeoutTests, SetChronoTimeoutLongTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(std::chrono::milliseconds{10000}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + // Do not check for the HTTP status code, since libcurl always provides the status code of the header if it was received + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(TimeoutTests, SetChronoLiteralTimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(2s); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(TimeoutTests, SetChronoLiteralTimeoutLowSpeed) { + Url url{server->GetBaseUrl() + "/low_speed_timeout.html"}; + Session session; + session.SetUrl(url); + session.SetTimeout(1000ms); + Response response = session.Get(); + EXPECT_EQ(url, response.url); + // Do not check for the HTTP status code, since libcurl always provides the status code of the header if it was received + EXPECT_EQ(ErrorCode::OPERATION_TIMEDOUT, response.error.code); +} + +TEST(ConnectTimeoutTests, SetConnectTimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetConnectTimeout(0L); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ConnectTimeoutTests, SetConnectTimeoutLongTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetConnectTimeout(10000L); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ConnectTimeoutTests, SetChronoConnectTimeoutTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetConnectTimeout(std::chrono::milliseconds{0}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(ConnectTimeoutTests, SetChronoConnectTimeoutLongTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetConnectTimeout(std::chrono::milliseconds{10000}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(LowSpeedTests, SetLowSpeedTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetLowSpeed({1, 1}); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PayloadTests, SetPayloadTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Session session; + session.SetUrl(url); + session.SetPayload({{"x", "5"}}); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(PayloadTests, SetPayloadLValueTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Session session; + session.SetUrl(url); + Payload payload{{"x", "5"}}; + session.SetPayload(payload); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MultipartTests, SetMultipartTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Session session; + session.SetUrl(url); + session.SetMultipart({{"x", "5"}}); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MultipartTests, SetMultipartValueTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Session session; + session.SetUrl(url); + Multipart multipart{{"x", "5"}}; + session.SetMultipart(multipart); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(MultipartTests, SetMultipartVectorPartsTest) { + Url url{server->GetBaseUrl() + "/form_post.html"}; + Session session; + session.SetUrl(url); + Multipart multipart{std::vector{Part{"x", "5"}}}; + session.SetMultipart(multipart); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": \"5\"\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyTests, SetBodyTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Session session; + session.SetUrl(url); + session.SetBody(Body{"x=5"}); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(BodyTests, SetBodyValueTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + Session session; + session.SetUrl(url); + Body body{"x=5"}; + session.SetBody(body); + Response response = session.Post(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(DigestTests, SetDigestTest) { + Url url{server->GetBaseUrl() + "/digest_auth.html"}; + Session session; + session.SetUrl(url); + session.SetAuth({"user", "password", AuthMode::DIGEST}); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UserAgentTests, SetUserAgentTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + UserAgent userAgent{"Test User Agent"}; + Session session; + session.SetUrl(url); + session.SetUserAgent(userAgent); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(userAgent, response.header["User-Agent"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(UserAgentTests, SetUserAgentStringViewTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + UserAgent userAgent{std::string_view{"Test User Agent"}}; + Session session; + session.SetUrl(url); + session.SetUserAgent(userAgent); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(userAgent, response.header["User-Agent"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CookiesTests, BasicCookiesTest) { + Url url{server->GetBaseUrl() + "/basic_cookies.html"}; + Session session{}; + session.SetUrl(url); + Response response = session.Get(); + Cookies res_cookies{response.cookies}; + std::string expected_text{"Basic Cookies"}; + cpr::Cookies expectedCookies{ + {"SID", "31d4d96e407aad42", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + {"lang", "en-US", "127.0.0.1", false, "/", true, std::chrono::system_clock::time_point{} + std::chrono::seconds(3905119080)}, + }; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + for (auto cookie = res_cookies.begin(), expectedCookie = expectedCookies.begin(); cookie != res_cookies.end() && expectedCookie != expectedCookies.end(); cookie++, expectedCookie++) { + EXPECT_EQ(expectedCookie->GetName(), cookie->GetName()); + EXPECT_EQ(expectedCookie->GetValue(), cookie->GetValue()); + EXPECT_EQ(expectedCookie->GetDomain(), cookie->GetDomain()); + EXPECT_EQ(expectedCookie->IsIncludingSubdomains(), cookie->IsIncludingSubdomains()); + EXPECT_EQ(expectedCookie->GetPath(), cookie->GetPath()); + EXPECT_EQ(expectedCookie->IsHttpsOnly(), cookie->IsHttpsOnly()); + EXPECT_EQ(expectedCookie->GetExpires(), cookie->GetExpires()); + } +} + +TEST(CookiesTests, ClientSetCookiesTest) { + Url url{server->GetBaseUrl() + "/cookies_reflect.html"}; + { + Session session{}; + session.SetUrl(url); + session.SetCookies(Cookies{ + {"SID", "31d4d96e407aad42"}, + {"lang", "en-US"}, + }); + Response response = session.Get(); + std::string expected_text{"SID=31d4d96e407aad42; lang=en-US;"}; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + } + { + Session session{}; + session.SetUrl(url); + Cookies cookie{ + {"SID", "31d4d96e407aad42"}, + {"lang", "en-US"}, + }; + session.SetCookies(cookie); + Response response = session.Get(); + std::string expected_text{"SID=31d4d96e407aad42; lang=en-US;"}; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + } +} + +TEST(CookiesTests, RedirectionWithChangingCookiesTest) { + Url url{server->GetBaseUrl() + "/redirection_with_changing_cookies.html"}; + { + Session session{}; + session.SetUrl(url); + session.SetCookies(Cookies{ + {"SID", "31d4d96e407aad42"}, + {"lang", "en-US"}, + }); + session.SetRedirect(Redirect(0L)); + Response response = session.Get(); + std::string expected_text{"Received cookies are: SID=31d4d96e407aad42; lang=en-US;"}; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + } + { + Session session{}; + session.SetUrl(url); + session.SetRedirect(Redirect(1L)); + Response response = session.Get(); + std::string expected_text{"Received cookies are: lang=en-US; SID=31d4d96e407aad42"}; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + } + { + Session session{}; + session.SetUrl(url); + session.SetCookies(Cookies{ + {"SID", "empty_sid"}, + }); + session.SetRedirect(Redirect(1L)); + Response response = session.Get(); + std::string expected_text{"Received cookies are: lang=en-US; SID=31d4d96e407aad42; SID=empty_sid;"}; + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + EXPECT_EQ(expected_text, response.text); + } +} + +TEST(DifferentMethodTests, GetPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(DifferentMethodTests, PostGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(DifferentMethodTests, GetPostGetTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(DifferentMethodTests, PostGetPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(DifferentMethodTests, MultipleGetPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + for (size_t i = 0; i < 100; ++i) { + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + } +} + +TEST(DifferentMethodTests, MultipleDeleteHeadPutGetPostTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Url urlPost{server->GetBaseUrl() + "/post_reflect.html"}; + Url urlPut{server->GetBaseUrl() + "/put.html"}; + Session session; + for (size_t i = 0; i < 10; ++i) { + { + session.SetUrl(url); + Response response = session.Delete(); + std::string expected_text{"Header reflect DELETE"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(urlPost); + std::string expectedBody = "a1b2c3Post"; + session.SetBody(expectedBody); + Response response = session.Post(); + EXPECT_EQ(expectedBody, response.text); + EXPECT_EQ(urlPost, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(url); + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(urlPut); + session.SetPayload({{"x", "5"}}); + Response response = session.Put(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(urlPut, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + session.SetUrl(url); + Response response = session.Head(); + std::string expected_text{"Header reflect HEAD"}; + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + } +} + +TEST(CurlHolderManipulateTests, CustomOptionTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + curl_easy_setopt(session.GetCurlHolder()->handle, CURLOPT_SSL_OPTIONS, CURLSSLOPT_ALLOW_BEAST | CURLSSLOPT_NO_REVOKE); + { + Response response = session.Get(); + std::string expected_text{"Header reflect GET"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } + { + Response response = session.Post(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + } +} + +TEST(LocalPortTests, SetLocalPortTest) { + Url url{server->GetBaseUrl() + "/local_port.html"}; + Session session; + uint16_t local_port{0}; + uint16_t local_port_range{0}; + Response response; + + // Try up to 10 times to get a free local port + for (size_t i = 0; i < 10; i++) { + session.SetUrl(url); + local_port = 40252 + (i * 100); // beware of HttpServer::GetPort when changing + local_port_range = 7000; + session.SetLocalPort(local_port); + session.SetLocalPortRange(local_port_range); + // expected response: body contains port number in specified range + // NOTE: even when trying up to 7000 ports there is the chance that all of them are occupied. + // It would be possible to also check here for ErrorCode::UNKNOWN_ERROR but that somehow seems + // wrong as then this test would pass in case SetLocalPort does not work at all + // or in other words: we have to assume that at least one port in the specified range is free. + response = session.Get(); + + if (response.error.code != ErrorCode::UNKNOWN_ERROR) { + break; + } + } + + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + // NOLINTNEXTLINE(google-runtime-int) + unsigned long port_from_response = std::strtoul(response.text.c_str(), nullptr, 10); + EXPECT_EQ(errno, 0); + EXPECT_GE(port_from_response, local_port); + EXPECT_LE(port_from_response, local_port + local_port_range); +} + +TEST(LocalPortTests, SetOptionTest) { + Url url{server->GetBaseUrl() + "/local_port.html"}; + Session session; + uint16_t local_port{0}; + uint16_t local_port_range{0}; + Response response; + + // Try up to 10 times to get a free local port + for (size_t i = 0; i < 10; i++) { + session.SetUrl(url); + local_port = 30252 + (i * 100); // beware of HttpServer::GetPort when changing + local_port_range = 7000; + session.SetOption(LocalPort(local_port)); + session.SetOption(LocalPortRange(local_port_range)); + // expected response: body contains port number in specified range + // NOTE: even when trying up to 7000 ports there is the chance that all of them are occupied. + // It would be possible to also check here for ErrorCode::UNKNOWN_ERROR but that somehow seems + // wrong as then this test would pass in case SetLocalPort does not work at all + // or in other words: we have to assume that at least one port in the specified range is free. + response = session.Get(); + + if (response.error.code != ErrorCode::UNKNOWN_ERROR) { + break; + } + } + + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + // NOLINTNEXTLINE(google-runtime-int) + unsigned long port_from_response = std::strtoul(response.text.c_str(), nullptr, 10); + EXPECT_EQ(errno, 0); + EXPECT_GE(port_from_response, local_port); + EXPECT_LE(port_from_response, local_port + local_port_range); +} + +// The tests using the port of the server as a source port for curl fail for windows. +// The reason probably is that Windows allows two sockets to bind to the same port if the full hostname is different. +// In these tests, mongoose binds to http://127.0.0.1:61936, while libcurl binds to a different hostname, but still port 61936. +// This seems to be okay for Windows, however, these tests expect an error like on Linux and MacOS +// We therefore, simply skip the tests if Windows is used +#ifndef _WIN32 +TEST(LocalPortTests, SetLocalPortTestOccupied) { + Url url{server->GetBaseUrl() + "/local_port.html"}; + Session session; + session.SetUrl(url); + session.SetLocalPort(server->GetPort()); + // expected response: request cannot be made as port is already occupied + Response response = session.Get(); + EXPECT_EQ(ErrorCode::INTERFACE_FAILED, response.error.code); +} + +TEST(LocalPortTests, SetOptionTestOccupied) { + Url url{server->GetBaseUrl() + "/local_port.html"}; + Session session; + session.SetUrl(url); + session.SetOption(LocalPort(server->GetPort())); + // expected response: request cannot be made as port is already occupied + Response response = session.Get(); + EXPECT_EQ(ErrorCode::INTERFACE_FAILED, response.error.code); +} +#endif // _WIN32 + +TEST(BasicTests, ReserveResponseString) { + Url url{server->GetBaseUrl() + "/hello.html"}; + Session session; + session.SetUrl(url); + session.SetReserveSize(4096); + Response response = session.Get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_GE(response.text.capacity(), 4096); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +std::vector Split(const std::string& s) { + std::vector encodings; + std::stringstream ss(s); + std::string encoding; + + while (std::getline(ss, encoding, ',')) { + encoding.erase(std::remove_if(encoding.begin(), encoding.end(), isspace), encoding.end()); // Trim + encodings.push_back(encoding); + } + + return encodings; +} + +void CompareEncodings(const std::string& response, const std::vector& expected) { + const std::vector responseVec = Split(response); + + EXPECT_EQ(responseVec.size(), expected.size()); + for (const std::string& encoding : expected) { + EXPECT_TRUE(std::find(responseVec.begin(), responseVec.end(), encoding) != responseVec.end()); + } +} + +TEST(BasicTests, AcceptEncodingTestWithMethodsStringMap) { + Url url{server->GetBaseUrl() + "/check_accept_encoding.html"}; + Session session; + session.SetUrl(url); + session.SetAcceptEncoding({{AcceptEncodingMethods::deflate, AcceptEncodingMethods::gzip, AcceptEncodingMethods::zlib}}); + Response response = session.Get(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + CompareEncodings(response.text, std::vector{"deflate", "gzip", "zlib"}); +} + +TEST(BasicTests, AcceptEncodingTestWithMethodsStringMapLValue) { + Url url{server->GetBaseUrl() + "/check_accept_encoding.html"}; + Session session; + session.SetUrl(url); + AcceptEncoding accept_encoding{{AcceptEncodingMethods::deflate, AcceptEncodingMethods::gzip, AcceptEncodingMethods::zlib}}; + session.SetAcceptEncoding(accept_encoding); + Response response = session.Get(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + CompareEncodings(response.text, std::vector{"deflate", "gzip", "zlib"}); +} + +TEST(BasicTests, AcceptEncodingTestWithCostomizedString) { + Url url{server->GetBaseUrl() + "/check_accept_encoding.html"}; + Session session; + session.SetUrl(url); + session.SetAcceptEncoding({{"deflate", "gzip", "zlib"}}); + Response response = session.Get(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + CompareEncodings(response.text, std::vector{"deflate", "gzip", "zlib"}); +} + +TEST(BasicTests, AcceptEncodingTestWithCostomizedStringLValue) { + Url url{server->GetBaseUrl() + "/check_accept_encoding.html"}; + Session session; + session.SetUrl(url); + AcceptEncoding accept_encoding{{"deflate", "gzip", "zlib"}}; + session.SetAcceptEncoding(accept_encoding); + Response response = session.Get(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + CompareEncodings(response.text, std::vector{"deflate", "gzip", "zlib"}); +} + +TEST(BasicTests, AcceptEncodingTestDisabled) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + session.SetAcceptEncoding({AcceptEncodingMethods::disabled}); + Response response = session.Get(); + + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); + // Ensure no 'Accept-Encoding' header got added + EXPECT_TRUE(response.header.find("Accept-Encoding") == response.header.end()); +} + +TEST(BasicTests, AcceptEncodingTestDisabledMultipleThrow) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + Session session; + session.SetUrl(url); + session.SetAcceptEncoding({AcceptEncodingMethods::disabled, AcceptEncodingMethods::deflate}); + EXPECT_THROW(session.Get(), std::invalid_argument); +} + +TEST(BasicTests, DisableHeaderExpect100ContinueTest) { + Url url{server->GetBaseUrl() + "/check_expect_100_continue.html"}; + std::string filename{"test_file"}; + std::string content{std::string(1024 * 1024, 'a')}; + std::ofstream test_file; + test_file.open(filename); + test_file << content; + test_file.close(); + Session session{}; + session.SetUrl(url); + session.SetMultipart({{"file", File{"test_file"}}}); + Response response = session.Post(); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncGetTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + cpr::AsyncResponse future = session->GetAsync(); + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); +} + +TEST(AsyncRequestsTests, AsyncGetMultipleTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + + std::vector responses; + std::vector> sessions; + for (size_t i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + sessions.emplace_back(session); + responses.emplace_back(session->GetAsync()); + } + + for (cpr::AsyncResponse& future : responses) { + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + } +} + +TEST(AsyncRequestsTests, AsyncGetMultipleTemporarySessionTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + responses.emplace_back(session->GetAsync()); + } + + for (cpr::AsyncResponse& future : responses) { + std::string expected_text{"Hello world!"}; + cpr::Response response = future.get(); + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + } +} + +TEST(AsyncRequestsTests, AsyncGetMultipleReflectTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::vector responses; + for (size_t i = 0; i < 10; ++i) { + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetParameters({{"key", std::to_string(i)}}); + responses.emplace_back(session->GetAsync()); + } + int i = 0; + for (cpr::AsyncResponse& future : responses) { + cpr::Response response = future.get(); + std::string expected_text{"Hello world!"}; + Url expected_url{url + "?key=" + std::to_string(i)}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(expected_url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + ++i; + } +} + +TEST(AsyncRequestsTests, AsyncWritebackDownloadTest) { + std::shared_ptr session = std::make_shared(); + cpr::Url url{server->GetBaseUrl() + "/download_gzip.html"}; + session->SetUrl(url); + session->SetHeader(cpr::Header{{"Accept-Encoding", "gzip"}}); + cpr::AsyncResponse future = session->DownloadAsync(cpr::WriteCallback{write_data, 0}); + cpr::Response response = future.get(); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(cpr::ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncPostTest) { + Url url{server->GetBaseUrl() + "/url_post.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload({{"x", "5"}}); + cpr::AsyncResponse future = session->PostAsync(); + cpr::Response response = future.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(201, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncPutTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload({{"x", "5"}}); + cpr::AsyncResponse future = session->PutAsync(); + cpr::Response response = future.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncHeadTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + cpr::AsyncResponse future = session->HeadAsync(); + cpr::Response response = future.get(); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncDeleteTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + cpr::AsyncResponse future = session->DeleteAsync(); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect DELETE"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncOptionsTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + cpr::AsyncResponse future = session->OptionsAsync(); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect OPTIONS"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(AsyncRequestsTests, AsyncPatchTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + cpr::AsyncResponse future = session->PatchAsync(); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect PATCH"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, GetCallbackTest) { + Url url{server->GetBaseUrl() + "/hello.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->GetCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{"Hello world!"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, PostCallbackTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->PostCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect POST"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, PutCallbackTest) { + Url url{server->GetBaseUrl() + "/put.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + session->SetPayload({{"x", "5"}}); + auto future = session->PutCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{ + "{\n" + " \"x\": 5\n" + "}"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"application/json"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, HeadCallbackTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->HeadCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{""}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, DeleteCallbackTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->DeleteCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect DELETE"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, OptionsCallbackTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->OptionsCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect OPTIONS"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, PatchCallbackTest) { + Url url{server->GetBaseUrl() + "/header_reflect.html"}; + std::shared_ptr session = std::make_shared(); + session->SetUrl(url); + auto future = session->PatchCallback([](Response r) { return r; }); + std::this_thread::sleep_for(sleep_time); + cpr::Response response = future.get(); + std::string expected_text{"Header reflect PATCH"}; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code); +} + +TEST(CallbackTests, Move) { + auto session = Session(); + session.SetDebugCallback(DebugCallback([](auto, auto, auto) {})); + + auto use = +[](Session& s) { + s.SetUrl(server->GetBaseUrl()); + s.Get(); + }; + use(session); +} + + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + ::testing::AddGlobalTestEnvironment(server); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/singleton_tests.cpp b/3rdparty/cpr/test/singleton_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6a18688f1d3772536372f8baf60b694380ce41ba --- /dev/null +++ b/3rdparty/cpr/test/singleton_tests.cpp @@ -0,0 +1,23 @@ +#include + +#include "singleton_tests.hpp" + +// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables) +CPR_SINGLETON_IMPL(TestSingleton) + +TEST(SingletonTests, GetInstanceTest) { + const TestSingleton* singleton = TestSingleton::GetInstance(); + EXPECT_NE(singleton, nullptr); +} + +TEST(SingletonTests, ExitInstanceTest) { + TestSingleton* singleton = TestSingleton::GetInstance(); + TestSingleton::ExitInstance(); + singleton = TestSingleton::GetInstance(); + EXPECT_EQ(singleton, nullptr); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/singleton_tests.hpp b/3rdparty/cpr/test/singleton_tests.hpp new file mode 100644 index 0000000000000000000000000000000000000000..cbf34d703e665ae320429fda0fbaec9559e01ce3 --- /dev/null +++ b/3rdparty/cpr/test/singleton_tests.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include "cpr/cpr.h" + +class TestSingleton { + CPR_SINGLETON_DECL(TestSingleton) + private: + TestSingleton() = default; +}; diff --git a/3rdparty/cpr/test/ssl_tests.cpp b/3rdparty/cpr/test/ssl_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..19ea4e4c8eca9c0c10532fc82b0af519dfa0b9dd --- /dev/null +++ b/3rdparty/cpr/test/ssl_tests.cpp @@ -0,0 +1,175 @@ +#include + +#include +#include +#include +#include + +#include "cpr/cprtypes.h" +#include "cpr/filesystem.h" +#include "cpr/ssl_options.h" + +#include "httpsServer.hpp" + + +using namespace cpr; + +static HttpsServer* server; + +static std::string caCertPath; +static std::string serverPubKeyPath; +static std::string clientKeyPath; +static std::string clientCertPath; + +std::string loadCertificateFromFile(const std::string certPath) { + std::ifstream certFile(certPath); + std::stringstream buffer; + buffer << certFile.rdbuf(); + return buffer.str(); +} + +TEST(SslTests, HelloWorldTestSimpel) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string baseDirPath{server->getBaseDirPath()}; + std::string crtPath{baseDirPath + "certificates/"}; + std::string keyPath{baseDirPath + "keys/"}; + + SslOptions sslOpts = Ssl(ssl::CaInfo{crtPath + "ca-bundle.crt"}, ssl::CertFile{crtPath + "client.crt"}, ssl::KeyFile{keyPath + "client.key"}, ssl::VerifyPeer{true}, ssl::PinnedPublicKey{keyPath + "server.pub"}, ssl::VerifyHost{true}, ssl::VerifyStatus{false}); + Response response = cpr::Get(url, sslOpts, Timeout{5000}, Verbose{}); + std::string expected_text = "Hello world!"; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code) << response.error.message; +} + +TEST(SslTests, HelloWorldTestFull) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string baseDirPath{server->getBaseDirPath()}; + std::string crtPath{baseDirPath + "certificates/"}; + std::string keyPath{baseDirPath + "keys/"}; + + SslOptions sslOpts = Ssl(ssl::TLSv1{}, ssl::ALPN{false}, +#if SUPPORT_NPN + ssl::NPN{false}, +#endif // DEBUG + ssl::CaInfo{crtPath + "ca-bundle.crt"}, ssl::CertFile{crtPath + "client.crt"}, ssl::KeyFile{keyPath + "client.key"}, ssl::PinnedPublicKey{keyPath + "server.pub"}, ssl::VerifyPeer{true}, ssl::VerifyHost{true}, ssl::VerifyStatus{false}); + Response response = cpr::Get(url, sslOpts, Timeout{5000}, Verbose{}); + std::string expected_text = "Hello world!"; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code) << response.error.message; +} + +TEST(SslTests, GetCertInfos) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + Url url{server->GetBaseUrl() + "/hello.html"}; + std::string baseDirPath{server->getBaseDirPath()}; + std::string crtPath{baseDirPath + "certificates/"}; + std::string keyPath{baseDirPath + "keys/"}; + + SslOptions sslOpts = Ssl(ssl::CaInfo{crtPath + "ca-bundle.crt"}, ssl::CertFile{crtPath + "client.crt"}, ssl::KeyFile{keyPath + "client.key"}, ssl::VerifyPeer{true}, ssl::VerifyHost{true}, ssl::VerifyStatus{false}); + + Response response = cpr::Get(url, sslOpts, Timeout{5000}, Verbose{}); + std::vector certInfos = response.GetCertInfos(); + + std::string expected_text = "Hello world!"; + std::vector expectedCertInfos{ + CertInfo{ + "Subject:CN = test-server", + "Issuer:C = GB, O = Example, CN = Sub CA", + "Version:2", + "Serial Number:acbefc2cde5b900b55548396556765d4", + "Signature Algorithm:ED25519", + "Public Key Algorithm:ED25519", + "X509v3 Authority Key Identifier:9B:B1:9B:21:61:DC:66:2B:3A:AD:ED:84:F1:05:B6:CE:99:82:C1:FC", + "X509v3 Basic Constraints:CA:FALSE", + "X509v3 Extended Key Usage:TLS Web Client Authentication, TLS Web Server Authentication", + "X509v3 Key Usage:Digital Signature, Key Encipherment", + "X509v3 Subject Key Identifier:66:47:54:F8:25:97:56:9A:52:56:35:B4:A7:52:60:0C:E7:4F:33:09", + "X509v3 Subject Alternative Name:DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1", + "Start date:May 7 10:18:22 2024 GMT", + "Expire date:May 6 10:18:22 2029 GMT", + "Signature:6d:63:d9:11:a3:9b:c7:9f:b6:23:12:27:e9:34:e0:a1:a3:20:be:fb:df:80:fe:53:08:9d:8c:e4:82:42:76:c2:55:13:e8:7c:86:83:33:0b:9a:9f:92:2a:3f:de:e9:32:78:c0:b1:bc:3f:42:e9:17:f9:9f:6c:15:35:a3:01:09:", + R"(Cert:-----BEGIN CERTIFICATE----- +MIIBtDCCAWagAwIBAgIRAKy+/CzeW5ALVVSDllVnZdQwBQYDK2VwMDAxCzAJBgNV +BAYTAkdCMRAwDgYDVQQKDAdFeGFtcGxlMQ8wDQYDVQQDDAZTdWIgQ0EwHhcNMjQw +NTA3MTAxODIyWhcNMjkwNTA2MTAxODIyWjAWMRQwEgYDVQQDDAt0ZXN0LXNlcnZl +cjAqMAUGAytlcAMhACdLUqJFSyspgGKJiXNlnOLU2dO/TLV+b8aIZNAX7EuVo4Gu +MIGrMB8GA1UdIwQYMBaAFJuxmyFh3GYrOq3thPEFts6ZgsH8MAwGA1UdEwEB/wQC +MAAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDwEB/wQEAwIF +oDAdBgNVHQ4EFgQUZkdU+CWXVppSVjW0p1JgDOdPMwkwLAYDVR0RBCUwI4IJbG9j +YWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMAUGAytlcANBAG1j2RGjm8ef +tiMSJ+k04KGjIL7734D+UwidjOSCQnbCVRPofIaDMwuan5IqP97pMnjAsbw/QukX ++Z9sFTWjAQk= +-----END CERTIFICATE----- +)", + }, + }; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code) << response.error.message; + EXPECT_EQ(1, certInfos.size()); + for (auto certInfo_it = certInfos.begin(), expectedCertInfo_it = expectedCertInfos.begin(); certInfo_it != certInfos.end() && expectedCertInfo_it != expectedCertInfos.end(); certInfo_it++, expectedCertInfo_it++) { + for (auto entry_it = (*certInfo_it).begin(), expectedEntry_it = (*expectedCertInfo_it).begin(); entry_it != (*certInfo_it).end() && expectedEntry_it != (*expectedCertInfo_it).end(); entry_it++, expectedEntry_it++) { + std::string search_string = "Identifier:keyid:"; + std::size_t search_index = (*entry_it).find(search_string); + if (search_index != std::string::npos) { + (*entry_it).replace(search_index, search_string.length(), "Identifier:"); + search_string = "\n"; + search_index = (*entry_it).find(search_string); + if (search_index != std::string::npos) { + (*entry_it).replace(search_index, search_string.length(), ""); + } + } + EXPECT_EQ(*expectedEntry_it, *entry_it); + } + std::cout << '\n'; + } +} + +#if SUPPORT_CURLOPT_SSL_CTX_FUNCTION +TEST(SslTests, LoadCertFromBufferTestSimpel) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + + Url url{server->GetBaseUrl() + "/hello.html"}; + + std::string baseDirPath{server->getBaseDirPath()}; + std::string crtPath{baseDirPath + "certificates/"}; + std::string keyPath{baseDirPath + "keys/"}; + std::string certBuffer = loadCertificateFromFile(crtPath + "ca-bundle.crt"); + SslOptions sslOpts = Ssl(ssl::CaBuffer{std::move(certBuffer)}, ssl::CertFile{crtPath + "client.crt"}, ssl::KeyFile{keyPath + "client.key"}, ssl::VerifyPeer{true}, ssl::VerifyHost{true}, ssl::VerifyStatus{false}); + Response response = cpr::Get(url, sslOpts, Timeout{5000}, Verbose{}); + std::string expected_text = "Hello world!"; + EXPECT_EQ(expected_text, response.text); + EXPECT_EQ(url, response.url); + EXPECT_EQ(std::string{"text/html"}, response.header["content-type"]); + EXPECT_EQ(200, response.status_code); + EXPECT_EQ(ErrorCode::OK, response.error.code) << response.error.message; +} +#endif + +fs::path GetBasePath(const std::string& execPath) { + return fs::path(fs::path{execPath}.parent_path().string() + "/").make_preferred(); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + fs::path baseDirPath = fs::path{GetBasePath(argv[0]).string() + "data/"}; + fs::path serverCertPath = fs::path{baseDirPath}.append("certificates/server.crt"); + fs::path serverKeyPath = fs::path{baseDirPath}.append("keys/server.key"); + server = new HttpsServer(std::move(baseDirPath), std::move(serverCertPath), std::move(serverKeyPath)); + ::testing::AddGlobalTestEnvironment(server); + + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/structures_tests.cpp b/3rdparty/cpr/test/structures_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..552fd1af1c367f19d51c8c43628ec85a6264539f --- /dev/null +++ b/3rdparty/cpr/test/structures_tests.cpp @@ -0,0 +1,62 @@ +#include "cpr/cprtypes.h" +#include + +#include + +#include "cpr/parameters.h" +#include "cpr/payload.h" + +using namespace cpr; + +TEST(PayloadTests, UseStringVariableTest) { + std::string value1 = "hello"; + std::string key2 = "key2"; + Payload payload{{"key1", value1}, {key2, "world"}}; + + std::string expected = "key1=hello&key2=world"; + EXPECT_EQ(payload.GetContent(CurlHolder()), expected); +} + +TEST(PayloadTests, DisableEncodingTest) { + std::string key1 = "key1"; + std::string key2 = "key2§$%&/"; + std::string value1 = "hello.,.,"; + std::string value2 = "hello"; + Payload payload{{key1, value1}, {key2, value2}}; + payload.encode = false; + + std::string expected = key1 + '=' + value1 + '&' + key2 + '=' + value2; + EXPECT_EQ(payload.GetContent(CurlHolder()), expected); +} + +TEST(ParametersTests, UseStringVariableTest) { + std::string value1 = "hello"; + std::string key2 = "key2"; + Parameters parameters{{"key1", value1}, {key2, "world"}}; + + std::string expected = "key1=hello&key2=world"; + EXPECT_EQ(parameters.GetContent(CurlHolder()), expected); +} + +TEST(ParametersTests, DisableEncodingTest) { + std::string key1 = "key1"; + std::string key2 = "key2§$%&/"; + std::string value1 = "hello.,.,"; + std::string value2 = "hello"; + Parameters parameters{{key1, value1}, {key2, value2}}; + parameters.encode = false; + + std::string expected = key1 + '=' + value1 + '&' + key2 + '=' + value2; + EXPECT_EQ(parameters.GetContent(CurlHolder()), expected); +} + +TEST(UrlToAndFromString, UrlTests) { + std::string s{"https://github.com/whoshuu/cpr"}; + cpr::Url url = s; + EXPECT_EQ(s, url.str()); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/threadpool_tests.cpp b/3rdparty/cpr/test/threadpool_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..66b42c1d51ea96426daebd05ae54f65980bc033c --- /dev/null +++ b/3rdparty/cpr/test/threadpool_tests.cpp @@ -0,0 +1,106 @@ +#include +#include +#include + + +#include "cpr/threadpool.h" + +TEST(ThreadPoolTests, DISABLED_BasicWorkOneThread) { + std::atomic_uint32_t invCount{0}; + uint32_t invCountExpected{100}; + + { + cpr::ThreadPool tp; + tp.SetMinThreadNum(1); + tp.SetMaxThreadNum(1); + tp.Start(0); + + for (size_t i = 0; i < invCountExpected; ++i) { + tp.Submit([&invCount]() -> void { invCount++; }); + } + + // Wait for the thread pool to finish its work + tp.Wait(); + } + + EXPECT_EQ(invCount, invCountExpected); +} + +TEST(ThreadPoolTests, DISABLED_BasicWorkMultipleThreads) { + std::atomic_uint32_t invCount{0}; + uint32_t invCountExpected{100}; + + { + cpr::ThreadPool tp; + tp.SetMinThreadNum(1); + tp.SetMaxThreadNum(10); + tp.Start(0); + + for (size_t i = 0; i < invCountExpected; ++i) { + tp.Submit([&invCount]() -> void { invCount++; }); + } + + // Wait for the thread pool to finish its work + tp.Wait(); + } + + EXPECT_EQ(invCount, invCountExpected); +} + +TEST(ThreadPoolTests, DISABLED_PauseResumeSingleThread) { + std::atomic_uint32_t invCount{0}; + + uint32_t repCount{100}; + uint32_t invBunchSize{20}; + + cpr::ThreadPool tp; + tp.SetMinThreadNum(1); + tp.SetMaxThreadNum(10); + tp.Start(0); + + for (size_t i = 0; i < repCount; ++i) { + tp.Pause(); + EXPECT_EQ(invCount, i * invBunchSize); + + for (size_t e = 0; e < invBunchSize; ++e) { + tp.Submit([&invCount]() -> void { invCount++; }); + } + tp.Resume(); + // Wait for the thread pool to finish its work + tp.Wait(); + + EXPECT_EQ(invCount, (i + 1) * invBunchSize); + } +} + +TEST(ThreadPoolTests, DISABLED_PauseResumeMultipleThreads) { + std::atomic_uint32_t invCount{0}; + + uint32_t repCount{100}; + uint32_t invBunchSize{20}; + + cpr::ThreadPool tp; + tp.SetMinThreadNum(1); + tp.SetMaxThreadNum(10); + tp.Start(0); + + for (size_t i = 0; i < repCount; ++i) { + tp.Pause(); + EXPECT_EQ(invCount, i * invBunchSize); + + for (size_t e = 0; e < invBunchSize; ++e) { + tp.Submit([&invCount]() -> void { invCount++; }); + } + tp.Resume(); + // Wait for the thread pool to finish its work + tp.Wait(); + + EXPECT_EQ(invCount, (i + 1) * invBunchSize); + } +} + + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/util_tests.cpp b/3rdparty/cpr/test/util_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..24787fcb5d889b75a67cf3a9ec3c5ea3de968716 --- /dev/null +++ b/3rdparty/cpr/test/util_tests.cpp @@ -0,0 +1,237 @@ +#include + +#include + +#include "cpr/cprtypes.h" +#include "cpr/util.h" + +using namespace cpr; + +TEST(UtilParseCookiesTests, BasicParseTest) { + Cookies expectedCookies{{Cookie("status", "on", "127.0.0.1", false, "/", false, std::chrono::system_clock::from_time_t(1656908640)), Cookie("name", "debug", "127.0.0.1", false, "/", false, std::chrono::system_clock::from_time_t(0))}}; + curl_slist* raw_cookies = new curl_slist{ + (char*) "127.0.0.1\tFALSE\t/\tFALSE\t1656908640\tstatus\ton", + new curl_slist{ + (char*) "127.0.0.1\tFALSE\t/\tFALSE\t0\tname\tdebug", + nullptr, + }, + }; + Cookies cookies = util::parseCookies(raw_cookies); + for (auto cookie = cookies.begin(), expectedCookie = expectedCookies.begin(); cookie != cookies.end() && expectedCookie != expectedCookies.end(); cookie++, expectedCookie++) { + EXPECT_EQ(expectedCookie->GetName(), cookie->GetName()); + EXPECT_EQ(expectedCookie->GetValue(), cookie->GetValue()); + EXPECT_EQ(expectedCookie->GetDomain(), cookie->GetDomain()); + EXPECT_EQ(expectedCookie->IsIncludingSubdomains(), cookie->IsIncludingSubdomains()); + EXPECT_EQ(expectedCookie->GetPath(), cookie->GetPath()); + EXPECT_EQ(expectedCookie->IsHttpsOnly(), cookie->IsHttpsOnly()); + EXPECT_EQ(expectedCookie->GetExpires(), cookie->GetExpires()); + } + delete raw_cookies->next; + delete raw_cookies; +} + +TEST(UtilParseHeaderTests, BasicParseTest) { + std::string header_string{ + "HTTP/1.1 200 OK\r\n" + "Server: nginx\r\n" + "Date: Sun, 05 Mar 2017 00:34:54 GMT\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 351\r\n" + "Connection: keep-alive\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Credentials: true\r\n" + "\r\n"}; + Header header = util::parseHeader(header_string); + EXPECT_EQ(std::string{"nginx"}, header["Server"]); + EXPECT_EQ(std::string{"Sun, 05 Mar 2017 00:34:54 GMT"}, header["Date"]); + EXPECT_EQ(std::string{"application/json"}, header["Content-Type"]); + EXPECT_EQ(std::string{"351"}, header["Content-Length"]); + EXPECT_EQ(std::string{"keep-alive"}, header["Connection"]); + EXPECT_EQ(std::string{"*"}, header["Access-Control-Allow-Origin"]); + EXPECT_EQ(std::string{"true"}, header["Access-Control-Allow-Credentials"]); +} + +TEST(UtilParseHeaderTests, NewlineTest) { + std::string header_string{ + "HTTP/1.1 200 OK\r\n" + "Auth:\n" + "Access-Control-Allow-Credentials: true\r\n" + "\r\n"}; + Header header = util::parseHeader(header_string); + EXPECT_EQ(std::string{""}, header["Server"]); + EXPECT_EQ(std::string{"true"}, header["Access-Control-Allow-Credentials"]); +} + +TEST(UtilParseHeaderTests, SpaceNewlineTest) { + std::string header_string{ + "HTTP/1.1 200 OK\r\n" + "Auth: \n" + "Access-Control-Allow-Credentials: true\r\n" + "\r\n"}; + Header header = util::parseHeader(header_string); + EXPECT_EQ(std::string{""}, header["Server"]); + EXPECT_EQ(std::string{"true"}, header["Access-Control-Allow-Credentials"]); +} + +TEST(UtilParseHeaderTests, CarriageReturnNewlineTest) { + std::string header_string{ + "HTTP/1.1 200 OK\n" + "Auth:\r\n" + "Access-Control-Allow-Credentials: true\r\n" + "\r\n"}; + Header header = util::parseHeader(header_string); + EXPECT_EQ(std::string{""}, header["Server"]); + EXPECT_EQ(std::string{"true"}, header["Access-Control-Allow-Credentials"]); +} + +TEST(UtilParseHeaderTests, SpaceCarriageReturnNewlineTest) { + std::string header_string{ + "HTTP/1.1 200 OK\n" + "Auth: \r\n" + "Access-Control-Allow-Credentials: true\r\n" + "\r\n"}; + Header header = util::parseHeader(header_string); + EXPECT_EQ(std::string{""}, header["Server"]); + EXPECT_EQ(std::string{"true"}, header["Access-Control-Allow-Credentials"]); +} + +TEST(UtilParseHeaderTests, BasicStatusLineTest) { + std::string header_string{ + "HTTP/1.1 200 OK\r\n" + "Server: nginx\r\n" + "Content-Type: application/json\r\n" + "\r\n"}; + std::string status_line; + std::string reason; + Header header = util::parseHeader(header_string, &status_line, &reason); + EXPECT_EQ(std::string{"HTTP/1.1 200 OK"}, status_line); + EXPECT_EQ(std::string{"OK"}, reason); + EXPECT_EQ(std::string{"nginx"}, header["Server"]); + EXPECT_EQ(std::string{"application/json"}, header["Content-Type"]); +} + +TEST(UtilParseHeaderTests, NewlineStatusLineTest) { + std::string header_string{ + "HTTP/1.1 407 Proxy Authentication Required\n" + "Server: nginx\r\n" + "Content-Type: application/json\r\n" + "\r\n"}; + std::string status_line; + std::string reason; + Header header = util::parseHeader(header_string, &status_line, &reason); + EXPECT_EQ(std::string{"HTTP/1.1 407 Proxy Authentication Required"}, status_line); + EXPECT_EQ(std::string{"Proxy Authentication Required"}, reason); + EXPECT_EQ(std::string{"nginx"}, header["Server"]); + EXPECT_EQ(std::string{"application/json"}, header["Content-Type"]); +} + +TEST(UtilParseHeaderTests, NoReasonSpaceTest) { + std::string header_string{ + "HTTP/1.1 200 \n" + "Server: nginx\r\n" + "Content-Type: application/json\r\n" + "\r\n"}; + std::string status_line; + std::string reason; + Header header = util::parseHeader(header_string, &status_line, &reason); + EXPECT_EQ(std::string{"HTTP/1.1 200"}, status_line); + EXPECT_EQ(std::string{""}, reason); + EXPECT_EQ(std::string{"nginx"}, header["Server"]); + EXPECT_EQ(std::string{"application/json"}, header["Content-Type"]); +} + +TEST(UtilParseHeaderTests, NoReasonTest) { + std::string header_string{ + "HTTP/1.1 200\n" + "Server: nginx\r\n" + "Content-Type: application/json\r\n" + "\r\n"}; + std::string status_line; + std::string reason; + Header header = util::parseHeader(header_string, &status_line, &reason); + EXPECT_EQ(std::string{"HTTP/1.1 200"}, status_line); + EXPECT_EQ(std::string{""}, reason); + EXPECT_EQ(std::string{"nginx"}, header["Server"]); + EXPECT_EQ(std::string{"application/json"}, header["Content-Type"]); +} + +TEST(UtilUrlEncodeTests, UnicodeEncoderTest) { + std::string input = "一二三"; + std::string result = util::urlEncode(input); + std::string expected = "%E4%B8%80%E4%BA%8C%E4%B8%89"; + EXPECT_EQ(result, expected); +} + +TEST(UtilUrlEncodeTests, AsciiEncoderTest) { + std::string input = "Hello World!"; + std::string result = util::urlEncode(input); + std::string expected = "Hello%20World%21"; + EXPECT_EQ(result, expected); +} + +TEST(UtilUrlDecodeTests, UnicodeDecoderTest) { + std::string input = "%E4%B8%80%E4%BA%8C%E4%B8%89"; + std::string result = util::urlDecode(input); + std::string expected = "一二三"; + EXPECT_EQ(result, expected); +} + +TEST(UtilUrlDecodeTests, AsciiDecoderTest) { + std::string input = "Hello%20World%21"; + std::string result = util::urlDecode(input); + std::string expected = "Hello World!"; + EXPECT_EQ(result, expected); +} + +TEST(UtilSecureStringClearTests, EmptyStringTest) { + std::string input; + util::secureStringClear(input); + EXPECT_TRUE(input.empty()); +} + +TEST(UtilSecureStringClearTests, NotEmptyStringTest) { + std::string input = "Hello World!"; + util::secureStringClear(input); + EXPECT_TRUE(input.empty()); +} + +TEST(UtilIsTrueTests, TrueTest) { + { + std::string input = "TRUE"; + bool output = util::isTrue(input); + EXPECT_TRUE(output); + } + { + std::string input = "True"; + bool output = util::isTrue(input); + EXPECT_TRUE(output); + } + { + std::string input = "true"; + bool output = util::isTrue(input); + EXPECT_TRUE(output); + } +} + +TEST(UtilIsTrueTests, FalseTest) { + { + std::string input = "FALSE"; + bool output = util::isTrue(input); + EXPECT_FALSE(output); + } + { + std::string input = "False"; + bool output = util::isTrue(input); + EXPECT_FALSE(output); + } + { + std::string input = "false"; + bool output = util::isTrue(input); + EXPECT_FALSE(output); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/cpr/test/version_tests.cpp b/3rdparty/cpr/test/version_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5bf7ed0036dd8457277dc3f2bc700298a79b318c --- /dev/null +++ b/3rdparty/cpr/test/version_tests.cpp @@ -0,0 +1,65 @@ +#include "cpr/cpr.h" +#include +#include +#include +#include + + +TEST(VersionTests, StringVersionExists) { +#ifndef CPR_VERSION + EXPECT_TRUE(false); +#endif // CPR_VERSION +} + +TEST(VersionTests, StringVersionValid) { + EXPECT_TRUE(CPR_VERSION != nullptr); + std::string version = CPR_VERSION; + + // Check if the version string is: '\d+\.\d+\.\d+' + bool digit = true; + size_t dotCount = 0; + for (size_t i = 0; i < version.size(); i++) { + if (i == 0) { + EXPECT_TRUE(std::isdigit(version[i])); + } else if (digit) { + if (version[i] == '.') { + digit = false; + dotCount++; + continue; + } + } + EXPECT_TRUE(std::isdigit(version[i])); + digit = true; + } + EXPECT_EQ(dotCount, 2); +} + +TEST(VersionTests, VersionMajorExists) { +#ifndef CPR_VERSION_MAJOR + EXPECT_TRUE(false); +#endif // CPR_VERSION_MAJOR +} + +TEST(VersionTests, VersionMinorExists) { +#ifndef CPR_VERSION_MINOR + EXPECT_TRUE(false); +#endif // CPR_VERSION_MINOR +} + +TEST(VersionTests, VersionPatchExists) { +#ifndef CPR_VERSION_PATCH + EXPECT_TRUE(false); +#endif // CPR_VERSION_PATCH +} + +TEST(VersionTests, VersionNumExists) { +#ifndef CPR_VERSION_NUM + EXPECT_TRUE(false); +#endif // CPR_VERSION_NUM +} + + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/3rdparty/s7/s7.c b/3rdparty/s7/s7.c index 1e69a82efcce54bb4f584054e26f9933d28a229f..31330c96df30a2bb71e6438982f6f955167724b0 100644 --- a/3rdparty/s7/s7.c +++ b/3rdparty/s7/s7.c @@ -308,7 +308,6 @@ #include #include #include - #include #else /* in Snd these are in mus-config.h */ #ifndef MUS_CONFIG_H_LOADED @@ -376,6 +375,7 @@ #include #include #include +#include #ifdef _MSC_VER #define MS_WINDOWS 1 @@ -1517,6 +1517,13 @@ static s7_pointer wrap_string(s7_scheme *sc, const char *str, s7_int len); static s7_pointer set_elist_1(s7_scheme *sc, s7_pointer x1); +static inline FILE *xfopen(const char *path, const char *mode) +{ + setlocale(LC_ALL, ".UTF8"); + return fopen(path, mode); +} +#define fopen(path, mode) xfopen(path, mode) + #if DISABLE_FILE_OUTPUT static FILE *old_fopen(const char *pathname, const char *mode) {return(fopen(pathname, mode));} diff --git a/bench/either.scm b/bench/either.scm index d46583b92c504e6bb8d3b8a2445da87bdb346d44..9884b889dfd3181beb7082478b5eedc9505d64ab 100644 --- a/bench/either.scm +++ b/bench/either.scm @@ -15,7 +15,7 @@ ; (import (liii timeit) - (liii either) + (liii rich-either) (liii lang) (liii base)) diff --git a/devel/200_1.md b/devel/200_1.md index 29116e669cf7dcfefbccceebd790121a58a07e87..bd9903393fa2a3e892e5b5cd9f746014fcf9ac57 100644 --- a/devel/200_1.md +++ b/devel/200_1.md @@ -1,5 +1,29 @@ # 200_1 +## 2026/1/19 Goldfish Scheme v17.11.24 + +### 📦 模块开源 / 重构 +- `(liii json)` 开源并重构:不依赖 `define-class`,原`json`改名为 `rich-json`,新增对应测试 +- `(liii either)` 开源并重构:实现方式调整,原`either`改名为 `rich-either`,新增对应测试 + +### 🧩 标准库 / SRFI +- 原 OO 版 `(liii set)` 改名为 `(liii rich-set)`;新增 SRFI-113 的 `(liii set)` 封装并补齐接口与测试 +- SRFI-113:完整实现 set/bag(构造、查询、变换、集合运算、可变版本),新增 `(liii bag)` 与测试 +- `hash-table` 增加 `hash-table-fold` +- SRFI-13:补齐 `string-index-right`、`string-skip`、`string-skip-right` 并扩展测试 +- SRFI-267:实现 raw string 字面量,新增 `(liii raw-string)` 与 `deindent` 宏及测试 + +### 🌐 网络与依赖 +- `(liii http)` 迁移自 liiischeme,基于 CPR;规范化响应 header 为小写并修复测试不稳定 +- 引入 `3rdparty/cpr` 与 xmake 包描述,构建语言提升到 C++17 + +### 🧰 构建 / CI / 跨平台 +- Windows:S7 `fopen` 支持 UTF-8 路径;修复 xmake 构建失败(`goldfish_repl_wasm` 依赖/平台限制) +- CI:xmake 版本更新到 v2.9.9,新增 deb 自动打包流程 +- 修正 goldfish target 安装目录 `goldfish/guenchi` + + + ## 2025/12/24 Goldfish Scheme v17.11.23 ### 🔧 基础设施改进 diff --git a/devel/200_17.md b/devel/200_17.md new file mode 100644 index 0000000000000000000000000000000000000000..f13348e23985fcfe15474ff810cc0b93071bacc4 --- /dev/null +++ b/devel/200_17.md @@ -0,0 +1,42 @@ +# [200_17] 修复 S7 中的 Windows 编码问题 + +## 任务相关的代码文件 +- `3rdparty/s7/s7.c` + +## 如何测试 + +```scheme +(with-output-to-file "中文.txt" (lambda () (display "ok"))) +``` + +在 Windows 上执行以上 goldfish 代码,应当正确创建名为 `中文.txt` 的文件。 +在其他平台上也是如此,没有变化。 + +## 使用 `setlocale .UTF8` 来让 fopen 正确处理编码 + +### What + +使用 `setlocale .UTF8` 来让 fopen 正确处理编码 + +### Why + +修复 S7 中 fopen 的 Windows 编码问题。 + +### How + +重定义 `fopen`,在新函数中调用 `setlocale(LC_ALL, ".UTF8")` + +https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setlocale-wsetlocale?view=msvc-170#utf-8-support + +#### Why `setlocale`? + +简洁性。此外,这种方法已经在 goldfish repl 中得到应用,因此不会带来任何额外的负担。 + +#### Why not tbox? + +`port` 不是公共的 S7 类型(i.e. 没有构造函数)。S7 的实现涉及锁和状态控制。在 glue 层 (`goldfish.hpp`) 中使用 `tbox` 单独实现 `open-output-file` 函数会比目前的实现更加复杂。这种复杂性源于需要手动构造 `port`,并在需要时关闭和释放 tbox 文件和文件锁(需要确定何时执行这些操作)。 + +#### Why not `_wfopen`? + +函数 `_wfopen` 需要包含头文件 `windows.h`,而该头文件会引入大量符号,因此需要进行的修改会非常广泛。这样做会使代码的复杂性高于目前的实现方式。 + diff --git a/devel/200_18.md b/devel/200_18.md new file mode 100644 index 0000000000000000000000000000000000000000..df570d592bf672d5a467af44a343e45386a2ee71 --- /dev/null +++ b/devel/200_18.md @@ -0,0 +1,42 @@ +# [200_18] Goldfish CI 自动打包 + +## 任务相关的代码文件 +- `xmake.lua` +- `.github/workflows/package.yml` + +## 如何测试 + +检查 GitHub action 仪表盘,能够看到执行成功,且 artifact 被上传。 + +## 2025-10-27 xmake target 安装目录 goldfish/guenchi + +### What + +xmake target 安装目录 goldfish/guenchi + +### Why + +goldfish target 缺少 `goldfish/guenchi` 目录的安装。 + +### How + +在 xmake.lua 中为 goldfish target 添加 `goldfish/guenchi` 目录的安装。 + +## 2025-10-23 新增 package.yml workflow + +### What + +实现 deb 的自动打包。 + +TODO: +1. 由于 xpack 的 add_targets 并不受 xmake config 设置的值影响,使用 sed CI 时替换,若有解决方案在此编号下继续更新。 +2. rpm(srpm) 没有被成功打包,由于一些环境问题,rpmbuild 无法找到依赖项目。后续继续在 `package.yml` 修复即可。 + +### Why + +方便内部使用,[dogfooding](https://en.wikipedia.org/wiki/Eating_your_own_dog_food)。 + +### How + +通过 xmake 的 xpack 描述打包,在 GitHub Action,每次 main 分支被 push 时候触发。 + diff --git a/devel/210_10.md b/devel/210_10.md new file mode 100644 index 0000000000000000000000000000000000000000..0b5979292846d78ac333f5d5ab792b6c0f913ded --- /dev/null +++ b/devel/210_10.md @@ -0,0 +1,79 @@ +# [210_10] 实现 bag 基础能力(SRFI-113) + +## 任务相关的代码文件 +- goldfish/srfi/srfi-113.scm +- goldfish/liii/bag.scm +- tests/goldfish/liii/bag-test.scm + +## 如何测试 +```bash +bin/goldfish tests/goldfish/liii/bag-test.scm +``` + + +## 2026/01/30 实现 bag 更新/子集/集合运算接口 +### What +补齐 SRFI-113 的 bag 更新、删除、搜索、子集关系与集合运算接口,并在 (liii bag) 中导出与补齐测试。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 bag-adjoin / bag-adjoin! / bag-replace / bag-replace! / bag-delete / bag-delete! / bag-delete-all / bag-delete-all! / bag-search!,以及 bag=? / bag? / bag<=? / bag>=? 与 bag-union / bag-intersection / bag-difference / bag-xor 及其线性更新版本,并添加到导出列表 +2. 在 goldfish/liii/bag.scm 中导出以上函数 +3. 在 tests/goldfish/liii/bag-test.scm 中添加详细注释与覆盖测试 + +### Why +提供多重集的增删改、搜索、包含关系与集合运算能力,覆盖 SRFI-113 对 bag 的核心语义。 + +### How +1. bag-adjoin!/bag-adjoin 对元素逐个计数 +1,前者就地修改,后者基于副本 +2. bag-replace!/bag-replace 替换等价元素的代表值并保持计数不变 +3. bag-delete!/bag-delete 逐个删除一个元素实例,前者就地修改,后者基于副本 +4. bag-delete-all!/bag-delete-all 根据列表删除元素实例,列表含重复则多次删除 +5. bag-search! 按 comparator 等价查找,命中/未命中分别调用 success/failure 并提供 insert/update/remove 操作 +6. 子集关系按“计数包含”判断:每个元素计数满足 <= 或严格 < 关系 +7. 相等性要求各元素计数一致 +8. union 取计数最大值;intersection 取最小值;difference 按计数相减并截断为 0;xor 取计数绝对差 + +## 2026/01/29 实现 bag 查询与转换接口 +### What +补齐 SRFI-113 的 bag 查询能力与复制/列表转换接口,并在 (liii bag) 中导出与补齐测试。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 bag-size / bag-find / bag-count / bag-any? / bag-every? / bag-copy / list->bag / list->bag! 并添加到导出列表 +2. 在 goldfish/liii/bag.scm 中导出以上函数,并提供默认比较器的 list->bag +3. 在 tests/goldfish/liii/bag-test.scm 中添加注释与覆盖测试(含空 bag、空列表与类型错误分支) + +### Why +提供多重集的基础查询能力与列表互操作接口,支持按条件计数与搜索,并符合 SRFI-113 语义。 + +### How +1. bag-size 汇总 entries 的计数,返回元素总数(含重复) +2. bag-find 遍历 entries,找到满足 predicate 的元素立即返回,否则调用 failure +3. bag-count 统计满足 predicate 的元素出现次数(含重复) +4. bag-any? / bag-every? 使用短路遍历判断是否存在/是否全部满足 +5. bag-copy 复制 entries 的计数并保持 comparator +6. list->bag 使用 comparator 构造 bag,保留重复元素 +7. list->bag! 将列表元素累加到现有 bag +8. 关键路径与类型错误通过测试覆盖 + +## 2026/01/29 实现 bag 构造器、访问器与谓词 +### What +实现 SRFI-113 的 bag 基础构造、访问与谓词函数,并在 (liii bag) 中提供默认比较器封装与测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 bag / bag-unfold / bag-member / bag-element-comparator / bag->list,并添加到导出列表 +2. 在 goldfish/liii/bag.scm 中提供默认比较器版本的 bag,并导出上述函数 +3. 在 tests/goldfish/liii/bag-test.scm 中添加文档注释与覆盖测试(含错误分支) +4. 在 goldfish/srfi/srfi-113.scm 中实现 bag? / bag-contains? / bag-empty? / bag-disjoint? 并添加到导出列表 +5. 在 goldfish/liii/bag.scm 中导出以上谓词 +6. 在 tests/goldfish/liii/bag-test.scm 中添加注释与测试(含类型错误分支) + +### Why +提供多重集(bag)的最小可用接口,用于存储重复元素并支持基础查询与遍历输出。 + +### How +1. bag 使用 comparator 构造,插入时按 comparator 相等性累加计数 +2. bag-unfold 使用 unfold 模式生成 bag +3. bag-member 返回 bag 中等价元素,未命中返回默认值 +4. bag->list 将元素按计数展开为列表 +5. bag? 使用 record-type 生成的谓词 +6. bag-contains? 使用 comparator 相等性在线性扫描 entries +7. bag-empty? 检查 entries 是否为空 +8. bag-disjoint? 逐元素比对,找到相等元素立即返回 #f +9. 关键路径与类型错误通过测试覆盖 diff --git a/devel/210_11.md b/devel/210_11.md new file mode 100644 index 0000000000000000000000000000000000000000..655190a77b12e19888ce009477e15e5ff5cd1e5d --- /dev/null +++ b/devel/210_11.md @@ -0,0 +1,3 @@ +# 210_11 + +## 2026/01/30 为hash-table补上hash-table-fold函数 \ No newline at end of file diff --git a/devel/210_3.md b/devel/210_3.md new file mode 100644 index 0000000000000000000000000000000000000000..84187e7ac292a049c2f34954437ddb96c7428f38 --- /dev/null +++ b/devel/210_3.md @@ -0,0 +1,7 @@ +# 210_3 + +## 2026/1/22 设计一个不依赖于 define-class 的 (liii json) + +## 2026/1/22 将json更改为rich-json + +## 2026/1/19 开源json代码 \ No newline at end of file diff --git a/devel/210_4.md b/devel/210_4.md new file mode 100644 index 0000000000000000000000000000000000000000..b64b1f923968f0e96875efc4de3f8a549b5d2b65 --- /dev/null +++ b/devel/210_4.md @@ -0,0 +1,3 @@ +# 210_4 + +## 2026/1/19 either重命名为 rich-either \ No newline at end of file diff --git a/devel/210_5.md b/devel/210_5.md new file mode 100644 index 0000000000000000000000000000000000000000..34e29cc6f99d154b6a12617764ce64177a9fa678 --- /dev/null +++ b/devel/210_5.md @@ -0,0 +1,5 @@ +# 210_5 + +## 2026/1/20 修改either的实现方法 + +## 2026/1/19 开源either相关的代码 \ No newline at end of file diff --git a/devel/210_6.md b/devel/210_6.md new file mode 100644 index 0000000000000000000000000000000000000000..e906b6074b8d41dc97ebe19941729e3a476f92b7 --- /dev/null +++ b/devel/210_6.md @@ -0,0 +1,56 @@ +# [210_6] (liii http) + +## 任务相关的代码文件 +- `src/goldfish.hpp` +- `tests/goldfish/liii/http-test.scm` +- `goldfish/liii/http.scm` +- `xmake.lua` +- `xmake/packages/c/cpr/` +- `3rdparty/cpr/` + +## 如何测试 + +```shell +# 可能需要清除缓存 +# rm .xmake/ build/ -r +xmake f -vyD +xmake b goldfish +./bin/goldfish tests/goldfish/liii/http-test.scm +``` + +## 2026/1/22 正规化 HTTP Header 的大小写 + +### What + +1. 在 `response2hashtable()` 中将 HTTP 头部键统一转为小写 +2. 修改测试用例,放宽对 `reason` 字段的检查(接受 "OK" 或空字符串) +3. 更新测试中的头部键引用为小写形式(如 "content-type" 替代 "Content-Type") + +### Why + +1. httpbin.org 使用负载均衡,导致: + - HTTP 头部键大小写不一致(Content-Type vs content-type) + - reason 字段有时为空(HTTP/2+ 规范允许省略 reason phrase) +2. 测试不稳定,取决于请求被路由到哪个后端实例 +3. HTTP 规范规定头部键名不区分大小写,但实际实现有差异 + +### How + +在 `goldfish.hpp` 标准化头部键名为小写。 + +## 2026/1/22 开源 `(liii http)` + +### What + +1. 将原 liiischeme 的 `(liii http)` 开源,迁移至 goldfish 代码库。 +2. 更新了一下测试文件,其中 header 是大小写敏感的,原测试使用小写的,没有取得预期的值。 +3. 将 `CRLF` 重规范为 `LF`。 +4. 由于依赖库 CPR 的需要,将 goldfish 语言从 C++ 11 提高至 C++ 17。 + +### Why + +移入 goldfish 主线开发。 + +### How + +简单迁移。 diff --git a/devel/210_7.md b/devel/210_7.md new file mode 100644 index 0000000000000000000000000000000000000000..0755d933d2b98f02e888812087a72de17fadc657 --- /dev/null +++ b/devel/210_7.md @@ -0,0 +1,362 @@ +# [210_7] 实现set + +## 任务相关的代码文件 +- goldfish/liii/set.scm +- goldfish/srfi/srfi-113.scm +- tests/goldfish/liii/set-test.scm + +## 如何测试 +```bash +bin/goldfish tests/goldfish/liii/set-test.scm +bin/lint goldfish/liii/set.scm +bin/lint goldfish/srfi/srfi-113.scm +bin/lint tests/goldfish/liii/set-test.scm +``` + +## 2026/01/28 实现 set-union! / set-intersection! / set-difference! / set-xor! 函数 +### What +在 (liii set) 模块中实现 set-union!、set-intersection!、set-difference!、set-xor! 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现以上函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加以上函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加文档注释和测试用例 + +### Why +这些函数提供集合运算的可变版本,便于在原 set 上进行并集、交集、差集与对称差集计算,减少分配并保持元素来源规则。 + +### How +1. set-union! 按顺序并入集合,仅在 set1 中不存在时加入 +2. set-intersection! 保留同时出现在所有集合中的元素 +3. set-difference! 移除出现在其他集合中的元素 +4. set-xor! 移除交集元素,并加入只存在于 set2 的元素 +5. 在测试中覆盖多集合、元素来源与类型/比较器错误场景 + +## 2026/01/28 实现 set-union / set-intersection / set-difference / set-xor 函数 +### What +在 (liii set) 模块中实现 set-union、set-intersection、set-difference、set-xor 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现以上函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加以上函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加文档注释和测试用例 + +### Why +这些集合运算用于组合、筛选或比较多个 set,是 set 操作的核心能力。结果集合应保留“首次出现”的元素来源,并遵循比较器等价规则。 + +### How +1. set-union 按顺序合并集合,仅在结果中不存在时才加入,确保元素来源正确 +2. set-intersection 只保留同时出现在所有集合中的元素(来自第一个集合) +3. set-difference 返回第一个集合与其余集合的非对称差集 +4. set-xor 返回两个集合的对称差集 +5. 在测试中覆盖多集合、比较器一致性与类型错误场景 + +## 2026/01/28 实现 set->list 与 list->set! 函数 +### What +在 (liii set) 模块中实现 set->list 与 list->set! 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set->list 与 list->set! 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set->list 与 list->set! 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set->list 与 list->set! 的文档注释和测试用例 + +### Why +set->list 便于将 set 转换为列表进行遍历或序列化;list->set! 用于将列表元素并入已有 set,重复元素会被去重。 + +### How +1. set->list 直接返回 set 的元素列表(顺序未指定) +2. list->set! 线性遍历列表并将元素加入 set +3. 在测试中覆盖空列表、比较器一致性与类型错误场景 + +## 2026/01/28 实现 set-partition 与 set-partition! 函数 +### What +在 (liii set) 模块中实现 set-partition 与 set-partition! 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-partition 与 set-partition! 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-partition 与 set-partition! 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-partition 与 set-partition! 的文档注释和测试用例 + +### Why +set-partition 返回两个新 set,分别包含满足与不满足 predicate 的元素;set-partition! 提供可变版本,并返回匹配与不匹配的两个集合。 + +### How +1. set-partition 创建两个与原 set 相同比较器的新 set +2. 遍历原 set 元素,按 predicate 结果分别写入两个新 set +3. set-partition! 线性遍历原 set,将不满足 predicate 的元素收集到新 set,并从原 set 移除 +4. 返回两个值:匹配集合与不匹配集合 + +## 2026/01/28 实现 set-remove 与 set-remove! 函数 +### What +在 (liii set) 模块中实现 set-remove 与 set-remove! 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-remove 与 set-remove! 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-remove 与 set-remove! 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-remove 与 set-remove! 的文档注释和测试用例 + +### Why +set-remove 返回不满足 predicate 的新 set;set-remove! 提供可变版本,线性遍历并原地移除满足 predicate 的元素。 + +### How +1. set-remove 创建与原 set 相同比较器的新 set +2. 遍历原 set 元素,将不满足 predicate 的元素写入新 set +3. set-remove! 线性遍历并移除满足 predicate 的元素 +4. 在测试中覆盖空集合与类型错误场景 + +## 2026/01/28 实现 set-filter 与 set-filter! 函数 +### What +在 (liii set) 模块中实现 set-filter 与 set-filter! 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-filter 与 set-filter! 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-filter 与 set-filter! 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-filter 与 set-filter! 的文档注释和测试用例 + +### Why +set-filter 用于返回满足 predicate 的新 set;set-filter! 提供可变版本,线性遍历并原地筛选元素。 + +### How +1. set-filter 创建与原 set 相同比较器的新 set +2. 遍历原 set 元素,满足 predicate 的元素写入新 set +3. set-filter! 收集不满足 predicate 的元素并从原 set 删除 +4. 在测试中覆盖空集合与类型错误场景 + +## 2026/01/28 实现 set-for-each 与 set-fold 函数 +### What +在 (liii set) 模块中实现 set-for-each 与 set-fold 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-for-each 与 set-fold 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-for-each 与 set-fold 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-for-each 与 set-fold 的文档注释和测试用例 + +### Why +set-for-each 用于对 set 中元素执行副作用操作;set-fold 用于按任意顺序遍历并累积结果,提供与 list fold 类似的聚合能力。 + +### How +1. set-for-each 遍历 set 并对每个元素调用 proc,忽略返回值 +2. set-fold 以 nil 作为初始累积值,对每个元素调用 proc 并更新累积结果 +3. set 为空时,set-fold 返回 nil +4. 在测试中覆盖空集合、累积与类型错误场景 + +## 2026/01/28 实现 set-map 函数 +### What +在 (liii set) 模块中实现 set-map 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-map 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-map 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-map 的文档注释和测试用例 + +### Why +set-map 用于对 set 中每个元素应用映射函数并返回新的 set。映射结果会按照提供的 comparator 去重,适合做类型转换或数据归一化。 + +### How +1. 创建一个使用 comparator 的新 set +2. 遍历原 set 的元素,对每个元素执行 proc +3. 将映射结果加入新 set,自动去重 +4. 返回新 set,并确保原 set 不变 + +## 2026/01/28 实现 set-search! 函数 +### What +在 (liii set) 模块中实现 set-search! 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-search! 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-search! 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-search! 的文档注释和测试用例 + +### Why +set-search! 提供基于 continuation 的可变搜索/更新能力:元素不存在时可选择插入或忽略;元素存在时可选择更新或删除,并返回 set 与 obj 两个值,便于将搜索与更新逻辑合并为一次操作。 + +### How +1. 使用 hash-table-ref/default 在 set 中查找元素,区分命中与未命中路径 +2. 未命中时调用 failure,并提供 insert/ignore continuations +3. 命中时调用 success,并提供 update/remove continuations +4. continuations 内返回 (values set obj),并确保在 update/remove 分支完成 set 的可变更新 +5. 在测试中覆盖 insert/ignore/update/remove 与类型错误场景 + +## 2026/01/27 实现 set-delete 系列函数 +### What +在 (liii set) 模块中实现 set-delete, set-delete!, set-delete-all, set-delete-all! 函数。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现这四个函数并导出。 +2. 在 goldfish/liii/set.scm 中导出这四个函数。 +3. 在 tests/goldfish/liii/set-test.scm 中添加文档注释和测试用例。 + +### Why +这些函数用于从 set 中移除一个或多个元素,包括函数式(不可变)和破坏性(可变)版本,以及接受列表作为参数的版本。 + +### How +- `set-delete`: 复制 set,遍历参数,使用 `hash-table-delete!`。 +- `set-delete!`: 直接在原 set 上遍历参数,使用 `hash-table-delete!`。 +- `set-delete-all`: 同 `set-delete`,但参数为 list。 +- `set-delete-all!`: 同 `set-delete!`,但参数为 list。 + +## 2026/01/27 实现 set-replace! 函数 +### What +在 (liii set) 模块中实现 set-replace! 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-replace! 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-replace! 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-replace! 的文档注释和测试用例 + +### Why +set-replace! 函数用于替换 set 中已存在的元素(可变操作)。 + +### How +1. 检查 element 是否存在于 set 中。 +2. 如果存在: + - 删除旧元素(hash-table-delete!)。 + - 添加新 element(set-add!)。 +3. 如果不存在,不做任何操作。 +4. 返回 set。 + +## 2026/01/27 实现 set-replace 函数 +### What +在 (liii set) 模块中实现 set-replace 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-replace 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-replace 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-replace 的文档注释和测试用例 + +### Why +set-replace 函数用于替换 set 中已存在的元素。这对于自定义比较器(如大小写不敏感字符串)非常有用,可以更新 set 中存储的实际值。 + +### How +1. 检查 element 是否存在于 set 中。 +2. 如果存在: + - 复制原 set。 + - 删除旧元素(使用 hash-table-delete! 确保 key 被移除)。 + - 添加新 element。 + - 返回新 set。 +3. 如果不存在,直接返回原 set。 + +## 2026/01/27 实现 set-adjoin! 函数 +### What +在 (liii set) 模块中实现 set-adjoin! 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-adjoin! 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-adjoin! 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-adjoin! 的文档注释和测试用例 + +### Why +set-adjoin! 函数用于向 set 中添加一个或多个元素,并返回修改后的 set。这是 set 的破坏性更新操作,效率高于 functional update。 + +### How +1. 遍历参数列表中的元素。 +2. 使用 `set-add!` 将每个元素直接添加到原 set 中。 +3. 返回原 set。 + +## 2026/01/27 实现 set-adjoin 函数 +### What +在 (liii set) 模块中实现 set-adjoin 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-adjoin 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-adjoin 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-adjoin 的文档注释和测试用例 + +### Why +set-adjoin 函数用于向 set 中添加一个或多个元素,并返回一个新的 set,原 set 不变。这是 set 的函数式更新操作。 + +### How +1. 使用 `set-copy` 创建原 set 的副本。 +2. 使用 `set-add!` 将新元素添加到副本中。 +3. 返回副本。 + +## 2026/01/27 实现 set-member 函数 +### What +在 (liii set) 模块中实现 set-member 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-member 函数并添加到导出列表 +2. 修改 goldfish/srfi/srfi-113.scm 中的 set-add! 函数,使其在哈希表中存储元素本身作为值,而不是固定值 1 +3. 在 goldfish/liii/set.scm 的导出列表中添加 set-member 函数 +4. 在 tests/goldfish/liii/set-test.scm 中添加 set-member 的文档注释和测试用例 + +### Why +set-member 函数用于在 set 中查找与指定元素相等的元素(可能不是同一个对象)。如果 set 底层使用哈希表,通过存储元素作为值,可以实现 O(1) 的查找。 + +### How +1. 修改 set-add!:`(hash-table-set! (set-hash-table s) element element)` +2. 实现 set-member:`(hash-table-ref/default (set-hash-table s) element default)` +3. 在测试中使用自定义的 hash 函数以确保 set-contains? 和 set-member 在使用自定义比较器时行为正确。 + +## 2026/01/27 实现 set-count 函数 +### What +在 (liii set) 模块中实现 set-count 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-count 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-count 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-count 的文档注释和测试用例 + +### Why +set-count 函数用于计算 set 中满足谓词的元素个数,这是 set 操作中常用的功能,是对 SRFI-113 的扩展。 + +### How +1. 在 SRFI-113 中实现 set-count 函数,使用 hash-table-for-each 遍历 set 元素并计数 +2. 在 set.scm 中导入并重新导出 set-count 函数 +3. 在测试文件中添加完整的文档注释和多种测试用例 + +## 2026/01/27 实现 set-find 函数 +### What +在 (liii set) 模块中实现 set-find 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-find 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-find 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-find 的文档注释和测试用例 + +### Why +set-find 函数用于在 set 中查找满足谓词的元素,如果找不到则调用 failure 函数,这是 set 操作中常用的功能,是对 SRFI-113 的扩展。 + +### How +1. 在 SRFI-113 中实现 set-find 函数,使用 hash-table-for-each 遍历 set 元素 +2. 使用 call/cc 实现提前返回,当找到满足谓词的元素时立即返回该元素 +3. 如果遍历完所有元素都没有找到,则调用 failure 函数 +4. 在 set.scm 中导入并重新导出 set-find 函数 +5. 在测试文件中添加完整的文档注释和多种测试用例 + +## 2026/01/27 实现 set-every? 函数 +### What +在 (liii set) 模块中实现 set-every? 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-every? 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-every? 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-every? 的文档注释和测试用例 + +### Why +set-every? 函数用于检查 set 中是否所有元素都满足给定的谓词函数,这是 set 操作中常用的功能,是对 SRFI-113 的扩展。 + +### How +1. 在 SRFI-113 中实现 set-every? 函数,使用 hash-table-for-each 遍历 set 元素 +2. 使用 call/cc 实现提前返回,当找到不满足谓词的元素时立即返回 #f +3. 在 set.scm 中导入并重新导出 set-every? 函数 +4. 在测试文件中添加完整的文档注释和多种测试用例 + +## 2026/01/27 实现 set-any? 函数 +### What +在 (liii set) 模块中实现 set-any? 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 中实现 set-any? 函数并添加到导出列表 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-any? 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-any? 的文档注释和测试用例 + +### Why +set-any? 函数用于检查 set 中是否有元素满足给定的谓词函数,这是 set 操作中常用的功能。 + +### How +1. 在 SRFI-113 中实现 set-any? 函数,使用 hash-table-for-each 遍历 set 元素 +2. 使用 call/cc 实现提前返回,当找到满足谓词的元素时立即返回 #t +3. 在 set.scm 中导入并重新导出 set-any? 函数 +4. 在测试文件中添加完整的文档注释和多种测试用例 + +## 2026/01/27 在 (liii set) 中暴露 set-size +### What +在 (liii set) 模块中暴露 set-size 函数,并在测试文件中新增相应的注释和测试用例。 + +1. 在 goldfish/srfi/srfi-113.scm 的导出列表中添加 set-size 函数 +2. 在 goldfish/liii/set.scm 的导出列表中添加 set-size 函数 +3. 在 tests/goldfish/liii/set-test.scm 中添加 set-size 的文档注释和测试用例 + +### Why +set-size 函数已经实现但未暴露给用户使用,需要将其添加到 (liii set) 模块的公共 API 中。 + +### How +1. 首先在底层 SRFI-113 实现中导出 set-size 函数 +2. 然后在 (liii set) 模块中导入并重新导出 set-size +3. 最后在测试文件中添加完整的文档注释和测试用例 + +## 2026/01/26 之前的提交和任务描述 +### 2026/1/26 实现set的谓词和构造器 +### 2026/1/26 将set改为rich-set diff --git a/devel/210_8.md b/devel/210_8.md new file mode 100644 index 0000000000000000000000000000000000000000..605543502cdd65467203eaefcee2e7426a6ef0be --- /dev/null +++ b/devel/210_8.md @@ -0,0 +1,23 @@ +# [210_8] 完整实现 SRFI-13 + +## 任务相关的代码文件 +- goldfish/liii/string.scm +- goldfish/srfi/srfi-13.scm +- tests/goldfish/liii/string-test.scm + +## 如何测试 + +```bash +bin/goldfish tests/goldfish/liii/string-test.scm +``` + +## 2026/01/27 实现 string-index-right 和 `string-skip(-right)` 函数 + +### What + +在 (srfi srfi-13) 模块中实现 `string-index-right`,`string-skip` 和 `string-skip-right` 函数,并在测试文件中新增相应的注释和测试用例。 + +### Why + +完整实现 SRFI-13。 + diff --git a/devel/210_9.md b/devel/210_9.md new file mode 100644 index 0000000000000000000000000000000000000000..5b3f0df0834322df92e367efd5d661ed3817485e --- /dev/null +++ b/devel/210_9.md @@ -0,0 +1,53 @@ +# [210_9] (liii raw-string) + +## 任务相关的代码文件 +- `goldfish/srfi/srfi-267.scm` +- `goldfish/liii/raw-string.scm` +- `tests/goldfish/liii/raw-string-test.scm` + +## 如何测试 + +```shell +./bin/goldfish tests/goldfish/liii/raw-string-test.scm +``` + +## 2026/1/27 实现 SRFI-267,并基于此实现 `(liii raw-string)` + +### What + +实现 SRFI-267 与 `(liii raw-string)`。 + +SRFI-267 提供了一种 raw string 的字面量写法: +``` + ⟨raw string (X)⟩ ⩴ #" X " ⟨raw string internal (X)⟩ " X " +``` + +摘抄示例如下: +``` +#"""" ; → "" +#""a"" ; → "a" +#""\"" ; → "\\" +#"-"""-" ; → "\"" +#"-" " "-" ; → " \" " +#"-"#""a"""-" ; → "#\"\"a\"\"" +#"-"ends with \""-" ; → "ends with \\\" +#""multiline +string"" ; → "multiline\nstring" +#"" + no whitespace stripping"" ; → "\n no whitespace stripping" +#""\(?(\d{3})\D{0,3}(\d{3})\D{0,3}(\d{4})"" + ; → "\\(?(\\d{3})\\D{0,3}(\\d{3})\\D{0,3}(\\d{4})" + ; Example from SRFI 264 +``` + +`(liii raw-string)` 提供了一个宏 `deindent` (alias `&-`),用于 C# 风格的缩进对齐。 + +注:SRFI 还处于 Draft 状态,有可能更改。 + +### Why + +测试与代码实现中有 raw string 字面量的需求。 + +### How + +参考 https://srfi.schemers.org/srfi-267/srfi-267.html 与 https://codeberg.org/avalenn/guile-raw-strings 并重新实现,在代码文件的 Header 中具体描述 Acknowledge。 diff --git a/devel/212_1.md b/devel/212_1.md new file mode 100644 index 0000000000000000000000000000000000000000..eca50951840b24c04bfe870794c0b574a1545cc1 --- /dev/null +++ b/devel/212_1.md @@ -0,0 +1,27 @@ +````markdown +# [212_1] 修复 Windows 平台 xmake 构建失败问题 + +## 如何测试 +在文件夹下面运行xmake命令,能直接构建成功 + +## 2026/1/22 修复 Windows 平台构建失败 +### What +修复了在 Windows 平台上使用 xmake 构建 Goldfish Scheme 时出现的两个问题: + +1. `goldfish_repl_wasm` target 缺少 `argh` 包依赖 +2. `goldfish_repl_wasm` target 在非 WASM 平台上错误地尝试构建 + +### Why +**问题 1:缺少 argh 包依赖** +- `src/goldfish.hpp` 中包含了 `#include ` +- `goldfish_repl_wasm` target 使用了 `goldfish.hpp`,但在 `xmake.lua` 中未声明对 `argh` 包的依赖 +- 导致编译时报错:`fatal error C1083: 无法打开包括文件: "argh.h": No such file or directory` + +**问题 2:平台限制错误** +- `goldfish_repl_wasm` 是专门为 WebAssembly 设计的 target +- 它只导出 C 函数接口(`eval_string`, `get_out`, `get_err`),没有 `main` 函数 +- 在 Windows 等非 WASM 平台上构建时,链接器报错:`LINK : fatal error LNK1561: 必须定义入口点` + +### How +1. 在 `goldfish_repl_wasm` target 的 `add_packages` 中添加 `"argh"` 依赖 +2. 将整个 `goldfish_repl_wasm` target 包裹在 `if is_plat("wasm") then ... end` 条件中,确保只在 WASM 平台上构建 \ No newline at end of file diff --git a/flake.nix b/flake.nix index 6da83e7f9adb257d88b3d0661abca88893fef46e..cf424763eb1857e39d6929cba0436d84218c9d26 100644 --- a/flake.nix +++ b/flake.nix @@ -101,7 +101,18 @@ {italic}🐟 {220}Goldfish{reset} 👾 $(type -p menu &>/dev/null && menu) ''; - packages = with pkgs; [ xmake ]; + imports = [ "${inputs.devshell}/extra/language/c.nix" ]; + language.c.compiler = pkgs.gcc; + language.c.includes = [ pkgs.curl.dev ]; + language.c.libraries = [ ]; + packages = with pkgs; [ + xmake + clang-tools + pkg-config + gnumake + cmake + unzip + ]; }; treefmt.config = { diff --git a/goldfish/guenchi/json.scm b/goldfish/guenchi/json.scm new file mode 100644 index 0000000000000000000000000000000000000000..ed3c4ba657b57d54386bf17d89246a85e7e120a8 --- /dev/null +++ b/goldfish/guenchi/json.scm @@ -0,0 +1,462 @@ +;; MIT License + +; Copyright guenchi (c) 2018 - 2019 +; Da Shen (c) 2024 - 2025 +; (Jack) Yansong Li (c) 2025 + +; Permission is hereby granted, free of charge, to any person obtaining a copy +; of this software and associated documentation files (the "Software"), to deal +; in the Software without restriction, including without limitation the rights +; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +; copies of the Software, and to permit persons to whom the Software is +; furnished to do so, subject to the following conditions: + +; The above copyright notice and this permission notice shall be included in all +; copies or substantial portions of the Software. + +; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +; SOFTWARE. + +(define-library (guenchi json) +(import (liii base) (liii chez) (liii alist) (liii list) (liii string) (liii unicode)) +(export + json-string-escape json-string-unescape string->json json->string + json-ref json-ref* + json-set json-set* json-push json-push* json-drop json-drop* json-reduce json-reduce*) +(begin + +(define (json-string-escape str) + (let ((out (open-output-string))) + (write-char #\" out) ; 起始引号 + (let loop ((i 0)) ; 尾递归遍历 + (if (= i (string-length str)) + (begin + (write-char #\" out) ; 结束引号 + (get-output-string out)) + (let ((c (string-ref str i))) + (case c + ((#\") (display "\\\"" out)) + ((#\\) (display "\\\\" out)) + ((#\/) (display "\\/" out)) + ((#\backspace) (display "\\b" out)) + ((#\xc) (display "\\f" out)) + ((#\newline) (display "\\n" out)) + ((#\return) (display "\\r" out)) + ((#\tab) (display "\\t" out)) + (else (write-char c out))) + (loop (+ i 1))))))) ; 尾递归调用 + + +(define (string-length-sum strings) + (let loop ((o 0) + (rest strings)) + (cond + ((eq? '() rest) o) + (else + (loop (+ o (string-length (car rest))) + (cdr rest)))))) + +(define (fast-string-list-append strings) + (let* ((output-length (string-length-sum strings)) + (output (make-string output-length #\_)) + (fill 0)) + (let outer ((rest strings)) + (cond + ((eq? '() rest) output) + (else + (let* ((s (car rest)) + (n (string-length s))) + (let inner ((i 0)) + (cond ((= i n) 'done) + (else + (string-set! output fill (string-ref s i)) + (set! fill (+ fill 1)) + (inner (+ i 1)))))) + (outer (cdr rest))))))) + +(define (handle-escape-char s end len) + (let ((next-char (if (< (+ end 1) len) + (string-ref s (+ end 1)) + #f))) + (case next-char + ((#\") ; 处理双引号 + (values "\\\"" 2)) + ((#\\) ; 处理反斜杠 + (values "\\\\" 2)) + ((#\/) ; 处理斜杠 + (values "/" 2)) + ((#\b) ; 处理退格符 + (values "\\b" 2)) + ((#\f) ; 处理换页符 + (values "\\f" 2)) + ((#\n) ; 处理换行符 + (values "\\n" 2)) + ((#\r) ; 处理回车符 + (values "\\r" 2)) + ((#\t) ; 处理制表符 + (values "\\t" 2)) + ((#\u) ; 处理 \u 转义字符 + (let ((start-pos (+ end 2)) ; \u 后的起始位置 + (end-pos (+ end 6))) ; \u 后的结束位置 + (if (and (>= start-pos 0) (< end-pos len)) ; 检查索引是否有效 + (let ((hex-str (substring s start-pos end-pos))) ; 提取 4 位十六进制数 + (let ((code-point (string->number hex-str 16))) ; 将十六进制转换为整数 + (when (not code-point) + (error 'parse-error (string-append "Invalid HEX sequence " hex-str))) + ;; 检查是否存在连续的两个 \u + (let ((next-u-pos (+ end 6))) ; 下一个 \u 的起始位置 + (if (and (< (+ next-u-pos 6) len) ; 检查是否足够剩余字符 + (char=? (string-ref s next-u-pos) #\\) + (char=? (string-ref s (+ next-u-pos 1)) #\u)) + ;; 存在连续的两个 \u + (let ((next-hex-str (substring s (+ next-u-pos 2) (+ next-u-pos 6)))) ; 提取下一个 4 位十六进制数 + (let ((next-code-point (string->number next-hex-str 16))) ; 将十六进制转换为整数 + (when (not next-code-point) + (error 'parse-error (string-append "Invalid HEX sequence " next-hex-str))) + ;; 检查是否满足代理对条件 + (if (and (>= code-point #xD800) (<= code-point #xDBFF) ; 高代理 + (>= next-code-point #xDC00) (<= next-code-point #xDFFF)) ; 低代理 + ;; 满足代理对条件,使用 unicode 模块计算码点并转换为字符串 + (let ((surrogate-code-point (+ (* (- code-point #xD800) #x400) + (- next-code-point #xDC00) #x10000))) ; 计算码点 + (values (utf8->string (codepoint->utf8 surrogate-code-point)) 12)) + ;; 不满足代理对条件,仅对第一个 \u 进行转换 + (values (utf8->string (codepoint->utf8 code-point)) 6)))) + ;; 不存在连续的两个 \u,仅对第一个 \u 进行转换 + (values (utf8->string (codepoint->utf8 code-point)) 6))))) + ;; 索引无效,返回原字符 + (error 'parse-error (string-append "HEX sequence too short " (substring s start-pos)))))) + (else + (error 'parse-error (string-append "Invalid escape char: " (string next-char))))))) + +(define string->json + (lambda (s) + (read (open-input-string + (let loop + ((s s) (bgn 0) (end 0) (rst '()) (len (string-length s)) (quts? #f) (lst '(#t))) + (cond + ((= end len) + (fast-string-list-append (reverse rst))) + ((and quts? (char=? (string-ref s end) #\\) (< (+ end 1) len)) + (let-values (((unescaped step) (handle-escape-char s end len))) + (loop s (+ end step) (+ end step) + (cons (string-append (substring s bgn end) unescaped) rst) + len quts? lst))) + ((and quts? (not (char=? (string-ref s end) #\"))) + (loop s bgn (+ 1 end) rst len quts? lst)) + (else + (case (string-ref s end) + ((#\{) + (loop s (+ 1 end) (+ 1 end) + (cons + (string-append + (substring s bgn end) "((" ) rst) len quts? (cons #t lst))) + ((#\}) + (loop s (+ 1 end) (+ 1 end) + (cons + (string-append + (substring s bgn end) "))") rst) len quts? (loose-cdr lst))) + ((#\[) + (loop s (+ 1 end) (+ 1 end) + (cons + (string-append + (substring s bgn end) "#(") rst) len quts? (cons #f lst))) + ((#\]) + (loop s (+ 1 end) (+ 1 end) + (cons + (string-append + (substring s bgn end) ")") rst) len quts? (loose-cdr lst))) + ((#\:) + (loop s (+ 1 end) (+ 1 end) + (cons + (string-append + (substring s bgn end) " . ") rst) len quts? lst)) + ((#\,) + (loop s (+ 1 end) (+ 1 end) + (cons + (string-append + (substring s bgn end) + (if (loose-car lst) ")(" " ")) rst) len quts? lst)) + ((#\") + (loop s bgn (+ 1 end) rst len (not quts?) lst)) + (else + (loop s bgn (+ 1 end) rst len quts? lst)))))))))) + +(define json->string + (lambda (json-scm) + (define f + (lambda (x) + (cond + ((string? x) (json-string-escape x)) + ((number? x) (number->string x)) + ((symbol? x) (symbol->string x)) + ((null? x) "{}") + (else (type-error "Unexpected x: " x))))) + + (define (delim x) + (if (zero? x) "" ",")) + + (when (procedure? json-scm) + (type-error "json->string: input must not be a procedure")) + + (let loop ((lst json-scm) (x (if (vector? json-scm) "[" "{"))) + (if (vector? lst) + (string-append x + (let loop-v ((len (vector-length lst)) (n 0) (y "")) + (if (< n len) + (let* ((k (vector-ref lst n)) + (result (cond + ((vector? k) + (loop k "[")) + ((pair? k) + (loop k "{")) + (else + (f k))))) + (loop-v len (+ n 1) + (string-append y (delim n) result))) + (string-append y "]")))) + (let* ((d (car lst)) + (k (loose-car d)) + (v (loose-cdr d))) + (when (not (list? d)) + (value-error d " must be a list")) + (let ((len (length d))) + (when (not (or (= len 0) (= len -1) (>= len 2))) + (value-error d " must be null, pair, or list with at least 2 elements"))) + + (if (null? (cdr lst)) + (if (null? d) + "{}" + (string-append x (f k) ":" + (cond + ((null? v) "{}") + ((list? v) (loop v "{")) + ((vector? v) (loop v "[")) + (else (f v))) + "}")) + (loop (cdr lst) + (cond + ((list? v) (string-append x (f k) ":" (loop v "{") ",")) + ((vector? v) (string-append x (f k) ":" (loop v "[") ",")) + (else (string-append x (f k) ":" (f v) ",")))))))))) + + +(define json-ref + (lambda (x k) + (define return + (lambda (x) + (if (symbol? x) + (cond + ((symbol=? x 'true) #t) + ((symbol=? x 'false) #f) + ((symbol=? x 'null) '()) + (else x)) + x))) + (if (vector? x) + (return (vector-ref x k)) + (let loop ((x x) (k k)) + (if (null? x) + '() + (if (equal? (caar x) k) + (return (cdar x)) + (loop (cdr x) k))))))) + +(define (json-ref* j . keys) + (let loop ((expr j) (keys keys)) + (if (null? keys) + expr + (loop (json-ref expr (car keys)) (cdr keys))))) + +(define json-set + (lambda (x v p) + (let ((x x) (v v) (p (if (procedure? p) p (lambda (x) p)))) + (if (vector? x) + (list->vector + (cond + ((boolean? v) + (if v + (let l ((x (vector->alist x))(p p)) + (if (null? x) + '() + (cons (p (cdar x)) (l (cdr x) p)))))) + ((procedure? v) + (let l ((x (vector->alist x))(v v)(p p)) + (if (null? x) + '() + (if (v (caar x)) + (cons (p (cdar x)) (l (cdr x) v p)) + (cons (cdar x) (l (cdr x) v p)))))) + (else + (let l ((x (vector->alist x))(v v)(p p)) + (if (null? x) + '() + (if (equal? (caar x) v) + (cons (p (cdar x)) (l (cdr x) v p)) + (cons (cdar x) (l (cdr x) v p)))))))) + (cond + ((boolean? v) + (if v + (let l ((x x)(p p)) + (if (null? x) + '() + (cons (cons (caar x) (p (cdar x)))(l (cdr x) p)))))) + ((procedure? v) + (let l ((x x)(v v)(p p)) + (if (null? x) + '() + (if (v (caar x)) + (cons (cons (caar x) (p (cdar x)))(l (cdr x) v p)) + (cons (car x) (l (cdr x) v p)))))) + (else + (let l ((x x)(v v)(p p)) + (if (null? x) + '() + (if (equal? (caar x) v) + (cons (cons v (p (cdar x)))(l (cdr x) v p)) + (cons (car x) (l (cdr x) v p))))))))))) + +(define (json-set* json k0 k1_or_v . ks_and_v) + (if (null? ks_and_v) + (json-set json k0 k1_or_v) + (json-set json k0 + (lambda (x) + (apply json-set* (cons x (cons k1_or_v ks_and_v))))))) + +(define (json-push x k v) + (if (vector? x) + (if (= (vector-length x) 0) + (vector v) + (list->vector + (let l ((x (vector->alist x)) (k k) (v v) (b #f)) + (if (null? x) + (if b '() (cons v '())) + (if (equal? (caar x) k) + (cons v (cons (cdar x) (l (cdr x) k v #t))) + (cons (cdar x) (l (cdr x) k v b))))))) + (cons (cons k v) x))) + +(define (json-push* json k0 v0 . rest) + (if (null? rest) + (json-push json k0 v0) + (json-set json k0 + (lambda (x) (apply json-push* (cons x (cons v0 rest))))))) + +(define json-drop + (lambda (x v) + (if (vector? x) + (if (zero? (vector-length x)) + x + (list->vector + (cond + ((procedure? v) + (let l ((x (vector->alist x)) (v v)) + (if (null? x) + '() + (if (v (caar x)) + (l (cdr x) v) + (cons (cdar x) (l (cdr x) v)))))) + (else + (let l ((x (vector->alist x)) (v v)) + (if (null? x) + '() + (if (equal? (caar x) v) + (l (cdr x) v) + (cons (cdar x) (l (cdr x) v))))))))) + (cond + ((procedure? v) + (let l ((x x) (v v)) + (if (null? x) + '() + (if (v (caar x)) + (l (cdr x) v) + (cons (car x) (l (cdr x) v)))))) + (else + (let l ((x x) (v v)) + (if (null? x) + '() + (if (equal? (caar x) v) + (l (cdr x) v) + (cons (car x) (l (cdr x) v)))))))))) + +(define json-drop* + (lambda (json key . rest) + (if (null? rest) + (json-drop json key) + (json-set json key + (lambda (x) (apply json-drop* (cons x rest))))))) + +(define json-reduce + (lambda (x v p) + (if (vector? x) + (list->vector + (cond + ((boolean? v) + (if v + (let l ((x (vector->alist x)) (p p)) + (if (null? x) + '() + (cons (p (caar x) (cdar x)) (l (cdr x) p)))) + x)) + ((procedure? v) + (let l ((x (vector->alist x)) (v v) (p p)) + (if (null? x) + '() + (if (v (caar x)) + (cons (p (caar x) (cdar x)) (l (cdr x) v p)) + (cons (cdar x) (l (cdr x) v p)))))) + (else + (let l ((x (vector->alist x)) (v v) (p p)) + (if (null? x) + '() + (if (equal? (caar x) v) + (cons (p (caar x) (cdar x)) (l (cdr x) v p)) + (cons (cdar x) (l (cdr x) v p)))))))) + (cond + ((boolean? v) + (if v + (let l ((x x) (p p)) + (if (null? x) + '() + (cons (cons (caar x) (p (caar x) (cdar x))) (l (cdr x) p)))) + x)) + ((procedure? v) + (let l ((x x) (v v) (p p)) + (if (null? x) + '() + (if (v (caar x)) + (cons (cons (caar x) (p (caar x) (cdar x))) (l (cdr x) v p)) + (cons (car x) (l (cdr x) v p)))))) + (else + (let l ((x x) (v v) (p p)) + (if (null? x) + '() + (if (equal? (caar x) v) + (cons (cons v (p v (cdar x))) (l (cdr x) v p)) + (cons (car x) (l (cdr x) v p)))))))))) + +(define (json-reduce* j v1 v2 . rest) + (cond + ((null? rest) (json-reduce j v1 v2)) + ((length=? 1 rest) + (json-reduce j v1 + (lambda (x y) + (let* ((new-v1 v2) + (p (last rest))) + (json-reduce y new-v1 + (lambda (n m) (p (list x n) m))))))) + (else + (json-reduce j v1 + (lambda (x y) + (let* ((new-v1 v2) + (p (last rest))) + (apply json-reduce* + (append (cons y (cons new-v1 (drop-right rest 1))) + (list (lambda (n m) (p (cons x n) m))))))))))) + +) ; end of begin +) ; end of define-library diff --git a/goldfish/liii/bag.scm b/goldfish/liii/bag.scm new file mode 100644 index 0000000000000000000000000000000000000000..7c245480605fbf26d5c7353f06393c3e4171a4cd --- /dev/null +++ b/goldfish/liii/bag.scm @@ -0,0 +1,52 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(define-library (liii bag) + (import (rename (srfi srfi-113) + (bag make-bag-with-comparator) + (list->bag list->bag-with-comparator)) + (only (srfi srfi-113) + bag-unfold bag-member bag-comparator bag->list + list->bag! bag-copy + bag? bag-contains? bag-empty? bag-disjoint? + bag-size bag-find bag-count bag-any? bag-every? + bag=? bag? bag<=? bag>=? + bag-union bag-intersection bag-difference bag-xor + bag-union! bag-intersection! bag-difference! bag-xor! + bag-adjoin bag-adjoin! bag-replace bag-replace! + bag-delete bag-delete! bag-delete-all bag-delete-all! + bag-search!) + (srfi srfi-128)) + (export bag bag-unfold bag-member bag-comparator + bag->list list->bag list->bag! bag-copy + bag? bag-contains? bag-empty? bag-disjoint? + bag-size bag-find bag-count bag-any? bag-every? + bag=? bag? bag<=? bag>=? + bag-union bag-intersection bag-difference bag-xor + bag-union! bag-intersection! bag-difference! bag-xor! + bag-adjoin bag-adjoin! bag-replace bag-replace! + bag-delete bag-delete! bag-delete-all bag-delete-all! + bag-search!) + + (define comp (make-default-comparator)) + + (define (bag . elements) + (apply make-bag-with-comparator comp elements)) + + (define (list->bag elements) + (list->bag-with-comparator comp elements)) + +) ; end of define-library diff --git a/goldfish/liii/either.scm b/goldfish/liii/either.scm index 90f688aaf272660780f232f1b82c82e67c8866bc..d091fcf1405add8f63c33dc99c81798dca6e09b0 100644 --- a/goldfish/liii/either.scm +++ b/goldfish/liii/either.scm @@ -1,5 +1,5 @@ ; -; Copyright (C) 2025 The Goldfish Scheme Authors +; Copyright (C) 2026 The Goldfish Scheme Authors ; ; Licensed under the Apache License, Version 2.0 (the "License"); ; you may not use this file except in compliance with the License. @@ -15,103 +15,167 @@ ; (define-library (liii either) - (import (liii option) (liii oop) (liii base)) - (export either left right) + (import (liii base)) + (export from-left to-left + from-right to-right + either? ; 导出通用判断函数 + either-left? either-right? + either-map either-for-each + either-get-or-else + either-or-else + either-filter-or-else + either-contains + either-every + either-any) (begin - (define-case-class either - ((type symbol?) - (value any?)) - - (define (%left?) - (eq? type 'left)) - - (define (%right?) - (eq? type 'right)) - - (define (%get) - value) - - (define (%or-else default) - (when (not (either :is-type-of default)) - (type-error "The first parameter of either%or-else must be a either case class")) - - (if (%right?) - (%this) - default)) - - (define (%get-or-else default) - (cond ((%right?) value) - ((and (procedure? default) (not (case-class? default))) - (default)) - (else default))) - - (define (%filter-or-else pred zero) - (unless (procedure? pred) - (type-error - (format #f "In funtion #<~a ~a>: argument *~a* must be *~a*! **Got ~a**" - %filter-or-else '(pred zero) 'pred "procedure" (object->string pred)))) - - (unless (any? zero) - (type-error - (format #f "In funtion #<~a ~a>: argument *~a* must be *~a*! **Got ~a**" - %filter-or-else '(pred zero) 'zero "any" (object->string zero)))) - (if (%right?) - (if (pred value) - (%this) - (left zero)) - (%this))) - - (define (%contains x) - (and (%right?) - (class=? x value))) - - (define (%for-each f) - (when (%right?) - (f value))) - - (define (%to-option) - (if (%right?) - (option value) - (none))) - - (define (%map f . args) - (chain-apply args - (if (%right?) - (right (f value)) - (%this)))) - - (define (%flat-map f . args) - (chain-apply args - (if (%right?) - (f value) - (%this)))) - - (define (%forall pred) - (unless (procedure? pred) - (type-error - (format #f "In funtion #<~a ~a>: argument *~a* must be *~a*! **Got ~a**" - %forall '(pred) 'pred "procedure" (object->string pred)))) - (if (%right?) - (pred value) - #t)) - - (define (%exists pred) - (unless (procedure? pred) - (type-error - (format #f "In funtion #<~a ~a>: argument *~a* must be *~a*! **Got ~a**" - %exists '(pred) 'pred "procedure" (object->string pred)))) - (if (%right?) - (pred value) - #f)) - - ) - - (define (left v) - (either 'left v)) - - (define (right v) - (either 'right v)) - - ) ; end of begin - ) ; end of define-library + ;; ====================== + ;; 构造函数 + ;; ====================== + + ;; 创建左值(错误情况) + (define (from-left value) + (cons value 'left)) + + ;; 创建右值(成功情况) + (define (from-right value) + (cons value 'right)) + + ;; ====================== + ;; 类型判断函数 + ;; ====================== + + ;; 检查是否是左值 + (define (either-left? either) + (and (pair? either) (eq? (cdr either) 'left))) + + ;; 检查是否是右值 + (define (either-right? either) + (and (pair? either) (eq? (cdr either) 'right))) + + ;; 检查是否是 either 类型 (即左值或右值) + (define (either? x) + (or (either-left? x) + (either-right? x))) + + ;; 类型安全检查 + (define (check-either x func-name) + (unless (either? x) + (type-error + (format #f "In function ~a: argument must be *Either* type! **Got ~a**" + func-name (object->string x))))) + + ;; ====================== + ;; 提取函数 + ;; ====================== + + ;; 从 either 中提取左值 + (define (to-left either) + (check-either either "to-left") + (cond + ((eq? (cdr either) 'left) + (car either)) + (else + (value-error "Cannot extract left from Right" either)))) + + ;; 从 either 中提取右值 + (define (to-right either) + (check-either either "to-right") + (cond + ((eq? (cdr either) 'right) + (car either)) + (else + (value-error "Cannot extract right from Left" either)))) + + ;; ====================== + ;; 高阶函数操作 + ;; ====================== + + ;; 映射函数:如果 either 是右值,则应用函数 f + (define (either-map f either) + (check-either either "either-map") + (unless (procedure? f) + (type-error (format #f "In function either-map: argument *f* must be *procedure*! **Got ~a**" f))) + (if (either-right? either) + (from-right (f (car either))) + either)) + + ;; 遍历函数:如果 either 是右值,则应用函数 f (执行副作用) + (define (either-for-each f either) + (check-either either "either-for-each") + (unless (procedure? f) + (type-error (format #f "In function either-for-each: argument *f* must be *procedure*! **Got ~a**" f))) + (when (either-right? either) + (f (car either)))) + + ;; ====================== + ;; 逻辑判断与过滤函数 + ;; ====================== + + ;; 过滤:如果是右值且不满足 pred,则转换为 (from-left zero) + (define (either-filter-or-else pred zero either) + (check-either either "either-filter-or-else") + (unless (procedure? pred) + (type-error + (format #f "In function either-filter-or-else: argument *pred* must be *procedure*! **Got ~a**" + (object->string pred)))) + + ;; 注意:通常不需要检查 zero,因为它作为 left 值可以是任何类型 + + (if (either-right? either) + (if (pred (car either)) + either + (from-left zero)) + either)) + + ;; 包含:如果是右值且内部值等于 x + (define (either-contains either x) + (check-either either "either-contains") + (and (either-right? either) + (equal? x (car either)))) + + ;; 全称量词:如果是右值则判断 pred,如果是左值默认为 #t + (define (either-every pred either) + (check-either either "either-every") + (unless (procedure? pred) + (type-error + (format #f "In function either-every: argument *pred* must be *procedure*! **Got ~a**" + (object->string pred)))) + + (if (either-right? either) + (pred (car either)) + #t)) + + ;; 存在量词:如果是右值则判断 pred,如果是左值默认为 #f + (define (either-any pred either) + (check-either either "either-any") + (unless (procedure? pred) + (type-error + (format #f "In function either-any: argument *pred* must be *procedure*! **Got ~a**" + (object->string pred)))) + + (if (either-right? either) + (pred (car either)) + #f)) + + ;; ====================== + ;; 附加实用函数 + ;; ====================== + + ;; 获取值或默认值 + (define (either-get-or-else either default) + (check-either either "either-get-or-else") + (if (either-right? either) + (car either) + default)) + + ;; 组合器:如果是 Left 则返回 alternative,否则返回自身 + (define (either-or-else either alternative) + (check-either either "either-or-else") + (if (either-right? either) + either + alternative)) + + + ) ; end of begin +) ; end of define-library \ No newline at end of file diff --git a/goldfish/liii/hash-table.scm b/goldfish/liii/hash-table.scm index e5b6593623e9e177d382b5bc50797e087c04d2df..8607f3accd1aeb446ca9f27ef82f6f4f95dc5005 100644 --- a/goldfish/liii/hash-table.scm +++ b/goldfish/liii/hash-table.scm @@ -25,11 +25,10 @@ hash-table-set! hash-table-delete! hash-table-intern! hash-table-update! hash-table-update!/default hash-table-pop! hash-table-clear! hash-table-size hash-table-keys hash-table-values hash-table-entries - hash-table-find hash-table-count + hash-table-find hash-table-count hash-table-fold hash-table-for-each hash-table-map->list hash-table->alist ) (begin ) ; end of begin ) ; end of library - diff --git a/goldfish/liii/http.scm b/goldfish/liii/http.scm new file mode 100644 index 0000000000000000000000000000000000000000..0207721b169bdf34accc6cd0f11349df779fcf00 --- /dev/null +++ b/goldfish/liii/http.scm @@ -0,0 +1,70 @@ +;; +;; COPYRIGHT: (C) 2025 Liii Network Inc +;; All rights reverved. +;; + +(define-library (liii http) +(import (liii hash-table) + (liii alist)) +(export http-head http-get http-post http-ok? + http-stream-get http-stream-post) +(begin + +(define (http-ok? r) + (let ((status-code (r 'status-code)) + (reason (r 'reason)) + (url (r 'url))) + (cond ((and (>= status-code 400) (< status-code 500)) + (error 'http-error + (string-append (number->string status-code) + " Client Error: " reason " for url: " url))) + ((and (>= status-code 500) (< status-code 600)) + (error 'http-error + (string-append (number->string status-code) + " Server Error: " reason " for url: " url))) + (else #t)))) + +(define* (http-head url) + (let1 r (g_http-head url) + r)) + +(define* (http-get url (params '()) (headers '()) (proxy '())) + (when (not (alist? params)) + (type-error params "is not a association list")) + (when (not (alist? proxy)) + (type-error proxy "is not a association list")) + (let1 r (g_http-get url params headers proxy) + r)) + +(define* (http-post url (params '()) (data "") (headers '()) (proxy '())) + (when (not (alist? proxy)) + (type-error proxy "is not a association list")) + (cond ((and (string? data) (> (string-length data) 0) (null? headers)) + (g_http-post url params data '(("Content-Type" . "text/plain")) proxy)) + (else (g_http-post url params data headers proxy)))) + +;; Streaming API wrapper functions + +(define* (http-stream-get url callback (userdata '()) (params '()) (proxy '())) + (when (not (alist? params)) + (type-error params "is not a association list")) + (when (not (alist? proxy)) + (type-error proxy "is not a association list")) + (when (not (procedure? callback)) + (type-error callback "is not a procedure")) + (g_http-stream-get url params proxy userdata callback)) + +(define* (http-stream-post url callback (userdata '()) (params '()) (data "") (headers '()) (proxy '())) + (when (not (alist? params)) + (type-error params "is not a association list")) + (when (not (alist? proxy)) + (type-error proxy "is not a association list")) + (when (not (procedure? callback)) + (type-error callback "is not a procedure")) + (cond ((and (string? data) (> (string-length data) 0) (null? headers)) + (g_http-stream-post url params data '(("Content-Type" . "text/plain")) proxy userdata callback)) + (else (g_http-stream-post url params data headers proxy userdata callback)))) + +) ; end of begin +) ; end of define-library + diff --git a/goldfish/liii/json.scm b/goldfish/liii/json.scm new file mode 100644 index 0000000000000000000000000000000000000000..712dd9dc5ee36dfa5431dc3aa7d077745f9ffa46 --- /dev/null +++ b/goldfish/liii/json.scm @@ -0,0 +1,194 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(define-library (liii json) + (import (liii base) + (liii list) + (rename (guenchi json) + (json-ref g:json-ref) (json-ref* g:json-ref*) + (json-set g:json-set) (json-set* g:json-set*) + (json-push g:json-push) (json-push* g:json-push*) + (json-drop g:json-drop) (json-drop* g:json-drop*) + (json-reduce g:json-reduce) (json-reduce* g:json-reduce*))) + (export + json-string-escape + string->json + json->string + + json-ref json-set json-push json-drop json-reduce + + json-null? json-object? json-array? json-string? json-float? json-number? json-integer? json-boolean? + + json-contains-key? + + json-ref-string json-ref-number json-ref-integer json-ref-boolean json-get-or-else + + json-keys) + + (begin + + ;;; --------------------------------------------------------- + ;;; 0. 统一接口 + ;;; --------------------------------------------------------- + + (define (ensure-json-structure x) + (unless (or (json-object? x) (json-array? x)) + (type-error "Value is not a JSON object or array" x))) + + (define (json-ref json key . args) + (if (null? json) ; Allow () to pass through as "not found" to support safe navigation + '() + (begin + (ensure-json-structure json) + (let ((val (if (and (json-object? json) (equal? json '(()))) + '() + (g:json-ref json key)))) + (if (null? args) + val + (apply json-ref (cons val args))))))) + + (define (json-set json key val . args) + (ensure-json-structure json) + (if (null? args) + (if (and (json-object? json) (equal? json '(()))) + json + (g:json-set json key val)) + (json-set json key + (lambda (x) + (apply json-set (cons x (cons val args))))))) + + (define (json-push json key val . args) + (ensure-json-structure json) + (if (null? args) + (if (and (json-object? json) (equal? json '(()))) + (g:json-push '() key val) + (g:json-push json key val)) + (json-set json key + (lambda (x) + (apply json-push (cons x (cons val args))))))) + + (define (json-drop json key . args) + (ensure-json-structure json) + (if (null? args) + (if (and (json-object? json) (equal? json '(()))) + json + (g:json-drop json key)) + (json-set json key + (lambda (x) + (apply json-drop (cons x args)))))) + + (define (json-reduce json key . args) + (if (null? json) + '() + (begin + (ensure-json-structure json) + (if (null? args) + (value-error "json-reduce: missing arguments") + (if (null? (cdr args)) + ;; Single level: (json-reduce json key proc) + (let ((proc (car args))) + (if (and (json-object? json) (equal? json '(()))) + json + (g:json-reduce json key proc))) + ;; Multi level + (let* ((keys (cons key (drop-right args 1))) + (proc (last args)) + (top-key (car keys)) + (rest-keys (cdr keys))) + (json-reduce json top-key + (lambda (k v) + (apply json-reduce (append (list v) rest-keys (list proc))))))))))) + + ;;; --------------------------------------------------------- + ;;; 1. 类型谓词 + ;;; --------------------------------------------------------- + + (define (json-null? x) + (eq? x 'null)) + + (define (json-object? x) + (and (list? x) + (not (null? x)) + (or (equal? x '(())) + (every pair? x)))) + + (define (json-array? x) + (vector? x)) + + (define (json-string? x) + (string? x)) + + (define (json-number? x) + (number? x)) + + (define (json-integer? x) + (integer? x)) + + (define (json-float? x) + (float? x)) + + (define (json-boolean? x) + (boolean? x)) + + ;;; --------------------------------------------------------- + ;;; 2. 状态检查 + ;;; --------------------------------------------------------- + + (define (json-contains-key? json key) + (if (not (json-object? json)) + #f + (if (equal? json '(())) + #f + (if (assoc key json) #t #f)))) + + ;;; --------------------------------------------------------- + ;;; 3. 安全获取器 + ;;; --------------------------------------------------------- + + (define (json-get-or-else json default) + (if (json-null? json) + default + json)) + + (define (json-ref-string json key default) + (let ((val (json-ref json key))) + (if (string? val) val default))) + + (define (json-ref-number json key default) + (let ((val (json-ref json key))) + (if (number? val) val default))) + + (define (json-ref-integer json key default) + (let ((val (json-ref json key))) + (if (integer? val) val default))) + + (define (json-ref-boolean json key default) + (let ((val (json-ref json key))) + (if (boolean? val) val default))) + + ;;; --------------------------------------------------------- + ;;; 4. 辅助工具 + ;;; --------------------------------------------------------- + + (define (json-keys json) + (if (json-object? json) + (if (equal? json '(())) + '() + (map car json)) + '())) + + ) ; end of begin +) ; end of define-library \ No newline at end of file diff --git a/goldfish/liii/lang.scm b/goldfish/liii/lang.scm index 02988777981287b87921dc7754a98f6e821411c8..498968eb3278d0bb1f76c477c169f377fc10b3c8 100644 --- a/goldfish/liii/lang.scm +++ b/goldfish/liii/lang.scm @@ -29,7 +29,7 @@ (liii error) (liii list) (liii option) - (liii either) + (liii rich-either) (liii rich-list) (liii rich-char) (liii rich-vector) diff --git a/goldfish/liii/raw-string.scm b/goldfish/liii/raw-string.scm new file mode 100644 index 0000000000000000000000000000000000000000..2608f86362703fd72eb7159334a04746e8b5e8c0 --- /dev/null +++ b/goldfish/liii/raw-string.scm @@ -0,0 +1,79 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +;; Acknowledgements +;; +;; This implementation is based on: +;; +;; 1. The idea deindent from guile-raw-strings by François Joulaud +;; https://codeberg.org/avalenn/guile-raw-strings +;; SPDX-License-Identifier: 0BSD +;; +;; 2. The deindentation follows C# raw string literal rules +;; https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/raw-string + +(define-library (liii raw-string) + (import (srfi srfi-267) + (srfi srfi-1) + (srfi srfi-13) + (liii error)) + (export deindent &-) + (begin + (define (string-split-lines str) + (let ((len (string-length str))) + (let loop ((start 0) (result '())) + (let ((nl-pos (string-index str #\newline start len))) + (if (not nl-pos) + (reverse (cons (substring str start len) result)) + (loop (+ nl-pos 1) + (cons (substring str start nl-pos) result))))))) + + (define (f-deindent str) + (when (or (string-null? str) + (not (char=? #\newline (string-ref str 0)))) + (value-error "Raw string must start on a new line after the opening delimiter")) + + (let* ((lines (string-split-lines (substring str 1 (string-length str)))) + (closing-line (last lines)) + (ref-indent (if (string-null? closing-line) + (value-error "Raw string delimiter must be on its own line") + (string-count closing-line #\space))) + (content-lines (drop-right lines 1))) + + ;; check indentation + (for-each (lambda (line idx) + (unless (string-null? line) + (let ((indent (or (string-skip line #\space) 0))) + (when (< indent ref-indent) + (value-error "Line ~a does not start with the same whitespace as the closing line of the raw string" (+ idx 1)))))) + content-lines + (iota (length content-lines))) + + (string-join + (map (lambda (line) + (if (string-null? line) + "" + (substring line ref-indent))) + content-lines) + "\n"))) + + (define-macro (stx-deindent v) + (if (string? v) + `(quote ,(f-deindent v)) + `(quote ,v))) + + (define deindent stx-deindent) + (define &- stx-deindent))) diff --git a/goldfish/liii/rich-either.scm b/goldfish/liii/rich-either.scm new file mode 100644 index 0000000000000000000000000000000000000000..f3e49559abdce9b3a10e774863869516122c4c01 --- /dev/null +++ b/goldfish/liii/rich-either.scm @@ -0,0 +1,118 @@ +; +; Copyright (C) 2025 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(define-library (liii rich-either) + (import (liii option) (liii oop) (liii base)) + (export rich-either left right) + (begin + + (define-case-class rich-either + ((type symbol?) + (value any?)) + + (define (%left?) + (eq? type 'left)) + + (define (%right?) + (eq? type 'right)) + + (define (%get) + value) + + (define (%or-else default) + (when (not (rich-either :is-type-of default)) + (type-error "The first parameter of either%or-else must be a either case class")) + + (if (%right?) + (%this) + default)) + + (define (%get-or-else default) + (cond ((%right?) value) + ((and (procedure? default) (not (case-class? default))) + (default)) + (else default))) + + (define (%filter-or-else pred zero) + (unless (procedure? pred) + (type-error + (format #f "In funtion #<~a ~a>: argument *~a* must be *~a*! **Got ~a**" + %filter-or-else '(pred zero) 'pred "procedure" (object->string pred)))) + + (unless (any? zero) + (type-error + (format #f "In funtion #<~a ~a>: argument *~a* must be *~a*! **Got ~a**" + %filter-or-else '(pred zero) 'zero "any" (object->string zero)))) + (if (%right?) + (if (pred value) + (%this) + (left zero)) + (%this))) + + (define (%contains x) + (and (%right?) + (class=? x value))) + + (define (%for-each f) + (when (%right?) + (f value))) + + (define (%to-option) + (if (%right?) + (option value) + (none))) + + (define (%map f . args) + (chain-apply args + (if (%right?) + (right (f value)) + (%this)))) + + (define (%flat-map f . args) + (chain-apply args + (if (%right?) + (f value) + (%this)))) + + (define (%forall pred) + (unless (procedure? pred) + (type-error + (format #f "In funtion #<~a ~a>: argument *~a* must be *~a*! **Got ~a**" + %forall '(pred) 'pred "procedure" (object->string pred)))) + (if (%right?) + (pred value) + #t)) + + (define (%exists pred) + (unless (procedure? pred) + (type-error + (format #f "In funtion #<~a ~a>: argument *~a* must be *~a*! **Got ~a**" + %exists '(pred) 'pred "procedure" (object->string pred)))) + (if (%right?) + (pred value) + #f)) + + ) + + (define (left v) + (rich-either 'left v)) + + (define (right v) + (rich-either 'right v)) + + + ) ; end of begin + ) ; end of define-library diff --git a/goldfish/liii/rich-json.scm b/goldfish/liii/rich-json.scm new file mode 100644 index 0000000000000000000000000000000000000000..7bbf9b52b827d7d2d3ff7c57ba32f217ef05df73 --- /dev/null +++ b/goldfish/liii/rich-json.scm @@ -0,0 +1,153 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(define-library (liii rich-json) +(import (liii base) (liii lang) (guenchi json)) +(export + rich-json + json-string-escape json-string-unescape string->json json->string + json-ref json-ref* + json-set json-set* json-push json-push* json-drop json-drop* json-reduce json-reduce*) +(begin + +(define-class rich-json + ((data any? #f)) + + (define (%get) + data) + + (define (%get-or-else default) + (if (%null?) + default + data)) + + (typed-define (%get-string (key any?) (default string?)) + (let1 r (json-ref data key) + (if (string? r) r default))) + + (typed-define (%get-number (key any?) (default number?)) + (let1 r (json-ref data key) + (if (number? r) r default))) + + (typed-define (%get-boolean (key any?) (default boolean?)) + (let1 r (json-ref data key) + (if (boolean? r) r default))) + + (define (%keys) + (if (not (%object?)) + '() + ((box data) :map car :collect))) + + (define (%apply x . xs) + (@make (apply json-ref* (cons data (cons x xs))))) + + (define (%set x . xs) + (let ((processed-xs (map (lambda (arg) + (if (case-class? arg) + (arg :get) + arg)) + xs))) + (rich-json (apply json-set* (cons data (cons x processed-xs)))))) + + (define (%transform key . args) + (if (null? args) + (%this) + (let ((more-keys ($ args :drop-right 1 :collect)) + (all-args (append (list data key) args))) + (if (null? more-keys) + (rich-json (apply json-reduce all-args)) + (rich-json (apply json-reduce* all-args)))))) + + (define (%drop key . args) + (if (null? args) + (rich-json (json-drop data key)) + (rich-json (apply json-drop* (append (list data key) args))))) + + (define (%push x . xs) + (let ((processed-xs (map (lambda (arg) + (if (case-class? arg) + (arg :get) + arg)) + xs))) + (@make (apply json-push* (cons data (cons x processed-xs)))))) + + (define (%null?) + (eq? data 'null)) + + (define (%object?) + (and (list? data) (not (null? data)))) + + (define (%contains-key? key) + (if (not (%object?)) + #f + ((box data) + :exists (lambda (x) (equal? (car x) key))))) + + (define (%array?) + (vector? data)) + + (define (%string?) + (string? data)) + + (define (%number?) + (number? data)) + + (define (%integer?) + (integer? data)) + + (define (%float?) + (float? data)) + + (define (%boolean?) + (boolean? data)) + + + (define (%to-string) + (cond ((integer? data) (number->string data)) + ((symbol? data) (symbol->string data)) + ((string? data) data) + (else (json->string data)))) + + (chained-define (@null) + (rich-json 'null)) + + (chained-define (@true) + (rich-json 'true)) + + (chained-define (@false) + (rich-json 'false)) + + (chained-define (@parse s) + (@apply (string->json s))) + + (chained-define (@apply x) + (let ((j (rich-json))) + (cond + ((string? x) (j :set-data! x)) + ((null? x) (j :set-data! 'null)) + ((boolean? x) (if x (j :set-data! 'true) (j :set-data! 'false))) + ((number? x) (j :set-data! x)) + ((procedure? x) + (type-error "rich-json: a procedure could not be converted to rich-json case class")) + (else (j :set-data! x))) + j)) + + (chained-define (@make x) + (@apply x)) +) + +) ; end of begin +) ; end of define-library \ No newline at end of file diff --git a/goldfish/liii/rich-set.scm b/goldfish/liii/rich-set.scm new file mode 100644 index 0000000000000000000000000000000000000000..2b8b6837f27994176d30232501b5d14dbf3d661b --- /dev/null +++ b/goldfish/liii/rich-set.scm @@ -0,0 +1,69 @@ +; +; Copyright (C) 2024 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(define-library (liii rich-set) + (import (liii lang)(liii hash-table)(srfi srfi-128)) + (export rich-hash-set) + (begin + + + (define-case-class rich-hash-set ((data hash-table?)) + + ;; Factory methods + (chained-define (@empty) + (rich-hash-set (make-hash-table))) + + ;; Basic operations + (define (%size) (hash-table-size data)) + + (define (%empty?) (hash-table-empty? data)) + + (define (%contains element) + (hash-table-contains? data element)) + + ;; Modification operations + (chained-define (%add-one! element) + (hash-table-set! data element #t) + (%this)) + + (chained-define (%remove! element) + (hash-table-delete! data element) + (%this)) + + (chained-define (%add-one element) + (let ((ht (make-hash-table))) + (hash-table-for-each + (lambda (k v) (hash-table-set! ht k v)) + data) + (hash-table-set! ht element #t) + (rich-hash-set ht))) + + (chained-define (%remove element) + (let ((ht (make-hash-table))) + (hash-table-for-each + (lambda (k v) (hash-table-set! ht k v)) + data) + (hash-table-delete! ht element) + (rich-hash-set ht))) + + (chained-define (%clear!) + (hash-table-clear! data) + (%this)) + + ) ; end of define-case-class + ) ; end of begin + ) ; end of define-library + diff --git a/goldfish/liii/set.scm b/goldfish/liii/set.scm index dac49e13b8e72a250f206fc3fa21505f692e2079..2101c030f3dd45098e6fa9cfa0d5a47483278114 100644 --- a/goldfish/liii/set.scm +++ b/goldfish/liii/set.scm @@ -1,5 +1,5 @@ ; -; Copyright (C) 2024 The Goldfish Scheme Authors +; Copyright (C) 2026 The Goldfish Scheme Authors ; ; Licensed under the Apache License, Version 2.0 (the "License"); ; you may not use this file except in compliance with the License. @@ -15,55 +15,27 @@ ; (define-library (liii set) - (import (liii lang)(liii hash-table)(srfi srfi-128)) - (export hash-set) - (begin - - - (define-case-class hash-set ((data hash-table?)) - - ;; Factory methods - (chained-define (@empty) - (hash-set (make-hash-table))) - - ;; Basic operations - (define (%size) (hash-table-size data)) - - (define (%empty?) (hash-table-empty? data)) - - (define (%contains element) - (hash-table-contains? data element)) - - ;; Modification operations - (chained-define (%add-one! element) - (hash-table-set! data element #t) - (%this)) - - (chained-define (%remove! element) - (hash-table-delete! data element) - (%this)) - - (chained-define (%add-one element) - (let ((ht (make-hash-table))) - (hash-table-for-each - (lambda (k v) (hash-table-set! ht k v)) - data) - (hash-table-set! ht element #t) - (hash-set ht))) - - (chained-define (%remove element) - (let ((ht (make-hash-table))) - (hash-table-for-each - (lambda (k v) (hash-table-set! ht k v)) - data) - (hash-table-delete! ht element) - (hash-set ht))) - - (chained-define (%clear!) - (hash-table-clear! data) - (%this)) - - ) ; end of define-case-class - ) ; end of begin - ) ; end of define-library - + (import (rename (srfi srfi-113) + (set make-set-with-comparator) + (list->set list->set-with-comparator)) + (srfi srfi-128)) + (export set set-unfold list->set list->set! set-copy set->list list->set-with-comparator make-set-with-comparator + set? set-contains? set-empty? set-disjoint? + set-element-comparator set-size + set=? set? set<=? set>=? + set-any? set-every? set-find set-count set-member set-search! set-map + set-for-each set-fold set-filter set-filter! set-remove set-remove! + set-partition set-partition! set-union set-intersection set-difference set-xor + set-union! set-intersection! set-difference! set-xor! + set-adjoin set-adjoin! set-replace set-replace! + set-delete set-delete! set-delete-all set-delete-all!) + + (define comp (make-default-comparator)) + + (define (set . elements) + (apply make-set-with-comparator comp elements)) + + (define (list->set elements) + (list->set-with-comparator comp elements)) + +) ; end of define-library diff --git a/goldfish/liii/string.scm b/goldfish/liii/string.scm index 7a55470c73717e0a9ac65b670be0a306fa35eb11..2ea2eb0e4c9e83197919cabdf316242b57935c2b 100644 --- a/goldfish/liii/string.scm +++ b/goldfish/liii/string.scm @@ -26,7 +26,7 @@ string-take string-take-right string-drop string-drop-right string-pad string-pad-right string-trim string-trim-right string-trim-both - string-index string-index-right + string-index string-index-right string-skip string-skip-right string-contains string-count string-upcase string-downcase string-fold string-fold-right string-for-each-index diff --git a/goldfish/srfi/srfi-113.scm b/goldfish/srfi/srfi-113.scm index 62b05ba7bff70c0df016b102e8cb6a2822e8003d..713515ae9707f12db5ce1579a7efad2096132a7f 100644 --- a/goldfish/srfi/srfi-113.scm +++ b/goldfish/srfi/srfi-113.scm @@ -1,4 +1,6 @@ -; Copyright (C) John Cowan (2015). All Rights Reserved. +;;;; SPDX-FileCopyrightText: 2013 John Cowan +;;;; +;;;; SPDX-License-Identifier: MIT ; ; Permission is hereby granted, free of charge, to any person obtaining a copy of ; this software and associated documentation files (the "Software"), to deal in @@ -20,10 +22,1030 @@ ; (define-library (srfi srfi-113) - (import (scheme base)) - (export set?) + (import (scheme base) + (scheme case-lambda) + (liii hash-table) + (liii error) + (srfi srfi-1) + (srfi srfi-128)) + (export set set-unfold list->set list->set! set-copy set->list + set? set-contains? set-empty? set-disjoint? + set-element-comparator set-size + set=? set? set<=? set>=? + set-any? set-every? set-find set-count set-member set-search! set-map + set-for-each set-fold set-filter set-filter! set-remove set-remove! + set-partition set-partition! set-union set-intersection set-difference set-xor + set-union! set-intersection! set-difference! set-xor! + set-adjoin set-adjoin! set-replace set-replace! + set-delete set-delete! set-delete-all set-delete-all! + bag bag-unfold bag-member bag-comparator bag->list + bag-copy list->bag list->bag! + bag? bag-contains? bag-empty? bag-disjoint? + bag-size bag-find bag-count bag-any? bag-every? + bag=? bag? bag<=? bag>=? + bag-union bag-intersection bag-difference bag-xor + bag-union! bag-intersection! bag-difference! bag-xor! + bag-adjoin bag-adjoin! bag-replace bag-replace! + bag-delete bag-delete! bag-delete-all bag-delete-all! + bag-search!) (begin + (define-record-type set-impl + (%make-set hash-table comparator) + set? + (hash-table set-hash-table) + (comparator set-element-comparator)) + + (define (check-set obj) + (if (not (set? obj)) (type-error "not a set" obj))) + + (define (check-same-comparator a b) + (if (not (eq? (set-element-comparator a) (set-element-comparator b))) + (value-error "different comparators" a b))) + + (define (make-set/comparator comparator) + (%make-set (make-hash-table comparator) comparator)) + + (define (set-add! s element) + (hash-table-set! (set-hash-table s) element element)) + + (define (set comparator . elements) + (let ((result (make-set/comparator comparator))) + (for-each (lambda (x) (set-add! result x)) elements) + result)) + + (define (list->set comparator elements) + (apply set comparator elements)) + + (define (list->set! s elements) + (check-set s) + (for-each (lambda (x) (set-add! s x)) elements) + s) + + (define (set-unfold stop? mapper successor seed comparator) + (let ((result (make-set/comparator comparator))) + (let loop ((seed seed)) + (if (stop? seed) + result + (begin + (set-add! result (mapper seed)) + (loop (successor seed))))))) + + (define (set-copy s) + (check-set s) + (list->set (set-element-comparator s) (hash-table-keys (set-hash-table s)))) + + (define (set->list s) + (check-set s) + (hash-table-keys (set-hash-table s))) + + + (define (set-size s) + (check-set s) + (hash-table-size (set-hash-table s))) + + + (define (set-contains? s member) + (check-set s) + (hash-table-contains? (set-hash-table s) member)) + + (define (set-empty? s) + (check-set s) + (hash-table-empty? (set-hash-table s))) + + (define (set-disjoint? a b) + (check-set a) + (check-set b) + (check-same-comparator a b) + (let ((na (set-size a)) + (nb (set-size b))) + (if (< na nb) + (not (any-in-other? a b)) + (not (any-in-other? b a))))) + + (define (any-in-other? small big) + (let ((ht-small (set-hash-table small)) + (ht-big (set-hash-table big))) + (call/cc (lambda (return) + (hash-table-for-each + (lambda (k v) + (if (hash-table-contains? ht-big k) (return #t))) + ht-small) + #f)))) + + + (define (binary-set<=? s1 s2) + (check-set s1) + (check-set s2) + (check-same-comparator s1 s2) + (let ((n1 (set-size s1)) + (n2 (set-size s2))) + (cond + ((> n1 n2) #f) + (else + (let ((ht1 (set-hash-table s1)) + (ht2 (set-hash-table s2))) + (call/cc + (lambda (return) + (hash-table-for-each + (lambda (k v) + (unless (hash-table-contains? ht2 k) + (return #f))) + ht1) + #t))))))) + + (define (set<=? . sets) + (if (null? sets) + #t + (let loop ((head (car sets)) (tail (cdr sets))) + (if (null? tail) + #t + (let ((next (car tail))) + (and (binary-set<=? head next) + (loop next (cdr tail)))))))) + + (define (set=? . sets) + (if (null? sets) + #t + (let loop ((head (car sets)) (tail (cdr sets))) + (if (null? tail) + #t + (let ((next (car tail))) + (and (binary-set=? head next) + (loop next (cdr tail)))))))) + + (define (binary-set=? s1 s2) + (check-set s1) + (check-set s2) + (check-same-comparator s1 s2) + (let ((n1 (set-size s1)) + (n2 (set-size s2))) + (and (= n1 n2) + (binary-set<=? s1 s2)))) + + (define (set=? . sets) + (if (null? sets) + #t + (let loop ((head (car sets)) (tail (cdr sets))) + (if (null? tail) + #t + (let ((next (car tail))) + (and (binary-set<=? next head) + (loop next (cdr tail)))))))) + + (define (set>? . sets) + (if (null? sets) + #t + (let loop ((head (car sets)) (tail (cdr sets))) + (if (null? tail) + #t + (let ((next (car tail))) + (and (binary-set= count 0)) + (type-error "bag-increment!" count)) + (if (= count 0) + bag + (let* ((entries (bag-entries bag)) + (entry (hash-table-ref/default entries element 0))) + (hash-table-set! entries element (+ count entry)) + bag))) + + (define (bag-decrement! bag element count) + (check-bag bag) + (if (not (and (exact-integer? count) (>= count 0))) + (type-error "bag-decrement!" count)) + (if (= count 0) + bag + (let* ((entries (bag-entries bag)) + (entry (hash-table-ref/default entries element 0))) + (if (> entry count) + (hash-table-set! entries element (- entry count)) + (hash-table-delete! entries element)) + bag))) + + (define (bag-contains? bag element) + (check-bag bag) + (hash-table-contains? (bag-entries bag) element)) + + (define (bag-empty? bag) + (check-bag bag) + (hash-table-empty? (bag-entries bag))) + + (define (bag-disjoint? a b) + (check-bag a) + (check-bag b) + (let ((entries-a (bag-entries a)) + (entries-b (bag-entries b))) + (call/cc + (lambda (return) + (hash-table-for-each + (lambda (k entry) + (when (hash-table-contains? entries-b k) + (return #f))) + entries-a) + #t)))) + + (define (bag comparator . elements) + (let ((result (make-bag/comparator comparator))) + (for-each (lambda (x) (bag-increment! result x 1)) elements) + result)) + + (define (bag-unfold stop? mapper successor seed comparator) + (let ((result (make-bag/comparator comparator))) + (let loop ((seed seed)) + (if (stop? seed) + result + (begin + (bag-increment! result (mapper seed) 1) + (loop (successor seed))))))) + + (define (list->bag comparator elements) + (apply bag comparator elements)) + + (define (list->bag! bag elements) + (check-bag bag) + (for-each (lambda (x) (bag-increment! bag x 1)) elements) + bag) + + (define (bag-copy bag) + (check-bag bag) + (let ((entries (make-hash-table (bag-comparator bag)))) + (hash-table-for-each + (lambda (k entry) + (hash-table-set! entries k entry)) + (bag-entries bag)) + (%make-bag entries (bag-comparator bag)))) + + + (define (bag-member bag element default) + (check-bag bag) + (if (hash-table-contains? (bag-entries bag) element) + element + default)) + + (define (bag->list bag) + (check-bag bag) + (let ((result '())) + (hash-table-for-each + (lambda (k entry) + (let loop ((i 0)) + (when (< i entry) + (set! result (cons k result)) + (loop (+ i 1))))) + (bag-entries bag)) + result)) + + (define (bag-size bag) + (bag-count (lambda (x) #t) bag)) + + (define (bag-find predicate bag failure) + (check-bag bag) + (let ((found (find predicate (hash-table-keys (bag-entries bag))))) + (or found (failure)))) + + (define (bag-count predicate bag) + (check-bag bag) + (let ((entries (bag-entries bag))) + (hash-table-fold + (lambda (k entry acc) + (if (predicate k) + (+ acc entry) + acc)) + 0 + entries))) + + (define (bag-any? predicate bag) + (check-bag bag) + (let ((found + (hash-table-find + (lambda (k entry) (predicate k)) + (bag-entries bag) + #f))) + (if found #t #f))) + + (define (bag-every? predicate bag) + (check-bag bag) + (let ((found + (hash-table-find + (lambda (k entry) (not (predicate k))) + (bag-entries bag) + #f))) + (if found #f #t))) + + (define (bag<=? . bags) + (if (null? bags) + #t + (let loop ((head (car bags)) (tail (cdr bags))) + (if (null? tail) + #t + (let ((next (car tail))) + (and (binary-bag<=? head next) + (loop next (cdr tail)))))))) + + (define (bag=? . bags) + (if (null? bags) + #t + (let loop ((head (car bags)) (tail (cdr bags))) + (if (null? tail) + #t + (let ((next (car tail))) + (and (binary-bag=? head next) + (loop next (cdr tail)))))))) + + (define (binary-bag=? b1 b2) + (check-bag b1) + (check-bag b2) + (check-same-bag-comparator b1 b2) + (let ((e1 (bag-entries b1)) + (e2 (bag-entries b2))) + (and (= (hash-table-size e1) (hash-table-size e2)) + (call/cc + (lambda (return) + (hash-table-for-each + (lambda (k count1) + (if (not (= count1 (hash-table-ref/default e2 k 0))) + (return #f))) + e1) + #t))))) + + (define (binary-bag<=? b1 b2) + (check-bag b1) + (check-bag b2) + (check-same-bag-comparator b1 b2) + (let ((e1 (bag-entries b1)) + (e2 (bag-entries b2))) + (if (> (hash-table-size e1) (hash-table-size e2)) + #f + (call/cc + (lambda (return) + (hash-table-for-each + (lambda (k count1) + (if (not (<= count1 (hash-table-ref/default e2 k 0))) + (return #f))) + e1) + #t))))) + + (define (bag=? . bags) + (if (null? bags) + #t + (let loop ((head (car bags)) (tail (cdr bags))) + (if (null? tail) + #t + (let ((next (car tail))) + (and (binary-bag<=? next head) + (loop next (cdr tail)))))))) + + (define (bag>? . bags) + (if (null? bags) + #t + (let loop ((head (car bags)) (tail (cdr bags))) + (if (null? tail) + #t + (let ((next (car tail))) + (and (binary-bag count2 count1) + (hash-table-set! result-entries k count2)))) + other-entries) + result)) + + (define (bag-intersection bag1 . bags) + (check-bag bag1) + (for-each + (lambda (b) + (check-bag b) + (check-same-bag-comparator bag1 b)) + bags) + (let ((result (make-bag/comparator (bag-comparator bag1))) + (entries1 (bag-entries bag1)) + (other-entries (map bag-entries bags))) + (define (min-count key count1) + (let loop ((rest other-entries) (minc count1)) + (if (null? rest) + minc + (loop (cdr rest) + (min minc (hash-table-ref/default (car rest) key 0)))))) + (hash-table-for-each + (lambda (k count1) + (let ((m (min-count k count1))) + (when (> m 0) + (hash-table-set! (bag-entries result) k m)))) + entries1) + result)) + + (define (bag-difference bag1 . bags) + (check-bag bag1) + (for-each + (lambda (b) + (check-bag b) + (check-same-bag-comparator bag1 b)) + bags) + (let ((result (make-bag/comparator (bag-comparator bag1))) + (entries1 (bag-entries bag1)) + (other-entries (map bag-entries bags))) + (define (sub-count key count1) + (let loop ((rest other-entries) (acc count1)) + (if (null? rest) + acc + (loop (cdr rest) + (- acc (hash-table-ref/default (car rest) key 0)))))) + (hash-table-for-each + (lambda (k count1) + (let ((r (sub-count k count1))) + (when (> r 0) + (hash-table-set! (bag-entries result) k r)))) + entries1) + result)) + + (define (bag-xor bag1 bag2) + (check-bag bag1) + (check-bag bag2) + (check-same-bag-comparator bag1 bag2) + (let ((result (make-bag/comparator (bag-comparator bag1))) + (e1 (bag-entries bag1)) + (e2 (bag-entries bag2))) + (hash-table-for-each + (lambda (k count2) + (let ((count1 (hash-table-ref/default e1 k 0))) + (when (= count1 0) + (hash-table-set! (bag-entries result) k count2)))) + e2) + (hash-table-for-each + (lambda (k count1) + (let* ((count2 (hash-table-ref/default e2 k 0)) + (diff (abs (- count1 count2)))) + (when (> diff 0) + (hash-table-set! (bag-entries result) k diff)))) + e1) + result)) + + (define (bag-union! bag1 . bags) + (check-bag bag1) + (for-each + (lambda (b) + (check-bag b) + (check-same-bag-comparator bag1 b) + (bag-union-into! bag1 b)) + bags) + bag1) + + (define (bag-intersection! bag1 . bags) + (check-bag bag1) + (for-each + (lambda (b) + (check-bag b) + (check-same-bag-comparator bag1 b)) + bags) + (let ((entries1 (bag-entries bag1)) + (other-entries (map bag-entries bags))) + (define (min-count key count1) + (let loop ((rest other-entries) (minc count1)) + (if (null? rest) + minc + (loop (cdr rest) + (min minc (hash-table-ref/default (car rest) key 0)))))) + (hash-table-for-each + (lambda (k count1) + (let ((m (min-count k count1))) + (if (> m 0) + (hash-table-set! entries1 k m) + (hash-table-delete! entries1 k)))) + entries1) + bag1)) + + (define (bag-difference! bag1 . bags) + (check-bag bag1) + (for-each + (lambda (b) + (check-bag b) + (check-same-bag-comparator bag1 b)) + bags) + (let ((entries1 (bag-entries bag1)) + (other-entries (map bag-entries bags))) + (define (sub-count key count1) + (let loop ((rest other-entries) (acc count1)) + (if (null? rest) + acc + (loop (cdr rest) + (- acc (hash-table-ref/default (car rest) key 0)))))) + (hash-table-for-each + (lambda (k count1) + (let ((r (sub-count k count1))) + (if (> r 0) + (hash-table-set! entries1 k r) + (hash-table-delete! entries1 k)))) + entries1) + bag1)) + + (define (bag-xor! bag1 bag2) + (check-bag bag1) + (check-bag bag2) + (check-same-bag-comparator bag1 bag2) + (let ((result (bag-xor bag1 bag2))) + (set-bag-entries! bag1 (bag-entries result)) + bag1)) + + (define (bag-adjoin! bag . elements) + (check-bag bag) + (for-each (lambda (x) (bag-increment! bag x 1)) elements) + bag) + + (define (bag-adjoin bag . elements) + (check-bag bag) + (let ((result (bag-copy bag))) + (for-each (lambda (x) (bag-increment! result x 1)) elements) + result)) + + (define (bag-replace! bag element) + (check-bag bag) + (let ((entries (bag-entries bag))) + (when (hash-table-contains? entries element) + (let ((count (hash-table-ref/default entries element 0))) + (hash-table-delete! entries element) + (hash-table-set! entries element count)))) + bag) + + (define (bag-replace bag element) + (check-bag bag) + (if (bag-contains? bag element) + (let ((result (bag-copy bag))) + (bag-replace! result element) + result) + bag)) + + (define (bag-delete! bag . elements) + (check-bag bag) + (for-each (lambda (x) (bag-decrement! bag x 1)) elements) + bag) + + (define (bag-delete bag . elements) + (apply bag-delete! (bag-copy bag) elements)) + + (define (bag-delete-all! bag element-list) + (check-bag bag) + (for-each (lambda (x) (bag-decrement! bag x 1)) element-list) + bag) + + (define (bag-delete-all bag element-list) + (bag-delete-all! (bag-copy bag) element-list)) + + (define (bag-search! bag element failure success) + (check-bag bag) + (let* ((comp (bag-comparator bag)) + (same? (comparator-equality-predicate comp)) + (entries (bag-entries bag)) + (not-found (list 'not-found)) + (found (call/cc + (lambda (return) + (hash-table-for-each + (lambda (k entry) + (when (same? k element) (return k))) + entries) + not-found)))) + (if (eq? found not-found) + (failure (lambda (obj) + (bag-increment! bag element 1) + (values bag obj)) + (lambda (obj) + (values bag obj))) + (success found + (lambda (new-element obj) + (bag-decrement! bag found 1) + (bag-increment! bag new-element 1) + (values bag obj)) + (lambda (obj) + (bag-decrement! bag found 1) + (values bag obj)))))) + ) ; end of begin ) ; end of define-library - diff --git a/goldfish/srfi/srfi-125.scm b/goldfish/srfi/srfi-125.scm index fe99656e97c5e261fcbda34acc4957f4e8562077..690540b7de44c54437e00da7276c937d5fd3e82f 100644 --- a/goldfish/srfi/srfi-125.scm +++ b/goldfish/srfi/srfi-125.scm @@ -15,13 +15,14 @@ ; (define-library (srfi srfi-125) - (import (srfi srfi-1) (liii base) (liii error)) + (import (srfi srfi-1) (srfi srfi-128) (liii base) (liii error)) (export make-hash-table hash-table hash-table-unfold alist->hash-table hash-table? hash-table-contains? hash-table-empty? hash-table=? hash-table-mutable? hash-table-ref hash-table-ref/default hash-table-set! hash-table-delete! hash-table-intern! hash-table-update! hash-table-update!/default hash-table-pop! hash-table-clear! hash-table-size hash-table-keys hash-table-values hash-table-entries hash-table-find - hash-table-count hash-table-for-each hash-table-map->list hash-table->alist) + hash-table-count hash-table-fold hash-table-for-each hash-table-map->list + hash-table->alist) (begin (define (assert-hash-table-type ht f) @@ -128,6 +129,15 @@ (typed-lambda ((pred? procedure?) (ht hash-table?)) (count (lambda (x) (pred? (car x) (cdr x))) (map values ht)))) + (define (hash-table-fold proc seed ht) + (assert-hash-table-type ht hash-table-fold) + (let ((result seed)) + (hash-table-for-each + (lambda (k v) + (set! result (proc k v result))) + ht) + result)) + (define hash-table-for-each (typed-lambda ((proc procedure?) (ht hash-table?)) (for-each (lambda (x) (proc (car x) (cdr x))) ht))) @@ -139,4 +149,3 @@ (define hash-table->alist (typed-lambda ((ht hash-table?)) (append-map (lambda (x) (list (car x) (cdr x))) (map values ht)))))) - diff --git a/goldfish/srfi/srfi-13.scm b/goldfish/srfi/srfi-13.scm index c3d3073c03adfa3f182b954f554ed9f9f492c50c..51bfc6d3c3e8b5301eb6859bbd0e1907c1e33b41 100644 --- a/goldfish/srfi/srfi-13.scm +++ b/goldfish/srfi/srfi-13.scm @@ -18,7 +18,7 @@ (import (liii base) (srfi srfi-1)) (export string-null? string-copy string-join string-every string-any string-take string-take-right string-drop string-drop-right string-pad string-pad-right string-trim string-trim-right - string-trim-both string-prefix? string-suffix? string-index string-index-right + string-trim-both string-prefix? string-suffix? string-index string-index-right string-skip string-skip-right string-contains string-count string-upcase string-downcase string-fold string-fold-right string-for-each-index string-reverse string-tokenize) (begin @@ -267,6 +267,34 @@ (ret (string-index-right-sub str-sub pred?))) (if ret (+ start ret) ret))) + (define (string-skip str char/pred? . start+end) + (define (string-skip-sub str pred?) + (let loop ((i 0)) + (cond ((>= i (string-length str)) #f) + ((pred? (string-ref str i)) (loop (+ i 1))) + (else i)))) + (let* ((start (if (null-list? start+end) + 0 + (car start+end))) + (str-sub (%string-from-range str start+end)) + (pred? (%make-criterion char/pred?)) + (ret (string-skip-sub str-sub pred?))) + (if ret (+ start ret) ret))) + + (define (string-skip-right str char/pred? . start+end) + (define (string-skip-right-sub str pred?) + (let loop ((i (- (string-length str) 1))) + (cond ((< i 0) #f) + ((pred? (string-ref str i)) (loop (- i 1))) + (else i)))) + (let* ((start (if (null-list? start+end) + 0 + (car start+end))) + (str-sub (%string-from-range str start+end)) + (pred? (%make-criterion char/pred?)) + (ret (string-skip-right-sub str-sub pred?))) + (if ret (+ start ret) ret))) + (define (string-contains str sub-str) (let loop ((i 0)) (let ((len (string-length str)) diff --git a/goldfish/srfi/srfi-267.scm b/goldfish/srfi/srfi-267.scm new file mode 100644 index 0000000000000000000000000000000000000000..767cd942497d43b4dff270a8f6ddd83956cb9cf5 --- /dev/null +++ b/goldfish/srfi/srfi-267.scm @@ -0,0 +1,73 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +;; Acknowledgements +;; +;; This implementation is based on: +;; +;; 1. SRFI-267: Raw String Syntax by Peter McGoron +;; https://srfi.schemers.org/srfi-267/ +;; Copyright © 2025-2026 Peter McGoron +;; Permission granted under the MIT-style license +;; +;; 2. Syntax #"" proposed by John Cowan +;; https://codeberg.org/scheme/r7rs/issues/32#issuecomment-8863095 +;; in Scheme raw string discussions + +(define-library (srfi srfi-267) + (export read-raw-string) + (begin + + (define (read-raw-string S) + ;; This parser starts reading after `"`. + ;; In the given examples, the parser starts at the dot: + ;; + ;; #"."asdf"" + ;; #"--."#"()""--" + (define (read-char* location) + (let ((ch (read-char))) ; (read-char (current-input-string)) + (if (eof-object? ch) + (error (list "eof in raw string literal" location)) + ch))) + (define delimiter + ;; always expect `"` + (let ((ch (read-char* 'delim))) + (append (string->list S) (list ch)))) + (call-with-port (open-output-string) + (lambda (out) + (define (read-delimiter n rest-of-delimiter) + (if (null? rest-of-delimiter) + (get-output-string out) + (let ((ch (read-char* 'check))) + (if (char=? ch (car rest-of-delimiter)) + (read-delimiter (+ n 1) (cdr rest-of-delimiter)) + (do ((n n (- n 1)) + (delimiter delimiter (cdr delimiter))) + ((zero? n) (read-raw ch)) + (write-char (car delimiter) out)))))) + (define (read-raw ch) + (if (char=? ch (car delimiter)) + (read-delimiter 1 (cdr delimiter)) + (begin (write-char ch out) + (read-raw (read-char* 'read))))) + (read-raw (read-char* 'read))))) + + (set! *#readers* + (cons (cons #\" read-raw-string) + *#readers*)) + + ) ; end of begin + ) ; end of library diff --git a/pkgs/goldfish.nix b/pkgs/goldfish.nix index 11cf1424608d93fdbc58ecd56916d027c450a4be..03933df8bc860d92b3ba82119da6580b3b4117c1 100644 --- a/pkgs/goldfish.nix +++ b/pkgs/goldfish.nix @@ -14,7 +14,7 @@ stdenv.mkDerivation { pname = "goldfish"; - version = "17.11.23"; + version = "17.11.24"; src = ./..; diff --git a/src/goldfish.hpp b/src/goldfish.hpp index a12f02e6b1210efd88051d9593a43855891642f4..960819011ce4b68ad772ebf995018f3e971dfbc4 100644 --- a/src/goldfish.hpp +++ b/src/goldfish.hpp @@ -51,12 +51,14 @@ #endif #endif +#include + #ifdef GOLDFISH_WITH_REPL #include #include #endif -#define GOLDFISH_VERSION "17.11.23" +#define GOLDFISH_VERSION "17.11.24" #define GOLDFISH_PATH_MAXN TB_PATH_MAXN @@ -69,6 +71,295 @@ using std::endl; using std::string; using std::vector; +static s7_pointer +response2hashtable (s7_scheme* sc, cpr::Response r) { + s7_pointer ht= s7_make_hash_table (sc, 8); + s7_hash_table_set (sc, ht, s7_make_symbol (sc, "status-code"), s7_make_integer (sc, r.status_code)); + s7_hash_table_set (sc, ht, s7_make_symbol (sc, "url"), s7_make_string (sc, r.url.c_str())); + s7_hash_table_set (sc, ht, s7_make_symbol(sc, "elapsed"), s7_make_real (sc, r.elapsed)); + s7_hash_table_set (sc, ht, s7_make_symbol (sc, "text"), s7_make_string (sc, r.text.c_str ())); + s7_hash_table_set (sc, ht, s7_make_symbol (sc, "reason"), s7_make_string (sc, r.reason.c_str ())); + s7_pointer headers= s7_make_hash_table(sc, r.header.size()); + for (const auto &header : r.header) { + const auto key= header.first.c_str (); + std::string key_lower = header.first; + std::transform(key_lower.begin(), key_lower.end(), + key_lower.begin(), ::tolower); + const auto value= header.second.c_str (); + s7_hash_table_set(sc, headers, + s7_make_string(sc, key_lower.c_str()), + s7_make_string(sc, value)); + } + s7_hash_table_set (sc, ht, s7_make_symbol(sc, "headers"), headers); + + return ht; +} + +inline cpr::Parameters +to_cpr_parameters (s7_scheme* sc, s7_pointer args) { + cpr::Parameters params= cpr::Parameters{}; + if (s7_is_list(sc, args)) { + s7_pointer iter= args; + while (!s7_is_null (sc, iter)) { + s7_pointer pair= s7_car (iter); + if (s7_is_pair (pair)) { + const char* key= s7_string (s7_car (pair)); + const char* value= s7_string (s7_cdr (pair)); + params.Add (cpr::Parameter (string (key), string (value))); + } + iter= s7_cdr (iter); + } + } + return params; +} + +inline cpr::Header +to_cpr_headers (s7_scheme* sc, s7_pointer args) { + cpr::Header headers= cpr::Header{}; + if (s7_is_list(sc, args)) { + s7_pointer iter= args; + while (!s7_is_null (sc, iter)) { + s7_pointer pair= s7_car (iter); + if (s7_is_pair (pair)) { + const char* key= s7_string (s7_car (pair)); + const char* value= s7_string (s7_cdr (pair)); + headers.insert (std::make_pair (key, value)); + } + iter= s7_cdr (iter); + } + } + return headers; +} + +inline cpr::Proxies +to_cpr_proxies (s7_scheme* sc, s7_pointer args) { + std::map proxy_map; + if (s7_is_list(sc, args)) { + s7_pointer iter= args; + while (!s7_is_null (sc, iter)) { + s7_pointer pair= s7_car (iter); + if (s7_is_pair (pair)) { + const char* key= s7_string (s7_car (pair)); + const char* value= s7_string (s7_cdr (pair)); + proxy_map[key] = value; + } + iter= s7_cdr (iter); + } + } + return cpr::Proxies(proxy_map); +} +static s7_pointer +f_http_head (s7_scheme* sc, s7_pointer args) { + const char* url= s7_string (s7_car (args)); + cpr::Session session; + session.SetUrl (cpr::Url (url)); + cpr::Response r= session.Head (); + return response2hashtable (sc, r); +} + +inline void +glue_http_head (s7_scheme* sc) { + s7_pointer cur_env= s7_curlet (sc); + const char* s_http_head = "g_http-head"; + const char* d_http_head = "(g_http-head url ...) => hash-table?"; + auto func_http_head= s7_make_typed_function (sc, s_http_head, f_http_head, 1, 0, false, d_http_head, NULL); + s7_define (sc, cur_env, s7_make_symbol (sc, s_http_head), func_http_head); +} + +static s7_pointer +f_http_get (s7_scheme* sc, s7_pointer args) { + const char* url= s7_string (s7_car (args)); + s7_pointer params= s7_cadr (args); + cpr::Parameters cpr_params= to_cpr_parameters(sc, params); + s7_pointer headers= s7_caddr (args); + cpr::Header cpr_headers= to_cpr_headers (sc, headers); + s7_pointer proxy= s7_cadddr (args); + cpr::Proxies cpr_proxies= to_cpr_proxies(sc, proxy); + + cpr::Session session; + session.SetUrl (cpr::Url (url)); + session.SetParameters (cpr_params); + session.SetHeader (cpr_headers); + if (s7_is_list(sc, proxy) && !s7_is_null(sc, proxy)) { + session.SetProxies(cpr_proxies); + } + + cpr::Response r= session.Get (); + return response2hashtable (sc, r); +} + +inline void +glue_http_get (s7_scheme* sc) { + s7_pointer cur_env= s7_curlet (sc); + const char* s_http_get= "g_http-get"; + const char* d_http_get= "(g_http-get url params headers proxy) => hash-table?"; + auto func_http_get= s7_make_typed_function (sc, s_http_get, f_http_get, 4, 0, false, d_http_get, NULL); + s7_define (sc, cur_env, s7_make_symbol (sc, s_http_get), func_http_get); +} + +static s7_pointer +f_http_post (s7_scheme* sc, s7_pointer args) { + const char* url= s7_string (s7_car (args)); + s7_pointer params= s7_cadr (args); + cpr::Parameters cpr_params= to_cpr_parameters(sc, params); + const char* body= s7_string (s7_caddr (args)); + cpr::Body cpr_body= cpr::Body (body); + s7_pointer headers= s7_cadddr (args); + cpr::Header cpr_headers= to_cpr_headers (sc, headers); + s7_pointer proxy= s7_car (s7_cddddr (args)); + cpr::Proxies cpr_proxies= to_cpr_proxies (sc, proxy); + + cpr::Session session; + session.SetUrl (cpr::Url (url)); + session.SetParameters (cpr_params); + session.SetBody (cpr_body); + session.SetHeader (cpr_headers); + if (s7_is_list(sc, proxy) && !s7_is_null(sc, proxy)) { + session.SetProxies(cpr_proxies); + } + + cpr::Response r= session.Post (); + return response2hashtable (sc, r); +} + +inline void +glue_http_post (s7_scheme* sc) { + s7_pointer cur_env= s7_curlet (sc); + const char* name= "g_http-post"; + const char* doc= "(g_http-post url params body headers proxy) => hash-table?"; + auto func_http_post= s7_make_typed_function (sc, name, f_http_post, 5, 0, false, doc, NULL); + s7_define (sc, cur_env, s7_make_symbol (sc, name), func_http_post); +} + +static s7_pointer +f_http_stream_get (s7_scheme* sc, s7_pointer args) { + const char* url = s7_string (s7_car (args)); + s7_pointer params = s7_cadr (args); + s7_pointer proxy = s7_caddr (args); + s7_pointer userdata = s7_cadddr (args); + s7_pointer callback = s7_car(s7_cddddr(args)); + + cpr::Parameters cpr_params = to_cpr_parameters(sc, params); + cpr::Proxies cpr_proxies = to_cpr_proxies(sc, proxy); + + cpr::Session session; + session.SetUrl (cpr::Url (url)); + session.SetParameters (cpr_params); + if (s7_is_list(sc, proxy) && !s7_is_null(sc, proxy)) { + session.SetProxies (cpr_proxies); + } + + session.SetWriteCallback(cpr::WriteCallback{[sc, callback](const std::string_view& data, intptr_t cpr_userdata) -> bool { + // Retrieve userdata from intptr_t + s7_pointer userdata_ptr = (s7_pointer)cpr_userdata; + + // Call the scheme callback inline + s7_pointer data_str = s7_make_string_with_length(sc, data.data(), data.length()); + s7_pointer args = s7_cons(sc, data_str, s7_cons(sc, userdata_ptr, s7_nil(sc))); + + s7_pointer ret = s7_call(sc, callback, args); + if (s7_is_boolean(ret)) { + return s7_boolean(sc, ret); + } + + return true; // Continue receiving + }, reinterpret_cast(userdata)}); + + try { + cpr::Response response = session.Get(); + } catch (const std::exception& e) { + return s7_make_integer(sc, 500); // Error case + } + return s7_undefined(sc); +} + +inline void +glue_http_stream_get (s7_scheme* sc) { + s7_pointer cur_env= s7_curlet (sc); + const char* s_stream_get = "g_http-stream-get"; + const char* d_stream_get = "(g_http-stream-get url params proxy userdata callback) => undefined"; + auto func_stream_get = s7_make_typed_function (sc, s_stream_get, f_http_stream_get, 5, 0, false, d_stream_get, NULL); + s7_define (sc, cur_env, s7_make_symbol (sc, s_stream_get), func_stream_get); +} + +static s7_pointer +f_http_stream_post (s7_scheme* sc, s7_pointer args) { + s7_pointer url_arg = s7_car (args); + s7_pointer params = s7_cadr (args); + s7_pointer body_arg = s7_caddr (args); + s7_pointer headers = s7_cadddr (args); + s7_pointer proxy = s7_car(s7_cddddr(args)); + s7_pointer userdata = s7_cadr(s7_cddddr(args)); + s7_pointer callback = s7_list_ref(sc, args, 6); + + const char* url = s7_string(url_arg); + const char* body = s7_string(body_arg); + + cpr::Parameters cpr_params = to_cpr_parameters(sc, params); + cpr::Header cpr_headers = to_cpr_headers(sc, headers); + cpr::Proxies cpr_proxies = to_cpr_proxies(sc, proxy); + + cpr::Session session; + session.SetUrl (cpr::Url (url)); + session.SetParameters (cpr_params); + session.SetBody (cpr::Body (body)); + session.SetHeader (cpr_headers); + if (s7_is_list(sc, proxy) && !s7_is_null(sc, proxy)) { + session.SetProxies (cpr_proxies); + } + + + // Store userdata in s7 managed memory to prevent GC + s7_pointer userdata_loc = s7_make_c_pointer(sc, (void*)userdata); + + session.SetWriteCallback(cpr::WriteCallback{[sc, callback](const std::string_view& data, intptr_t cpr_userdata) -> bool { + // Retrieve userdata from intptr_t + s7_pointer userdata_ptr = (s7_pointer)cpr_userdata; + + // Call the scheme callback inline + s7_pointer data_str = s7_make_string_with_length(sc, data.data(), data.length()); + s7_pointer args = s7_cons(sc, data_str, s7_cons(sc, userdata_ptr, s7_nil(sc))); + + s7_pointer ret = s7_call(sc, callback, args); + if (s7_is_boolean(ret)) { + return s7_boolean(sc, ret); + } + + return true; // Continue receiving + }, reinterpret_cast(userdata)}); + + try { + cpr::Response response = session.Post(); + } catch (const std::exception& e) { + return s7_make_integer(sc, 500); // Error case + } + return s7_undefined(sc); +} + +inline void +glue_http_stream_post (s7_scheme* sc) { + s7_pointer cur_env= s7_curlet (sc); + + const char* s_stream_post = "g_http-stream-post"; + const char* d_stream_post = "(g_http-stream-post url params body headers proxy userdata callback) => undefined"; + auto func_stream_post = s7_make_typed_function (sc, s_stream_post, f_http_stream_post, 7, 0, false, d_stream_post, NULL); + s7_define (sc, cur_env, s7_make_symbol (sc, s_stream_post), func_stream_post); +} + +inline void +glue_http (s7_scheme* sc) { + glue_http_head (sc); + glue_http_get (sc); + glue_http_post (sc); +} + +inline void +glue_http_stream (s7_scheme* sc) { + glue_http_stream_get(sc); + glue_http_stream_post(sc); +} + + inline s7_pointer string_vector_to_s7_vector (s7_scheme* sc, vector v) { int N = v.size (); @@ -1105,6 +1396,8 @@ glue_for_community_edition (s7_scheme* sc) { glue_liii_datetime (sc); glue_liii_uuid (sc); glue_liii_hashlib (sc); + glue_http (sc); + glue_http_stream (sc); } static void @@ -1775,3 +2068,4 @@ repl_for_community_edition (s7_scheme* sc, int argc, char** argv) { } } // namespace goldfish + diff --git a/src/goldfish_repl.cpp b/src/goldfish_repl.cpp index 67cae47d81cf8f4011105c9ce63411722f13494e..6875e90a7ba3f8a5325188e8832b1a9e6b63f626 100644 --- a/src/goldfish_repl.cpp +++ b/src/goldfish_repl.cpp @@ -75,6 +75,7 @@ eval_string (const char* code) { s7_eval_c_string (wasm_sc, "(load \"liii/cut.scm\")"); s7_eval_c_string (wasm_sc, "(load \"liii/datetime.scm\")"); s7_eval_c_string (wasm_sc, "(load \"liii/either.scm\")"); + s7_eval_c_string (wasm_sc, "(load \"liii/rich-either.scm\")"); s7_eval_c_string (wasm_sc, "(load \"liii/error.scm\")"); s7_eval_c_string (wasm_sc, "(load \"liii/hash-table.scm\")"); s7_eval_c_string (wasm_sc, "(load \"liii/lang.scm\")"); diff --git a/tests/goldfish/liii/bag-test.scm b/tests/goldfish/liii/bag-test.scm new file mode 100644 index 0000000000000000000000000000000000000000..1ede2390e0fec53b12edfe7e1cbdba728b800168 --- /dev/null +++ b/tests/goldfish/liii/bag-test.scm @@ -0,0 +1,800 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(import (scheme base) + (liii check) + (liii bag) + (liii error)) + +(check-set-mode! 'report-failed) + +;; --- Data Setup --- +(define b-empty (bag)) +(define comp (bag-comparator b-empty)) + +#| +bag +创建一个新的 bag。 + +语法 +---- +(bag element ...) + +参数 +---- +element ... : any +初始元素。 + +返回值 +----- +返回包含指定元素的 bag。 +|# +(define b-1-2 (bag 1 2 2)) +(define b-list (bag->list b-1-2)) +(check (bag-member b-1-2 1 #f) => 1) +(check (bag-member b-1-2 2 #f) => 2) +(check (bag-member b-1-2 3 'none) => 'none) +(check-true (eq? (bag-comparator b-1-2) comp)) +(check (bag-member b-empty 1 'missing) => 'missing) + +;; bag->list should include duplicates +(check-false (not (member 1 b-list))) +(check-false (not (member 2 b-list))) +(check (length b-list) => 3) + +#| +bag-copy +复制一个 bag。 + +语法 +---- +(bag-copy bag) + +参数 +---- +bag : bag +目标 bag。 + +返回值 +----- +返回一个新的 bag,包含原 bag 的所有元素,比较器相同。 +|# +(let ((copy (bag-copy b-1-2))) + (check-true (bag? copy)) + (check-false (eq? copy b-1-2)) + (check-true (eq? (bag-comparator copy) comp)) + (check (bag-size copy) => 3) + (check (bag-count (lambda (x) (= x 2)) copy) => 2)) +(check-true (bag-empty? (bag-copy b-empty))) +(check-catch 'type-error (bag-copy "not a bag")) + +#| +list->bag +将列表转换为 bag。 + +语法 +---- +(list->bag list) + +参数 +---- +list : list +要转换的列表。 + +返回值 +----- +返回包含列表中所有元素的 bag(使用默认比较器,重复元素保留)。 +|# +(define b-list-1 (list->bag '(1 2 2 3))) +(check-true (bag? b-list-1)) +(check-true (eq? (bag-comparator b-list-1) comp)) +(check (bag-size b-list-1) => 4) +(check (bag-count (lambda (x) (= x 2)) b-list-1) => 2) +(define b-list-empty (list->bag '())) +(check-true (bag-empty? b-list-empty)) + +#| +list->bag! +将列表元素并入 bag(可变操作)。 + +语法 +---- +(list->bag! bag list) + +参数 +---- +bag : bag +目标 bag。 + +list : list +要并入的元素列表。 + +返回值 +------ +返回修改后的 bag(与传入的 bag 是同一个对象)。 +|# +(define b-list-merge (bag 1 2)) +(define b-list-merge-result (list->bag! b-list-merge '(2 3 3))) +(check-true (eq? b-list-merge-result b-list-merge)) +(check (bag-size b-list-merge) => 5) +(check (bag-count (lambda (x) (= x 2)) b-list-merge) => 2) +(check (bag-count (lambda (x) (= x 3)) b-list-merge) => 2) +(list->bag! b-list-merge '()) +(check (bag-size b-list-merge) => 5) +(check-catch 'type-error (list->bag! "not a bag" '(1 2))) + + + + +;; 不同类型元素也可存入 bag +(define b-mixed (bag "a" 'a 0)) +(check (bag-member b-mixed "a" #f) => "a") +(check (bag-member b-mixed 'a #f) => 'a) +(check (bag-member b-mixed 0 #f) => 0) + +;; equal? 等价元素应命中 +(define a1 "hello") +(define a2 (string-copy a1)) +(define b-strings (bag a1)) +(check-true (string=? (bag-member b-strings a2 #f) "hello")) + +#| +bag-unfold +使用 unfold 模式创建 bag。 + +语法 +---- +(bag-unfold stop? mapper successor seed comparator) + +参数 +---- +stop? : procedure +停止谓词。接收当前种子,返回布尔值。 + +mapper : procedure +映射函数。接收当前种子,返回要添加到 bag 的元素。 + +successor : procedure +后继函数。接收当前种子,返回下一个种子。 + +seed : any +初始种子。 + +comparator : comparator +比较器。 + +返回值 +----- +返回由 unfold 生成的 bag。 +|# +(define b-unfold + (bag-unfold (lambda (n) (> n 3)) + (lambda (n) n) + (lambda (n) (+ n 1)) + 1 + comp)) +(check (bag-member b-unfold 1 #f) => 1) +(check (bag-member b-unfold 2 #f) => 2) +(check (bag-member b-unfold 3 #f) => 3) +(check (bag-member b-unfold 4 'no) => 'no) +(check-true (eq? (bag-comparator b-unfold) comp)) +(check-catch 'type-error + (bag-unfold (lambda (n) #t) + (lambda (n) n) + (lambda (n) n) + 0 + "not a comparator")) + +;; stop? 立即为真,返回空 bag +(define b-unfold-empty + (bag-unfold (lambda (n) #t) + (lambda (n) n) + (lambda (n) n) + 0 + comp)) +(check (bag-member b-unfold-empty 1 'none) => 'none) + +;; mapper 返回常量,重复元素也应能命中 +(define b-unfold-dup + (bag-unfold (lambda (n) (> n 2)) + (lambda (n) 'x) + (lambda (n) (+ n 1)) + 0 + comp)) +(check (bag-member b-unfold-dup 'x #f) => 'x) + +#| +bag-member +获取 bag 中与给定元素相等的元素。 + +语法 +---- +(bag-member bag element default) + +参数 +---- +bag : bag +目标 bag。 + +element : any +要查找的元素。 + +default : any +未找到时返回的默认值。 + +返回值 +----- +如果 bag 中存在与 element 等价的元素,返回该元素;否则返回 default。 +|# +(check (bag-member b-1-2 2 #f) => 2) +(check (bag-member b-1-2 9 'missing) => 'missing) +(check-catch 'type-error (bag-member "not a bag" 1 #f)) +(check (bag-member b-empty 1 'none) => 'none) + +#| +bag? +判断是否为 bag。 + +语法 +---- +(bag? obj) + +参数 +---- +obj : any +要检查的对象。 + +返回值 +----- +如果 obj 是 bag,返回 #t;否则返回 #f。 +|# +(check-true (bag? b-empty)) +(check-true (bag? b-1-2)) +(check-false (bag? "not a bag")) +(check-false (bag? '())) + +#| +bag-contains? +判断 bag 是否包含元素。 + +语法 +---- +(bag-contains? bag element) + +参数 +---- +bag : bag +目标 bag。 + +element : any +要检查的元素。 + +返回值 +----- +如果 bag 中存在与 element 等价的元素,返回 #t;否则返回 #f。 +|# +(check-true (bag-contains? b-1-2 2)) +(check-false (bag-contains? b-1-2 9)) +(check-false (bag-contains? b-empty 1)) +(check-catch 'type-error (bag-contains? "not a bag" 1)) + +#| +bag-empty? +判断 bag 是否为空。 + +语法 +---- +(bag-empty? bag) + +参数 +---- +bag : bag +目标 bag。 + +返回值 +----- +如果 bag 为空,返回 #t;否则返回 #f。 +|# +(check-true (bag-empty? b-empty)) +(check-false (bag-empty? b-1-2)) +(check-catch 'type-error (bag-empty? "not a bag")) + +#| +bag-disjoint? +判断两个 bag 是否不相交。 + +语法 +---- +(bag-disjoint? bag1 bag2) + +参数 +---- +bag1 : bag +第一个 bag。 + +bag2 : bag +第二个 bag。 + +返回值 +----- +如果两个 bag 没有相等元素,返回 #t;否则返回 #f。 +|# +(check-true (bag-disjoint? (bag 1 1) (bag 2 2))) +(check-false (bag-disjoint? (bag 1 1) (bag 1 2))) +(check-true (bag-disjoint? b-empty (bag 1))) +(check-true (bag-disjoint? (bag 1) b-empty)) +(check-catch 'type-error (bag-disjoint? "not a bag" (bag 1))) +(check-catch 'type-error (bag-disjoint? (bag 1) "not a bag")) + +#| +bag-size +返回 bag 中元素总数(含重复)。 + +语法 +---- +(bag-size bag) + +参数 +---- +bag : bag +目标 bag。 + +返回值 +----- +返回 bag 中元素总数(包含重复元素)。 +|# +(check (bag-size b-empty) => 0) +(check (bag-size b-1-2) => 3) +(check-catch 'type-error (bag-size "not a bag")) + +#| +bag-find +查找满足条件的元素。 + +语法 +---- +(bag-find predicate bag failure) + +参数 +---- +predicate : procedure +判断函数,接收元素并返回布尔值。 + +bag : bag +目标 bag。 + +failure : procedure +未找到时调用的过程。 + +返回值 +----- +返回第一个满足 predicate 的元素,否则返回 failure 的结果。 +|# +(check (bag-find even? b-1-2 (lambda () 'none)) => 2) +(check (bag-find (lambda (x) (> x 9)) b-1-2 (lambda () 'missing)) => 'missing) +(check-catch 'type-error (bag-find even? "not a bag" (lambda () 'none))) + +#| +bag-count +统计满足条件的元素数量(含重复)。 + +语法 +---- +(bag-count predicate bag) + +参数 +---- +predicate : procedure +判断函数,接收元素并返回布尔值。 + +bag : bag +目标 bag。 + +返回值 +----- +返回满足 predicate 的元素总数(含重复)。 +|# +(check (bag-count even? b-1-2) => 2) +(check (bag-count (lambda (x) (> x 9)) b-1-2) => 0) +(check-catch 'type-error (bag-count even? "not a bag")) + +#| +bag-any? +判断是否存在满足条件的元素。 + +语法 +---- +(bag-any? predicate bag) + +参数 +---- +predicate : procedure +判断函数,接收元素并返回布尔值。 + +bag : bag +目标 bag。 + +返回值 +----- +如果存在满足 predicate 的元素,返回 #t,否则返回 #f。 +|# +(check-true (bag-any? even? b-1-2)) +(check-false (bag-any? (lambda (x) (> x 9)) b-1-2)) +(check-false (bag-any? even? b-empty)) +(check-catch 'type-error (bag-any? even? "not a bag")) + +#| +bag-every? +判断是否所有元素都满足条件。 + +语法 +---- +(bag-every? predicate bag) + +参数 +---- +predicate : procedure +判断函数,接收元素并返回布尔值。 + +bag : bag +目标 bag。 + +返回值 +----- +如果 bag 中所有元素都满足 predicate 返回 #t,否则返回 #f。 +空 bag 返回 #t。 +|# +(check-true (bag-every? (lambda (x) (> x 0)) b-1-2)) +(check-false (bag-every? even? b-1-2)) +(check-true (bag-every? even? b-empty)) +(check-catch 'type-error (bag-every? even? "not a bag")) + +#| +bag-adjoin / bag-adjoin! +向 bag 添加元素(允许重复)。 + +语法 +---- +(bag-adjoin bag element ...) +(bag-adjoin! bag element ...) + +参数 +---- +bag : bag +目标 bag。 + +element ... : any +要添加的元素(可重复)。 + +返回值 +----- +bag-adjoin 返回新的 bag,原 bag 保持不变(非破坏性)。 +bag-adjoin! 就地修改原 bag,并返回修改后的 bag(破坏性)。 +|# +(let ((b (bag 1 2 2))) + (define b2 (bag-adjoin b 2 3)) + (check (bag-size b) => 3) + (check (bag-size b2) => 5) + (check (bag-count (lambda (x) (= x 2)) b2) => 3) + (bag-adjoin! b 2 3) + (check (bag-size b) => 5)) +(check-catch 'type-error (bag-adjoin "not a bag" 1)) +(check-catch 'type-error (bag-adjoin! "not a bag" 1)) + +#| +bag-replace / bag-replace! +替换 bag 中与元素等价的代表值,保留计数。 + +语法 +---- +(bag-replace bag element) +(bag-replace! bag element) + +参数 +---- +bag : bag +目标 bag。 + +element : any +用于替换的元素(按 comparator 等价判断)。 + +返回值 +----- +bag-replace 返回新的 bag,原 bag 保持不变(非破坏性)。 +bag-replace! 就地修改原 bag,并返回修改后的 bag(破坏性)。 +|# +(let* ((s1 "hello") + (s2 (string-copy s1)) + (b (bag s1)) + (b2 (bag-replace b s2))) + (check-true (eq? (car (bag->list b)) s1)) + (check-true (eq? (car (bag->list b2)) s2))) +(let* ((s1 "hello") + (s2 (string-copy s1)) + (b (bag s1))) + (bag-replace! b s2) + (check-true (eq? (car (bag->list b)) s2))) + +#| +bag-delete / bag-delete! +删除一个元素实例。 + +语法 +---- +(bag-delete bag element ...) +(bag-delete! bag element ...) + +参数 +---- +bag : bag +目标 bag。 + +element ... : any +要删除的元素,每个元素只删除一个实例。 + +返回值 +----- +bag-delete 返回新的 bag,原 bag 保持不变(非破坏性)。 +bag-delete! 就地修改原 bag,并返回修改后的 bag(破坏性)。 +|# +(let ((b (bag 1 2 2 3))) + (define b2 (bag-delete b 2 3)) + (check (bag-size b) => 4) + (check (bag-size b2) => 2) + (check (bag-count (lambda (x) (= x 2)) b2) => 1)) +(let ((b (bag 1 2 2 3))) + (bag-delete! b 2 3) + (check (bag-size b) => 2) + (check (bag-count (lambda (x) (= x 2)) b) => 1)) + +#| +bag-delete-all / bag-delete-all! +按列表删除元素实例(列表含重复则多次删除)。 + +语法 +---- +(bag-delete-all bag element-list) +(bag-delete-all! bag element-list) + +参数 +---- +bag : bag +目标 bag。 + +element-list : list +要删除的元素列表,列表中重复元素会多次删除。 + +返回值 +----- +bag-delete-all 返回新的 bag,原 bag 保持不变(非破坏性)。 +bag-delete-all! 就地修改原 bag,并返回修改后的 bag(破坏性)。 +|# +(let ((b (bag 1 2 2 2 3))) + (define b2 (bag-delete-all b '(2 2 3))) + (check (bag-size b) => 5) + (check (bag-size b2) => 2) + (check (bag-count (lambda (x) (= x 2)) b2) => 1)) +(let ((b (bag 1 2 2 2 3))) + (bag-delete-all! b '(2 2 3)) + (check (bag-size b) => 2) + (check (bag-count (lambda (x) (= x 2)) b) => 1)) + +#| +bag-search! +搜索并根据成功/失败分支更新 bag。 + +语法 +---- +(bag-search! bag element failure success) + +参数 +---- +bag : bag +目标 bag。 + +element : any +要查找的元素(按 comparator 等价判断)。 + +failure : procedure +未命中时调用,签名:(lambda (insert ignore) ...) + +success : procedure +命中时调用,签名:(lambda (element update remove) ...) + +返回值 +----- +返回 (values bag obj),具体 obj 由 failure/success 回调决定。 +|# +(let ((yam (bag #\y #\a #\m))) + (define (failure/insert insert ignore) + (insert 1)) + (define (failure/ignore insert ignore) + (ignore 2)) + (define (success/update element update remove) + (update #\b 3)) + (define (success/remove element update remove) + (remove 4)) + + (call-with-values + (lambda () (bag-search! (bag-copy yam) #\! failure/insert error)) + (lambda (b obj) + (check-true (bag-contains? b #\!)) + (check obj => 1))) + (call-with-values + (lambda () (bag-search! (bag-copy yam) #\! failure/ignore error)) + (lambda (b obj) + (check-false (bag-contains? b #\!)) + (check obj => 2))) + (call-with-values + (lambda () (bag-search! (bag-copy yam) #\y error success/update)) + (lambda (b obj) + (check-true (bag-contains? b #\b)) + (check-false (bag-contains? b #\y)) + (check obj => 3))) + (call-with-values + (lambda () (bag-search! (bag-copy yam) #\a error success/remove)) + (lambda (b obj) + (check-false (bag-contains? b #\a)) + (check obj => 4)))) + +#| +bag=? / bag? / bag<=? / bag>=? +多重集包含关系与相等性判断。 + +语法 +---- +(bag=? bag1 bag2 ...) +(bag? bag1 bag2 ...) +(bag<=? bag1 bag2 ...) +(bag>=? bag1 bag2 ...) + +参数 +---- +bag1, bag2 ... : bag +参与比较的 bag。 + +返回值 +----- +返回 #t 或 #f。 +|# +(let ((b1 (bag 1 1 2)) + (b2 (bag 1 1 2 2)) + (b3 (bag 1 1 2))) + (check-true (bag=? b1 b3)) + (check-false (bag=? b1 b2)) + (check-true (bag<=? b1 b2)) + (check-false (bag<=? b2 b1)) + (check-true (bag=? b2 b1)) + (check-true (bag>? b2 b1))) + +#| +bag-union / bag-intersection / bag-difference / bag-xor +多重集并集/交集/差集/对称差。 + +语法 +---- +(bag-union bag1 bag2 ...) +(bag-intersection bag1 bag2 ...) +(bag-difference bag1 bag2 ...) +(bag-xor bag1 bag2) + +参数 +---- +bag1, bag2 ... : bag +参与运算的 bag(bag-xor 仅支持两个)。 + +返回值 +----- +返回新的 bag,不修改原 bag。 +|# +(let* ((b1 (bag 'a 'a 'b)) + (b2 (bag 'a 'b 'b 'c)) + (u (bag-union b1 b2)) + (i (bag-intersection b1 b2)) + (d (bag-difference b1 b2)) + (x (bag-xor b1 b2))) + (check (bag-count (lambda (x) (eq? x 'a)) u) => 2) + (check (bag-count (lambda (x) (eq? x 'b)) u) => 2) + (check (bag-count (lambda (x) (eq? x 'c)) u) => 1) + + (check (bag-count (lambda (x) (eq? x 'a)) i) => 1) + (check (bag-count (lambda (x) (eq? x 'b)) i) => 1) + + (check (bag-count (lambda (x) (eq? x 'a)) d) => 1) + (check (bag-count (lambda (x) (eq? x 'b)) d) => 0) + + (check (bag-count (lambda (x) (eq? x 'a)) x) => 1) + (check (bag-count (lambda (x) (eq? x 'b)) x) => 1) + (check (bag-count (lambda (x) (eq? x 'c)) x) => 1)) + +#| +bag-union! / bag-intersection! / bag-difference! / bag-xor! +就地更新版本的并集/交集/差集/对称差。 + +语法 +---- +(bag-union! bag1 bag2 ...) +(bag-intersection! bag1 bag2 ...) +(bag-difference! bag1 bag2 ...) +(bag-xor! bag1 bag2) + +参数 +---- +bag1, bag2 ... : bag +参与运算的 bag(bag-xor! 仅支持两个)。 + +返回值 +----- +返回修改后的 bag1。 +|# +(let* ((b1 (bag 'a 'a 'b)) + (b2 (bag 'a 'b 'b 'c))) + (bag-union! b1 b2) + (check (bag-count (lambda (x) (eq? x 'a)) b1) => 2) + (check (bag-count (lambda (x) (eq? x 'b)) b1) => 2) + (check (bag-count (lambda (x) (eq? x 'c)) b1) => 1)) + +(let* ((b1 (bag 'a 'a 'b)) + (b2 (bag 'a 'b 'b 'c))) + (bag-intersection! b1 b2) + (check (bag-count (lambda (x) (eq? x 'a)) b1) => 1) + (check (bag-count (lambda (x) (eq? x 'b)) b1) => 1) + (check (bag-count (lambda (x) (eq? x 'c)) b1) => 0)) + +(let* ((b1 (bag 'a 'a 'b)) + (b2 (bag 'a 'b 'b 'c))) + (bag-difference! b1 b2) + (check (bag-count (lambda (x) (eq? x 'a)) b1) => 1) + (check (bag-count (lambda (x) (eq? x 'b)) b1) => 0) + (check (bag-count (lambda (x) (eq? x 'c)) b1) => 0)) + +(let* ((b1 (bag 'a 'a 'b)) + (b2 (bag 'a 'b 'b 'c))) + (bag-xor! b1 b2) + (check (bag-count (lambda (x) (eq? x 'a)) b1) => 1) + (check (bag-count (lambda (x) (eq? x 'b)) b1) => 1) + (check (bag-count (lambda (x) (eq? x 'c)) b1) => 1)) + +#| +bag-comparator +获取 bag 的 comparator。 + +语法 +---- +(bag-comparator bag) + +参数 +---- +bag : bag +目标 bag。 + +返回值 +----- +返回 bag 使用的 comparator。 +|# +(check-true (eq? (bag-comparator b-empty) comp)) +(check-true (eq? (bag-comparator b-1-2) comp)) + +#| +内部校验 check-bag 的函数也要覆盖错误分支。 +|# +(check-catch 'type-error (bag-member "not a bag" 1 #f)) + + +(check-report) diff --git a/tests/goldfish/liii/either-test.scm b/tests/goldfish/liii/either-test.scm new file mode 100644 index 0000000000000000000000000000000000000000..ec8b13bd6a4367e15774863f44b40d2e3a1188ed --- /dev/null +++ b/tests/goldfish/liii/either-test.scm @@ -0,0 +1,274 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(import (liii check) + (liii error) + (liii either)) + +(check-set-mode! 'report-failed) + + +;; ========================================== +;; 1. 基础构造与提取测试 +;; ========================================== + +#| +from-left +创建 Left 值(通常代表错误或异常情况)。 +|# +(check (to-left (from-left "error message")) => "error message") +(check (to-left (from-left 42)) => 42) +(check (to-left (from-left '())) => '()) + +#| +from-right +创建 Right 值(通常代表成功或有效数据)。 +|# +(check (to-right (from-right "success data")) => "success data") +(check (to-right (from-right 100)) => 100) +(check (to-right (from-right '(1 2 3))) => '(1 2 3)) + + +;; ========================================== +;; 2. 谓词测试 +;; ========================================== + +#| +either-left? / either-right? +类型判断函数。 +|# +(let ((left-val (from-left "error")) + (right-val (from-right "success"))) + (check-true (either-left? left-val)) + (check-false (either-right? left-val)) + (check-true (either-right? right-val)) + (check-false (either-left? right-val))) + +;; 边界情况测试:非 Pair 不是 Either +(check-false (either-left? '())) +(check-false (either-right? '())) +(check-false (either-left? "string")) + + +;; ========================================== +;; 3. 高阶函数操作测试 (Map / For-Each) +;; ========================================== + +#| +either-map +Functor 映射操作。 +|# +(let ((left-val (from-left "error")) + (right-val (from-right 5))) + ;; 对左值应用 map 应该返回原值 + (check (to-left (either-map (lambda (x) (* x 2)) left-val)) => "error") + ;; 对右值应用 map 应该应用函数 + (let ((result (either-map (lambda (x) (* x 2)) right-val))) + (check-true (either-right? result)) + (check (to-right result) => 10))) + +#| +either-for-each +副作用遍历操作。 +|# +(let ((counter 0) + (left-val (from-left "error")) + (right-val (from-right 5))) + ;; 对左值应用 for-each 不执行 + (either-for-each (lambda (x) (set! counter (+ counter x))) left-val) + (check counter => 0) + ;; 对右值应用 for-each 执行 + (either-for-each (lambda (x) (set! counter (+ counter x))) right-val) + (check counter => 5)) + + +;; ========================================== +;; 4. 实用函数测试 (Utility Functions) +;; ========================================== + +#| +either-get-or-else +简单获取值或默认值。 +|# +(check (either-get-or-else (from-right 42) 0) => 42) +(check (either-get-or-else (from-left "error") 0) => 0) + +#| +either-or-else +Either 级别的备选方案。 +|# +(let ((main (from-right 1)) + (backup (from-right 2)) + (fail (from-left 0))) + (check (to-right (either-or-else main backup)) => 1) + (check (to-right (either-or-else fail backup)) => 2)) + + +;; ========================================== +;; 5. 逻辑判断与过滤测试 +;; ========================================== + +#| +either-filter-or-else +条件过滤。 + +语法 +---- +(either-filter-or-else pred zero either) + +描述 +---- +- 如果是 Right 且满足 pred -> 保持 Right +- 如果是 Right 且不满足 pred -> 变为 Left(zero) +- 如果是 Left -> 保持 Left +|# +(let ((r10 (from-right 10)) ; 偶数 + (r11 (from-right 11)) ; 奇数 + (l (from-left "orig"))) ; 原始错误 + + ;; 1. Right 且满足条件 -> 保持原样 + (check (to-right (either-filter-or-else even? "err" r10)) => 10) + + ;; 2. Right 但不满足条件 -> 变为 Left("err") + (let ((res (either-filter-or-else even? "Must be even" r11))) + (check-true (either-left? res)) + (check (to-left res) => "Must be even")) + + ;; 3. Left -> 保持原样 (忽略条件) + (let ((res-l (either-filter-or-else even? "Must be even" l))) + (check-true (either-left? res-l)) + (check (to-left res-l) => "orig"))) + +#| +either-contains +包含判断。 + +语法 +---- +(either-contains either x) +|# +(check-true (either-contains (from-right 10) 10)) +(check-false (either-contains (from-right 11) 10)) ; 值不同 +(check-false (either-contains (from-left 10) 10)) ; 状态不对 + +#| +either-every +全称量词 (空真性测试)。 + +语法 +---- +(either-every pred either) + +描述 +---- +Right 必须满足 pred。 +Left 总是返回 #t。 +|# +(check-true (either-every even? (from-right 10))) +(check-false (either-every even? (from-right 11))) +(check-true (either-every even? (from-left "error"))) ; Left 总是 #t + +#| +either-any +存在量词。 + +语法 +---- +(either-any pred either) + +描述 +---- +Right 必须满足 pred。 +Left 总是返回 #f。 +|# +(check-true (either-any even? (from-right 10))) +(check-false (either-any even? (from-right 11))) +(check-false (either-any even? (from-left "error"))) ; Left 总是 #f + + +;; ========================================== +;; 6. 综合流程测试 +;; ========================================== + +;; 测试 Map 的连续使用 +(let* ((val1 (from-right 10)) + (val2 (either-map (lambda (x) (+ x 5)) val1)) ;; Right 15 + (val3 (either-map (lambda (x) (* x 2)) val2))) ;; Right 30 + (check-true (either-right? val3)) + (check (to-right val3) => 30)) + +;; 测试错误处理流程 (验证短路特性) +(let* ((error-val (from-left "network error")) + ;; 下面的 map 不应执行,因为输入已经是 Left + (mapped-error (either-map (lambda (x) (string-append "Error: " x)) error-val))) + (check-true (either-left? mapped-error)) + (check (to-left mapped-error) => "network error")) + + + +;; ========================================== +;; 7. 异常与边界测试 (Check-Catch) +;; ========================================== + +;; ------------------------------------------ +;; A. 测试 check-either 类型守卫 +;; 预期:所有函数接收非 Either 类型时,抛出 'type-error +;; ------------------------------------------ + +;; 提取函数防卫 +(check-catch 'type-error (to-left "not-either")) +(check-catch 'type-error (to-left 123)) +(check-catch 'type-error (to-right '())) + +;; 高阶函数防卫 +(check-catch 'type-error (either-map (lambda (x) x) "not-either")) +(check-catch 'type-error (either-for-each (lambda (x) x) "not-either")) + +;; 逻辑函数防卫 +(check-catch 'type-error (either-filter-or-else even? 0 "not-either")) +(check-catch 'type-error (either-contains "not-either" 1)) +(check-catch 'type-error (either-every even? "not-either")) +(check-catch 'type-error (either-any even? "not-either")) + +;; 实用函数防卫 +(check-catch 'type-error (either-get-or-else "not-either" 0)) +(check-catch 'type-error (either-or-else "not-either" (from-right 1))) + +;; ------------------------------------------ +;; B. 测试参数类型检查 (procedure?) +;; 预期:传入非函数参数时,抛出 'type-error +;; ------------------------------------------ + +(define valid-right (from-right 10)) + +(check-catch 'type-error (either-map "not-a-proc" valid-right)) +(check-catch 'type-error (either-for-each "not-a-proc" valid-right)) +(check-catch 'type-error (either-filter-or-else "not-a-proc" 0 valid-right)) +(check-catch 'type-error (either-every "not-a-proc" valid-right)) +(check-catch 'type-error (either-any "not-a-proc" valid-right)) + +;; ------------------------------------------ +;; C. 测试逻辑错误 (Value Error) +;; 预期:违反 Either 语义的操作,抛出 'value-error +;; ------------------------------------------ + +;; 试图从 Right 中提取 Left +(check-catch 'value-error (to-left (from-right "I am Right"))) + +;; 试图从 Left 中提取 Right +(check-catch 'value-error (to-right (from-left "I am Left"))) + +(check-report) \ No newline at end of file diff --git a/tests/goldfish/liii/hash-table-test.scm b/tests/goldfish/liii/hash-table-test.scm index e00c833bf8dbedf00b745cbfacd13a7cedcf4e38..8a8de828ca4c68bf0d06cc9f680bd4f047d73f48 100644 --- a/tests/goldfish/liii/hash-table-test.scm +++ b/tests/goldfish/liii/hash-table-test.scm @@ -256,6 +256,12 @@ (hash-table 'a 1 'b 2 'c 3)) (check cnt => 6)) +(let ((ht (hash-table 'a 1 'b 2 'c 3))) + (check (hash-table-fold (lambda (k v acc) (+ acc v)) 0 ht) => 6) + ) + +(check (hash-table-fold (lambda (k v acc) (+ acc v)) 10 (hash-table)) => 10) + (let* ((ht (hash-table 'a 1 'b 2 'c 3)) (ks (hash-table-map->list (lambda (k v) k) ht)) (vs (hash-table-map->list (lambda (k v) v) ht))) @@ -275,4 +281,3 @@ => (list 'k1 'v1)) (check-report) - diff --git a/tests/goldfish/liii/http-test.scm b/tests/goldfish/liii/http-test.scm new file mode 100644 index 0000000000000000000000000000000000000000..f488d7cf0f70c3d25f1796933d5a04c32e5b5052 --- /dev/null +++ b/tests/goldfish/liii/http-test.scm @@ -0,0 +1,135 @@ +(import (liii check) + (liii http) + (liii string) + (liii rich-json) + (only (liii lang) display*) + (only (liii base) let1)) + +(check-set-mode! 'report-failed) + +(let1 r (http-head "https://httpbin.org") + (check (r 'status-code) => 200) + (check (r 'url) => "https://httpbin.org/") + (check-true (real? (r 'elapsed))) + ;; NOTE: httpbin.org's LB routes to different backends. + ;; Some return "OK", others empty string for reason. + ;; HTTP/2+ allows omitting reason phrases. + (check-true (or (equal? (r 'reason) "OK") + (equal? (r 'reason) ""))) + (check (r 'text) => "") + (check ((r 'headers) "content-type") => "text/html; charset=utf-8") + (check ((r 'headers) "content-length") => "9593") + (check-true (http-ok? r))) + +(let1 r (http-get "https://httpbin.org") + (check (r 'status-code) => 200) + (check-true (> (string-length (r 'text)) 0)) + (check ((r 'headers) "content-type") => "text/html; charset=utf-8")) + +(let1 r (http-get "https://httpbin.org/get" + :params '(("key1" . "value1") ("key2" . "value2"))) + (check-true (string-contains (r 'text) "value1")) + (check-true (string-contains (r 'text) "value2")) + (check (r 'url) => "https://httpbin.org/get?key1=value1&key2=value2")) + +(let1 r (http-post "https://httpbin.org/post" + :params '(("key1" . "value1") ("key2" . "value2"))) + (check-true (string-contains (r 'text) "value1")) + (check-true (string-contains (r 'text) "value2")) + (check (r 'status-code) => 200) + (check (r 'url) => "https://httpbin.org/post?key1=value1&key2=value2")) + +(let* ((r (http-post "https://httpbin.org/post" + :data "This is raw data")) + (json (string->json (r 'text)))) + (display* (r 'text) "\n") + (display* json "\n") + (display* (json->string json) "\n") + (check (r 'status-code) => 200) + (check (json-ref json "data") => "This is raw data")) + +;; Streaming HTTP tests + +;; Test streaming GET with simple endpoint +(let ((collected '()) + (userdata-received #f) + (userdata-expected '("streaming" test "param"))) + (http-stream-get "https://httpbin.org/get" + (lambda (chunk userdata) + (display userdata) + (newline) + (set! userdata-received userdata) + (when (> (string-length chunk) 0) + (set! collected (cons chunk collected)))) + userdata-expected + '(("query" . "test_values") ("limit" . "10"))) + (check-true (> (length collected) 0)) + (check userdata-received => userdata-expected)) + +;; Test streaming GET with JSON endpoint +(let1 collected '() + (http-stream-get "https://jsonplaceholder.typicode.com/posts/1" + (lambda (chunk userdata) + (when (> (string-length chunk) 0) + (set! collected (cons chunk collected))))) + (let ((response (string-join (reverse collected) ""))) + (check-true (> (string-length response) 0)) + (check-true (string-contains response "userId")))) + +;; Test streaming POST with JSON data +(let1 collected '() + (http-stream-post "https://httpbin.org/post" + (lambda (chunk userdata) + (when (> (string-length chunk) 0) + (set! collected (cons chunk collected)))) + '() + '(("param1" . "value1")) + "{\"test\": \"streaming-json\"}" + '(("Content-Type" . "application/json"))) + (let ((response (string-join (reverse collected) ""))) + (check-true (> (string-length response) 0)) + (check-true (string-contains response "streaming-json")))) + +;; Test streaming POST with plain text +(let1 collected '() + (http-stream-post "https://httpbin.org/post" + (lambda (chunk userdata) + (when (> (string-length chunk) 0) + (set! collected (cons chunk collected)))) + '() + '() + "Simple streaming POST test") + (let ((response (string-join (reverse collected) ""))) + (check-true (> (string-length response) 0)) + (check-true (string-contains response "Simple streaming POST test")))) + +;; Test streaming POST with XML data +(let1 collected '() + (http-stream-post "https://httpbin.org/post" + (lambda (chunk userdata) + (when (> (string-length chunk) 0) + (set! collected (cons chunk collected)))) + '() + '() + "stream-xml-test" + '(("Content-Type" . "application/xml"))) + (let ((response (string-join (reverse collected) ""))) + (check-true (> (string-length response) 0)) + (check-true (string-contains response "stream-xml-test")))) + +;; Test streaming POST with form data +(let1 collected '() + (http-stream-post "https://httpbin.org/post" + (lambda (chunk userdata) + (when (> (string-length chunk) 0) + (set! collected (cons chunk collected)))) + '() + '() + "field1=stream-test&field2=form-data" + '(("Content-Type" . "application/x-www-form-urlencoded"))) + (let ((response (string-join (reverse collected) ""))) + (check-true (> (string-length response) 0)) + (check-true (string-contains response "stream-test")))) + +(check-report) + diff --git a/tests/goldfish/liii/json-test.scm b/tests/goldfish/liii/json-test.scm new file mode 100644 index 0000000000000000000000000000000000000000..ee019f7833f22b7719ac87706ff5bf811a542fc4 --- /dev/null +++ b/tests/goldfish/liii/json-test.scm @@ -0,0 +1,1096 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(import (liii check) + (liii json) + (liii base) + (liii error)) + +; comment this line to show detailed check reports +;(check-set-mode! 'report-failed) + +; shared test data +(define bob-j '((bob . ((age . 18) + (sex . male) + (name . "Bob"))))) + + + + + + +#| +类型谓词测试 (Type Predicates) +检查 JSON 数据类型的判断函数。 +包含:json-null?, json-object?, json-array?, json-string?, + json-number?, json-integer?, json-float?, json-boolean? +|# + +;; 1. json-null? +;; 只有符号 'null 才是 JSON 的 null +(check-true (json-null? 'null)) +(check-false (json-null? '((name . "Alice")))) + +;; 2. json-object? +;; 在 guenchi json 中,非空列表 (alist) 表示对象 +(check-true (json-object? '((name . "Alice")))) +(check-true (json-object? '((a . 1) (b . 2)))) +(check-false (json-object? '())) +(check-false (json-object? #(1 2))) +(check-false (json-object? "{}")) +(check-false (json-object? '(1 2 3))) + +;; 3. json-array? +;; 向量 (vector) 表示数组 +(check-true (json-array? #(1 2 3))) +(check-true (json-array? #())) ; 空向量是空数组 +(check-true (json-array? #("a" "b"))) +(check-false (json-array? '(1 2 3))); 列表不是数组 +(check-false (json-array? "[]")) + +;; 4. json-string? +(check-true (json-string? "hello")) +(check-true (json-string? "")) +(check-false (json-string? 'hello)) ; 符号不是字符串 +(check-false (json-string? 123)) + +;; 5. json-number? +;; 包含整数和浮点数 +(check-true (json-number? 123)) +(check-true (json-number? 3.14)) +(check-true (json-number? -10)) +(check-true (json-number? 0)) +(check-false (json-number? "123")) + +;; 6. json-integer? +(check-true (json-integer? 100)) +(check-true (json-integer? 0)) +(check-true (json-integer? -5)) +(check-false (json-integer? 3.14)) +(check-false (json-integer? 1.0)) + +;; 7. json-float? +(check-true (json-float? 3.14)) +(check-true (json-float? -0.01)) +(check-false (json-float? 100)) + +;; 8. json-boolean? +;; Scheme 的 #t 和 #f +(check-true (json-boolean? #t)) +(check-true (json-boolean? #f)) +(check-false (json-boolean? 0)) +(check-false (json-boolean? "true")) + +#| +json-ref (Basic) +获取JSON对象的值。 + +语法 +---- +(json-ref json key) +或直接使用数据本身。 + +返回值 +----- +返回JSON对象的值。 + + +|# + + +;; 字典。 +(check (string->json "{\"name\":\"Bob\",\"age\":21}") => `(("name" . "Bob") ("age" . 21))) +;; 数组 +(check (string->json "[1,2,3]") => #(1 2 3)) + +#| +json-get-or-else +如果 JSON 对象为 null,则返回默认值;否则返回 JSON 对象本身。 + +语法 +---- +(json-get-or-else json default-value) + +参数 +---- +json : any +JSON 数据对象。 + +default-value : any +当 JSON 对象为 null 时返回的默认值。 + +返回值 +----- +- 如果 JSON 对象不为 null,返回该对象本身 +- 如果 JSON 对象为 null,返回 default-value + +功能 +---- +提供安全的 null 值处理机制,当遇到 JSON null 时使用默认值替代。 +|# + +(check (json-get-or-else 'null bob-j) => bob-j) + +#| +json-keys +获取JSON对象的所有键名。 + +语法 +---- +(json-keys json) + +参数 +---- +json : any +JSON数据对象。 + +返回值 +----- +返回JSON对象的所有键名列表。 + +功能 +---- +- 对于对象类型JSON,返回所有键名的列表 +- 对于数组、null、布尔值等非对象类型,返回空列表 +|# + +(let1 j '((bob . ((age . 18) (sex . male)))) + (check (json-keys j) => '(bob)) + (check (json-keys (json-ref j 'bob)) => '(age sex))) + + +(check (json-keys 'null) => '()) +(check (json-keys 'true) => '()) +(check (json-keys 'false) => '()) +(check (json-keys (string->json "[1,2,3]")) => '()) +(check (json-keys (string->json "{}")) => '()) + +#| +json-ref (Nested) +通过键路径访问JSON对象的嵌套值。 + +语法 +---- +(json-ref json key1 key2 ...) + +参数 +---- +key1, key2, ... : symbol | string | number | boolean +用于访问嵌套值的键路径。 + +返回值 +----- +返回指定键路径对应的JSON值。 + +功能 +---- +- 支持多层嵌套访问 +- 如果键不存在,返回空列表 '() (相当于 missing/nil) +- 支持符号、字符串、数字和布尔值作为键 +|# +(check (json-ref bob-j 'bob 'age) => 18) +(check (json-ref bob-j 'bob 'sex) => 'male) +(check (json-ref bob-j 'alice) => '()) +(check (json-ref bob-j 'alice 'age) => '()) +(check (json-ref bob-j 'bob 'name) => "Bob") + +(let1 j '((bob . ((age . 18) (sex . male)))) + (check (json-null? (json-ref j 'alice)) => #f) ; 这里的 '() 视为 missing,不是 'null + (check (null? (json-ref j 'alice)) => #t) ; 确认为 Scheme 空列表 + (check (json-null? (json-ref j 'bob)) => #f)) + +(let1 j '((alice . ((age . 18) (sex . male)))) + (check (json-null? (json-ref j 'alice)) => #f) + (check (null? (json-ref j 'bob)) => #t)) + + +#| +json-contains-key? +检查JSON对象是否包含指定的键。 + +语法 +---- +(json-contains-key? json key) + +参数 +---- +key : symbol | string | number | boolean +要检查的键名。 + +返回值 +----- +返回布尔值: +- #t:如果键存在 +- #f:如果键不存在 + +功能 +---- +- 仅检查当前层级的键 +- 不检查嵌套对象中的键 +- 对于非对象类型JSON,总是返回#f +|# + +(let1 j '((bob . ((age . 18) (sex . male)))) + (check-false (json-contains-key? j 'alice)) + (check-true (json-contains-key? j 'bob)) + (check-false (json-contains-key? j 'age)) + (check-false (json-contains-key? j 'sex))) + + +(check-false (json-contains-key? (string->json "{}") "a")) + +(let1 j #(1 2 3) + (check (json->string j) => "[1,2,3]")) + +(check (string->json "{a:{b:1,c:2}}") => '((a . ((b . 1) (c . 2))))) +; json->string 生成宽松格式(key无引号) +(check (json->string '((a . ((b . 1) (c . 2))))) => "{a:{b:1,c:2}}") + +(check-catch 'value-error (json->string '((a)))) + + + +#| +string->json (Parse) +将json格式的字符串的转化成JSON对象。 + +语法 +---- +(string->json json_string) + +参数 +---- +json_string : json格式的字符串 + +返回值 +----- +返回对应的JSON数据结构。 + +功能 +---- +- 将json格式的字符串的转化成JSON对象。 +- 包含object、array、string、number、“true”、“false”、“null” +|# +(check (string->json "[]") => #()) +(check (string->json "[true]") => #(true)) +(check (string->json "[false]") => #(false)) +(check (string->json "[1,2,3]") => #(1 2 3)) +(check (string->json "[{data: 1},{}]") => #(((data . 1)) (()))) ;; 数组里面有对象 +(check (string->json "{}") => '(())) +(check (string->json "{args: {}}") => '((args ()))) +(check (string->json "{\"args\": {}}") => '(("args" ()))) +(check (string->json "{\"args\": {}, data: 1}") => '(("args" ()) (data . 1))) +(check (string->json "{\"args\": {}, data: [1,2,3]}") => '(("args" ()) (data . #(1 2 3)))) ;;JSON对象的值是数组 +(check (string->json "{\"args\": {}, data: true}") => `(("args" ()) (data . true))) +(check (string->json "{\"args\": {}, data: null}") => `(("args" ()) (data . null))) + +;; todo bug需要修复 +; (check (string->json "[null]") => #(null)) +; (check (string->json "[true],[true]") => #t) +; (check (string->json "{\"args\": {}, data: [true]}") => '(("args" ()) (data . #(#t)))) +; (check (string->json "{\"args\": {}, data: [null]}") => '(("args" ()) (data . #(null)))) + + +#| +json-string-escape +将字符串转换为JSON格式的转义字符串。 + +语法 +---- +(json-string-escape string) + +参数 +---- +string : string +要转义的原始字符串。 + +返回值 +----- +返回JSON格式的转义字符串,包含双引号。 + +功能 +---- +- 转义JSON特殊字符:\" \\ \/ \b \f \n \r \t +- 添加双引号包围 +- 支持Unicode字符 +- 优化处理Base64等安全字符串 + +边界条件 +-------- +- 空字符串返回"" +- 包含特殊字符的字符串会被正确转义 +- 支持中文字符和Unicode转义 + +性能特征 +-------- +- 时间复杂度:O(n),n为字符串长度 +- 空间复杂度:O(n),存储转义后的字符串 + +兼容性 +------ +- 符合JSON标准 +- 支持所有可打印Unicode字符 +|# +; Basic json-string-escape tests +(check (json-string-escape "hello") => "\"hello\"") +(check (json-string-escape "hello\"world") => "\"hello\\\"world\"") +(check (json-string-escape "hello\\world") => "\"hello\\\\world\"") +(check (json-string-escape "hello/world") => "\"hello\\/world\"") +(check (json-string-escape "hello\bworld") => "\"hello\\bworld\"") +(check (json-string-escape "hello\fworld") => "\"hello\\fworld\"") +(check (json-string-escape "hello\nworld") => "\"hello\\nworld\"") +(check (json-string-escape "hello\rworld") => "\"hello\\rworld\"") +(check (json-string-escape "hello\tworld") => "\"hello\\tworld\"") + +; Extended json-string-escape tests for optimized version +; Test empty string +(check (json-string-escape "") => "\"\"") + +; Test single character strings +(check (json-string-escape "A") => "\"A\"") +(check (json-string-escape "\"") => "\"\\\"\"") +(check (json-string-escape "\\") => "\"\\\\\"") + +; Test Base64-like strings (should use fast path for large strings) +(check (json-string-escape "ABC") => "\"ABC\"") +(check (json-string-escape "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=") + => "\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=\"") + +; Test typical Base64 encoded string (short) - these will use slow path now +(check (json-string-escape "SGVsbG8gV29ybGQ=") => "\"SGVsbG8gV29ybGQ=\"") +(check (json-string-escape "VGhpcyBpcyBhIHRlc3Q=") => "\"VGhpcyBpcyBhIHRlc3Q=\"") + +; Test Base64 with padding +(check (json-string-escape "QWxhZGRpbjpvcGVuIHNlc2FtZQ==") => "\"QWxhZGRpbjpvcGVuIHNlc2FtZQ==\"") + +; Test large Base64-like string WITHOUT slashes (should trigger fast path optimization) +; This is a 1024-character Base64-like string without slashes +(let ((large-base64 + (string-append + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567"))) + (check (json-string-escape large-base64) + => (string-append "\"" large-base64 "\""))) + +; Test mixed content strings (should NOT use fast path) +(check (json-string-escape "Hello123+=") => "\"Hello123+=\"") +(check (json-string-escape "Base64WithNewline\nText") => "\"Base64WithNewline\\nText\"") +(check (json-string-escape "Base64With\"Quote") => "\"Base64With\\\"Quote\"") + +; Test edge cases for sampling logic +; String exactly 1000 chars of Base64 (at threshold, should use slow path) +(let ((threshold-base64 + (make-string 1000 #\A))) ; 1000 'A' characters + (check (json-string-escape threshold-base64) + => (string-append "\"" threshold-base64 "\""))) + +; String just over 1000 chars with Base64 content (should use fast path) +(let ((large-base64-1001 + (string-append (make-string 1001 #\A)))) ; 1001 'A' characters + (check (json-string-escape large-base64-1001) + => (string-append "\"" large-base64-1001 "\""))) + +; String over 1000 chars but with non-Base64 in first 100 chars (should use slow path) +(let ((mixed-large + (string-append "Quote\"InFirst100" (make-string 990 #\A)))) + (check (json-string-escape mixed-large) + => (string-append "\"Quote\\\"InFirst100" (make-string 990 #\A) "\""))) + +; Test numeric strings (Base64-like) +(check (json-string-escape "1234567890") => "\"1234567890\"") +(check (json-string-escape "0123456789ABCDEFabcdef") => "\"0123456789ABCDEFabcdef\"") + +; Test URL-safe Base64 characters (but no slashes) +(check (json-string-escape "URLsafe_Base64chars") => "\"URLsafe_Base64chars\"") + +; Test performance edge case: very long string with escape chars +(let ((long-escaped + (make-string 50 #\"))) ; 50 quote characters + (check (string-length (json-string-escape long-escaped)) => 102)) ; 50*2 + 2 quotes = 102 + +; Test that all Base64 safe characters are recognized correctly (no slashes) +(check (json-string-escape "ABCDEFGHIJKLMNOPQRSTUVWXYZ") => "\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"") +(check (json-string-escape "abcdefghijklmnopqrstuvwxyz") => "\"abcdefghijklmnopqrstuvwxyz\"") +(check (json-string-escape "0123456789") => "\"0123456789\"") +(check (json-string-escape "+=") => "\"+=\"") + +#| +string->json (Advanced) +将JSON字符串解析为Scheme数据结构。 + +语法 +---- +(string->json json-string) + +参数 +---- +json-string : string +要解析的JSON格式字符串。 + +返回值 +----- +返回对应的Scheme数据结构。 + +功能 +---- +- 解析JSON对象、数组、字符串、数字、布尔值、null +- 支持Unicode转义序列 +- 支持代理对解析 +- 自动处理转义字符 + +边界条件 +-------- +- 无效JSON字符串抛出parse-error异常 +- 支持宽松语法(不带引号的键名) +- 支持中文字符和Unicode转义 + +性能特征 +-------- +- 时间复杂度:O(n),n为字符串长度 +- 空间复杂度:O(n),存储解析后的数据结构 + +兼容性 +------ +- 符合JSON标准 +- 支持扩展语法(不带引号的键名) +|# +(check (string->json "{\"age\":18}") => `(("age" . 18))) +(check (string->json "{age:18}") => `((age . 18))) +(check (string->json "{\"name\":\"中文\"}") => `(("name" . "中文"))) + +(check (string->json "{\"name\":\"Alice\\nBob\"}") => '(("name" . "Alice\nBob"))) +(check (string->json "{\"name\":\"Alice\\tBob\"}") => '(("name" . "Alice\tBob"))) +(check (string->json "{\"name\":\"Alice\\rBob\"}") => '(("name" . "Alice\rBob"))) +(check (string->json "{\"name\":\"Alice\\bBob\"}") => '(("name" . "Alice\bBob"))) +(check (string->json "{\"name\":\"Alice\\fBob\"}") => '(("name" . "Alice\fBob"))) +(check (string->json "{\"name\":\"Alice\\\\Bob\"}") => '(("name" . "Alice\\Bob"))) +(check (string->json "{\"name\":\"Alice\\\/Bob\"}") => '(("name" . "Alice/Bob"))) +(check (string->json "{\"name\":\"Alice\\\"Bob\"}") => '(("name" . "Alice\"Bob"))) + +(check (string->json "[\"\\u0041\"]") => #("A")) +(check (string->json "[\"\\u0041\\u0042\"]") => #("AB")) ; 多个 \u 转义字符 +(check (string->json "[\"\\u4E2D\\u6587\"]") => #("中文")) ; 中文字符 +(check (string->json "[\"\\uD83D\\uDE00\"]") => #("😀")) ; 代理对 + +(check (string->json "{\"name\":\"\\u4E2D\\u6587\"}") => '(("name" . "中文"))) ; 嵌套 JSON +(check (string->json "{\"emoji\":\"\\uD83D\\uDE00\"}") => '(("emoji" . "😀"))) ; 嵌套 JSON 中的代理对 + +(check-catch 'parse-error (string->json "[\"\\u004G\"]")) ; \u 后包含非十六进制字符 +(check-catch 'parse-error (string->json "[\"\\a\"]")) +(check (string->json "") => (eof-object)) +(check (string->json ".") => (eof-object)) +(check-catch 'read-error (string->json "[")) + +#| +json->string +将Scheme数据结构转换为JSON字符串。 + +语法 +---- +(json->string data) + +参数 +---- +data : any +要转换的Scheme数据结构。 + +返回值 +----- +返回JSON格式的字符串。 + +功能 +---- +- 支持对象、数组、字符串、数字、布尔值、null +- 自动转义特殊字符 +- 支持嵌套数据结构 +- 符合JSON标准格式 + +边界条件 +-------- +- 无效数据结构(非法 JSON 数据,如循环引用、不支持的类型等)抛出 value-error 异常 +- 空对象转换为{} +- 空数组转换为[] + +性能特征 +-------- +- 时间复杂度:O(n),n为数据结构大小 +- 空间复杂度:O(n),存储生成的JSON字符串 + +兼容性 +------ +- 符合JSON标准 +- 支持所有标准JSON数据类型 +|# + +(check (json->string #()) => "[]") +(check (json->string #(1 2 3)) => "[1,2,3]") +(check (json->string #("a" "b")) => "[\"a\",\"b\"]") +(check (json->string #(1 "a" true null)) => "[1,\"a\",true,null]") +(check (json->string '(("name" . "Alice"))) => "{\"name\":\"Alice\"}") +;; 多个键值对 +(check (json->string '(("id" . 1) ("active" . true))) => "{\"id\":1,\"active\":true}") +(check (json->string '((name . "Bob"))) => "{name:\"Bob\"}") +(check (json->string '((x . 10) (y . 20))) => "{x:10,y:20}") +(check (json->string #((("id" . 1)) (("id" . 2)))) => "[{\"id\":1},{\"id\":2}]") +(check (json->string '(("scores" . #(85 90 95)))) => "{\"scores\":[85,90,95]}") +(check (json->string + '(("user" . (("name" . "Dave") + ("tags" . #("admin" "editor")))))) + => "{\"user\":{\"name\":\"Dave\",\"tags\":[\"admin\",\"editor\"]}}") +(check (json->string '(("text" . "Line1\nLine2"))) => "{\"text\":\"Line1\\nLine2\"}") +(check (json->string #( "He said \"Hello\"" )) => "[\"He said \\\"Hello\\\"\"]") + +(check-catch 'value-error (json->string '((a)))) + + +#| +json-set +设置JSON对象中指定键的值。 + +语法 +---- +(json-set json key1 key2 ... value) + +参数 +---- +key1, key2, ... : symbol | string | number | boolean +用于定位嵌套值的键路径。 + +value : any | function +要设置的值或转换函数。 + +返回值 +----- +返回新的JSON对象,包含更新后的值。 + +功能 +---- +- 支持多层嵌套设置 +- 如果键不存在,会创建新的键值对 +- 支持函数作为最后一个参数,用于转换现有值 +- 支持符号、字符串、数字和布尔值作为键 + +边界条件 +-------- +- 不存在的键路径会创建新的嵌套结构 +- 函数参数接收当前值并返回新值 +- 支持任意深度的嵌套设置 + +性能特征 +-------- +- 时间复杂度:O(k),k为键路径长度 +- 空间复杂度:O(n),创建新的JSON对象 + +兼容性 +------ +- 支持链式调用 (通过嵌套函数调用) +|# +; json-set +; 单层,键为符号 +(let* ((j0 `((age . 18) (sex . male))) + (j1 (json-set j0 'age 19)) + (j2 (json-set j0 'age 'null))) + (check (json-ref j0 'age) => 18) + (check (json-ref j1 'age) => 19) + ;; 注意:json-ref 获取 'null 时会返回 '() + (check (json-ref j2 'age) => '())) + +; 单层,键为字符串 +(let* ((j0 `(("age" . 18) ("sex" . male))) + (j1 (json-set j0 "age" 19))) + (check (json-ref-number j1 "age" 0) => 19) + (check (json-ref j0 "age") => 18)) + +; 单层,键为整数 (Array set) +(let* ((j0 #(red green blue)) + (j1 (json-set j0 0 'black))) + (check j0 => #(red green blue)) + (check j1 => #(black green blue))) + +; 单层,键为布尔值 (不常见,但测试覆盖) +(let* ((j0 '((bob . 18) (jack . 16))) + (j1 (json-set j0 #t 3)) + (j2 (json-set j0 #t (lambda (x) (+ x 1))))) + (check j1 => '((bob . 3) (jack . 3))) + (check j2 => '((bob . 19) (jack . 17)))) + +; 多层,键为符号 +(let* ((j0 '((person . ((name . "Alice") (age . 25))))) + (j1 (json-set j0 'person 'age 26))) + (check (json-ref j1 'person 'age) => 26)) + +; 多层,键为字符串 +(let* ((j0 '((person . ((name . "Alice") + (age . 25) + (address . ((city . "Wonderland") + (zip . "12345"))))))) + (j1 (json-set j0 'person 'address 'city "Newland"))) + (check (json-ref j1 'person 'address 'city) => "Newland")) + +; 单层,最后一个参数不是值,而是一个函数 +(let* ((j0 '((name . "Alice") (age . 25))) + (j1 (json-set j0 'age (lambda (x) (+ x 1))))) + (check (json-ref j1 'age) => 26)) + +; 多层,最后一个参数不是值,而是一个函数 +(let* ((j0 '((person . ((name . "Alice") (age . 25))))) + (j1 (json-set j0 'person 'age (lambda (x) (+ x 1))))) + (check (json-ref j1 'person 'age) => 26)) + +; set with nested structure +(let* ((j0 `((age . 18) (sex . male))) + (j1 20) + (j2 (json-set j0 'age j1))) + (check (json-ref j2 'age) => 20)) + +(let* ((j0 `((person . ((name . "Alice") (age . 25))))) + (j1 26) + (j2 (json-set j0 'person 'age j1))) + (check (json-ref j2 'person 'age) => 26)) + +(let* ((j0 `((person . ((name . "Alice") (age . 25))))) + (j1 `((name . "Bob") (age . 30))) + (j2 (json-set j0 'person j1))) + (check (json-ref j2 'person 'name) => "Bob") + (check (json-ref j2 'person 'age) => 30)) + +#| +json-ref-boolean +获取JSON对象中的布尔值,如果值不是布尔类型则返回默认值。 + +语法 +---- +(json-ref-boolean json key default-value) + +参数 +---- +key : symbol | string | number | boolean +要获取的键名。 + +default-value : boolean +当值不是布尔类型或键不存在时返回的默认值。 + +返回值 +----- +返回布尔值: +- 如果键存在且值为布尔类型,返回该值 +- 否则返回default-value + +功能 +---- +- 安全地获取布尔值,避免类型错误 +- 支持符号、字符串、数字和布尔值作为键 +|# +; json-ref-boolean +(let* ((j0 '((active . #t) (verified . #f) (name . "Alice")))) + (check (json-ref-boolean j0 'active #f) => #t) + (check (json-ref-boolean j0 'verified #t) => #f) + (check (json-ref-boolean j0 'name #f) => #f) + (check (json-ref-boolean j0 'nonexistent #t) => #t)) + +#| +json-reduce (Transform) +转换JSON对象中指定键的值。 + +语法 +---- +(json-reduce json key1 key2 ... transform-fn) +(json-reduce json predicate-fn transform-fn) +(json-reduce json #t transform-fn) +(json-reduce json #f transform-fn) + +参数 +---- +key1, key2, ... : symbol | string | number | boolean +用于定位嵌套值的键路径。 + +predicate-fn : function +用于选择要转换的键的谓词函数。 + +transform-fn : function +转换函数,接收键和值,返回新值。 + +返回值 +----- +返回新的JSON对象,包含转换后的值。 + +功能 +---- +- 支持多层嵌套转换 +- 支持谓词函数选择要转换的键 +- 支持#t转换所有键,#f不转换任何键 +- 转换函数接收键和值作为参数 +|# +; json-reduce +(let* ((j0 '((name . "Alice") (age . 25))) + (j1 (json-reduce j0 'name (lambda (k v) (string-upcase v))))) + (check (json-ref j1 'name) => "ALICE") + (check (json-ref j1 'age) => 25)) + +(let* ((j0 '((person . ((name . "Alice") (age . 25))))) + (j1 (json-reduce j0 'person (lambda (k v) v)))) + (check (json-ref j1 'person) => '((name . "Alice") (age . 25)))) + +(let* ((j0 '((name . "Alice") (age . 25))) + (j1 (json-reduce j0 (lambda (k) (equal? k 'age)) (lambda (k v) (+ v 1))))) + (check (json-ref j1 'age) => 26) + (check (json-ref j1 'name) => "Alice")) + +(let* ((j0 '((name . "Alice") (age . 25))) + (j1 (json-reduce j0 #t (lambda (k v) (if (string? v) (string-upcase v) v))))) + (check (json-ref j1 'name) => "ALICE") + (check (json-ref j1 'age) => 25)) + +(let* ((j0 '((name . "Alice") (age . 25))) + (j1 (json-reduce j0 #f (lambda (k v) v)))) + (check (json-ref j1 'name) => "Alice") + (check (json-ref j1 'age) => 25)) + + +; Test json-reduce with multiple nested levels +(let* ((j0 '((user . ((profile . ((contact . ((email . "alice@example.com") + (phone . "123-456-7890"))))))))) + (j1 (json-reduce j0 'user 'profile 'contact 'email + (lambda (k v) (string-append v ".verified"))))) + (check (json-ref j1 'user 'profile 'contact 'email) => "alice@example.com.verified")) + +; Test json-reduce for conditional transformation with predicate function +(let* ((j0 '((user . ((data . ((scores . #(85 90 78 92 88)) + (settings . ((notifications . #t) + (theme . "dark"))))))))) + (j1 (json-reduce j0 'user 'data + (lambda (k) (equal? k 'scores)) + (lambda (k v) (vector-map (lambda (score) (+ score 5)) v))))) + (check (json-ref j1 'user 'data 'scores) => #(90 95 83 97 93)) + (check (json-ref j1 'user 'data 'settings 'theme) => "dark")) + +; Compare transform (reduce) and set +(let* ((j0 '((user . ((profile . ((name . "Alice") + (age . 25) + (scores . #(85 90 78)))))))) + (j1 (json-reduce j0 'user 'profile 'scores (lambda (k v) + (vector-map (lambda (score) (+ score 5)) v)))) + (j2 (json-set j0 'user 'profile 'scores #(90 95 83)))) + (check (json-ref j1 'user 'profile 'scores) => #(90 95 83)) + (check (json-ref j2 'user 'profile 'scores) => #(90 95 83)) + (check (json-ref j1 'user 'profile 'name) => "Alice") + (check (json-ref j2 'user 'profile 'name) => "Alice")) + +(let ((json '())) + (check (json-reduce json 'name (lambda (k v) v)) + => '())) + +(let ((json #())) + (check (json-reduce json 'name (lambda (k v) v)) + => #())) + +(let1 json '((person . ((name . "Alice") + (age . 25) + (address . ((city . "Wonderland") + (zip . "12345")))))) + (let1 updated-json (json-reduce json 'person 'address 'city (lambda (x y) (string-upcase y))) + (check (json-ref updated-json 'person 'address 'city) => "WONDERLAND"))) + + + +#| +json-push +向JSON对象中添加新的键值对。 + +语法 +---- +(json-push json key1 key2 ... value) + +参数 +---- +key1, key2, ... : symbol | string | number | boolean +用于定位嵌套位置的键路径。 + +value : any +要添加的值。 + +返回值 +----- +返回新的JSON对象,包含新增的键值对。 + +功能 +---- +- 支持多层嵌套添加 +- 如果键已存在,会覆盖原有值 (对于对象) +- 如果键不存在,会创建新的键值对 +- 支持符号、字符串、数字和布尔值作为键 +|# +; json-push +; 多层,键为符号 +(let* ((j0 '((person . ((name . "Alice") (age . 25))))) + (j1 (json-push j0 'person 'city "Wonderland"))) + (check (json-ref j1 'person 'city) => "Wonderland")) + +; 多层,键为字符串 +(let* ((j0 '(("person" . (("name" . "Alice") ("age" . 25))))) + (j1 (json-push j0 "person" "city" "Wonderland"))) + (check (json-ref j1 "person" "city") => "Wonderland")) + +; 多层,键为符号 +(let* ((j0 '((person . ((name . "Alice") + (age . 25) + (address . ((city . "Oldland") + (zip . "12345"))))))) + (j1 (json-push j0 'person 'address 'street "Main St"))) + (check (json-ref j1 'person 'address 'street) => "Main St")) + +; 多层,JSON是向量 +(let* ((j0 '((data . #(1 2 3)))) + (j1 (json-push j0 'data 3 4))) + (check (json-ref j1 'data) => #(1 2 3 4))) + +; 多层,JSON是二维向量 +(let* ((j0 '((data . #(#(1 2) #(3 4))))) + (j1 (json-push j0 'data 1 2 5))) + ;; 索引1是 #(3 4),push key 2 val 5 -> #(3 4 5) + (check (json-ref j1 'data) => #(#(1 2) #(3 4 5)))) + +(let* ((j0 '((flags . ((#t . "true") (#f . "false"))))) + (j1 (json-push j0 'flags #t "yes"))) + (check (json-ref j1 'flags #t) => "yes")) + + + +#| +json-drop (Deep Drop) +从 JSON 数据结构中删除指定的元素。 + +语法 +---- +1. 路径删除模式(Deep Delete): + (json-drop json key1 key2 ... target-key) + +2. 谓词删除模式(Shallow Filter): + (json-drop json predicate-fn) + +参数 +---- +key1, key2, ... : symbol | string | number | boolean + 用于定位要删除元素的层级路径。 + - 最后一个参数是要删除的目标键(对象)或索引(数组)。 + - 前面的参数是导航路径。 + +predicate-fn : function (lambda (key) ...) + 用于筛选要删除项的谓词函数。 + - 接收参数: + * 对于对象 (Object):接收 **键名 (Key)** (通常是 Symbol)。 + * 对于数组 (Array):接收 **索引 (Index)** (整数)。 + - 返回值:如果不希望保留该项(即希望删除),返回 #t;否则返回 #f。 + - 注意:此函数**不接收**元素的值 (Value)。 + +返回值 +----- +返回一个新的数据结构,其中指定的元素已被移除。 + +功能 +---- +1. **路径删除**: + - 支持多层嵌套定位。 + - 就像文件系统路径一样,精准打击并删除路径末端的一个元素。 + - 如果路径不存在,操作无效,返回原对象。 + +2. **谓词删除**: + - 仅作用于**当前层级**(浅层)。 + - 批量删除所有满足条件的项。 + +示例 +---- +;; 路径删除:删除 person 下 address 里的 zip 字段 +(json-drop j 'person 'address 'zip) + +;; 谓词删除(对象):删除所有键名为 string 类型或特定名称的键 +(json-drop j (lambda (k) (eq? k 'age))) +|# + + + +(let* ((json '((name . "Alice") (age . 25)))) + (let ((updated-json (json-drop json 'age))) + (check (json-ref updated-json 'age) => '()))) + +(let* ((json '((name . "Alice") + (age . 25) + (address . ((city . "Wonderland") + (zip . "12345")))))) + (let ((updated-json (json-drop json 'address 'city))) + (check (json-ref updated-json 'address 'city) => '()))) + +(let* ((json '((name . "Alice") + (age . 25) + (address . ((city . "Wonderland") + (zip . "12345")))))) + (let1 j1 (json-drop json (lambda (k) (equal? k 'city))) + (check (json-ref j1 'address 'city) => "Wonderland")) ; city在address下,顶层drop不影响 + (let1 j2 (json-drop json (lambda (k) (equal? k 'name))) + (check (json-ref j2 'name) => '())) + (let1 j3 (json-drop json 'address (lambda (k) (equal? k 'city))) + (check (json-ref j3 'address 'city) => '()))) + +(let* ((j0 '((name . "Alice") (age . 25) (city . "Wonderland"))) + (j1 (json-drop j0 'age))) + (check (json-ref j1 'age) => '()) + (check (json-ref j1 'name) => "Alice") + (check (json-ref j1 'city) => "Wonderland")) + +(let* ((j0 '((user . ((profile . ((name . "Alice") + (age . 25) + (scores . #(85 90 78)))))))) + (j1 (json-drop j0 'user 'profile 'scores))) + (check (json-ref j1 'user 'profile 'scores) => '()) + (check (json-ref j1 'user 'profile 'name) => "Alice") + (check (json-ref j1 'user 'profile 'age) => 25)) + +(let* ((j0 '((data . #(1 2 3 4 5)))) + (j1 (json-drop j0 'data (lambda (k) (and (number? k) (even? k)))))) + ;; 删除偶数索引: 0(1), 2(3), 4(5). 剩下索引 1(2), 3(4). + (check (json-ref j1 'data) => #(2 4))) + +(let* ((j0 '((settings . (("theme" . "dark") + (notifications . #t) + ("language" . "en"))))) + (j1 (json-drop j0 'settings (lambda (k) (string? k))))) + (check (json-ref j1 'settings "theme") => '()) + (check (json-ref j1 'settings "language") => '())) + +(let* ((j0 '((a . 1) (b . 2) (c . 3))) + (j1 (json-drop j0 (lambda (k) (member k '(a c)))))) + (check (json-ref j1 'a) => '()) + (check (json-ref j1 'b) => 2) + (check (json-ref j1 'c) => '())) + + +(let* ((j0 #()) + (j1 (json-drop j0 0))) + (check j1 => #())) + + + +(let1 json '((person . ((name . "Alice") + (age . 25) + (address . ((city . "Wonderland") + (zip . "12345")))))) + (let1 updated-json (json-reduce json 'person 'address 'city (lambda (x y) (string-upcase y))) + (check (json-ref updated-json 'person 'address 'city) => "WONDERLAND"))) + + +; json-push with objects +(let* ((j0 `((person . ((name . "Alice") (age . 25))))) + (j1 "Wonderland") + (j2 (json-push j0 'person 'city j1))) + (check (json-ref j2 'person 'city) => "Wonderland")) + +(let* ((j0 `((person . ((name . "Alice") (age . 25))))) + (j1 `((city . "Wonderland") (zip . "12345"))) + (j2 (json-push j0 'person 'address j1))) + (check (json-ref j2 'person 'address 'city) => "Wonderland") + (check (json-ref j2 'person 'address 'zip) => "12345")) + + + +; Test with nested objects +(let* ((j0 `((person . ((name . "Alice") (age . 25))))) + (j1 `((address . ((city . "Wonderland") (zip . "12345"))))) + (j2 (json-set j0 'person j1))) + (check (json-ref j2 'person 'address 'city) => "Wonderland") + (check (json-ref j2 'person 'address 'zip) => "12345")) + +; Test with mixed objects and primitive values +(let* ((j0 `((person . ((name . "Alice") (age . 25))))) + (j1 "Wonderland") + (j2 (json-set (json-push j0 'person 'city j1) 'person 'age 26))) + (check (json-ref j2 'person 'city) => "Wonderland") + (check (json-ref j2 'person 'age) => 26)) + +; Test with null +(let* ((j0 `((person . ((name . "Alice") (age . 25))))) + (j1 'null) + (j2 (json-set j0 'person 'age j1))) + (check (json-ref j2 'person 'age) => '())) + +; Test with boolean +(let* ((j0 `((person . ((name . "Alice") (age . 25))))) + (j1 'true) + (j2 (json-push j0 'person 'active j1))) + (check (json-ref j2 'person 'active) => #t)) + +; Test with array +(let* ((j0 `((person . ((name . "Alice") (age . 25))))) + (j1 #(1 2 3)) + (j2 (json-push j0 'person 'scores j1))) + (check (json-ref j2 'person 'scores) => #(1 2 3))) + +(check + (json->string + `(("messages" . #((("role" . "user") ("content" . #(1 2 3))) + (("role" . "user") ("content" . "中文")))))) + => "{\"messages\":[{\"role\":\"user\",\"content\":[1,2,3]},{\"role\":\"user\",\"content\":\"中文\"}]}") + +(check + (json->string + `(("messages" . #( + (("role" . "user") ("content" . #( + (("text" . "1") ("type" . "text")) + (("text" . "2") ("type" . "text")) + ))) + (("role" . "user") ("content" . "中文")) + )))) + => "{\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"1\",\"type\":\"text\"},{\"text\":\"2\",\"type\":\"text\"}]},{\"role\":\"user\",\"content\":\"中文\"}]}" +) + +#| +Error Handling Tests +|# + +;; json-ref invalid structure +(check-catch 'type-error (json-ref "not-a-json" 'key)) +(check-catch 'type-error (json-ref 123 'key)) + +;; json-set invalid structure +(check-catch 'type-error (json-set "not-a-json" 'key "val")) +(check-catch 'type-error (json-set 123 'key "val")) + +;; json-push invalid structure +(check-catch 'type-error (json-push "not-a-json" 'key "val")) +(check-catch 'type-error (json-push 123 'key "val")) + +;; json-drop invalid structure +(check-catch 'type-error (json-drop "not-a-json" 'key)) +(check-catch 'type-error (json-drop 123 'key)) + +;; json-reduce invalid structure +(check-catch 'type-error (json-reduce "not-a-json" 'key (lambda (k v) v))) +(check-catch 'type-error (json-reduce 123 'key (lambda (k v) v))) + +(check-report) \ No newline at end of file diff --git a/tests/goldfish/liii/raw-string-test.scm b/tests/goldfish/liii/raw-string-test.scm new file mode 100644 index 0000000000000000000000000000000000000000..f133b0b69d4a12f68e610cab9791f6aba1d0635a --- /dev/null +++ b/tests/goldfish/liii/raw-string-test.scm @@ -0,0 +1,310 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(import (liii check) + (liii raw-string)) + +(check-set-mode! 'report-failed) + +(check #"""" => "") +(check #"" "" => " ") +(check #""a"" => "a") +(check #""\"" => "\\") +(check #"-"""-" => "\"") +(check #"-"\""-" => "\\\"") +(check #"-"#"()""-" => "#\"()\"") +(check #"-"#""a"""-" => "#\"\"a\"\"") +(check #"-"ends with \""-" => "ends with \\\"") + +(check + #""multiline +string"" + => "multiline\nstring") + +(check + #"" + no whitespace stripping"" + => "\n no whitespace stripping") + +(check + #"" + no whitespace stripping + "" + => "\n no whitespace stripping\n ") + +(check + #"" + 注释 ;; comment + "" + => "\n 注释 ;; comment\n ") + +(check + #"HTML" + + + "测试页面" + +

这里有很多"引号"

+ + + "HTML" + => "\n\n\n \"测试页面\"\n \n

这里有很多\"引号\"

\n \n\n ") + +(check + #"HTML" + + "测试页面" + +

这里有很多"引号"

+ + + "HTML" + => "\n\n \"测试页面\"\n \n

这里有很多\"引号\"

\n \n\n ") + +(check + #"HTML" + + "测试页面" + +

这里有很多"引号"

+ +"HTML" + => "\n\n \"测试页面\"\n \n

这里有很多\"引号\"

\n \n") + +;; ==================== +;; deindent / &- +;; ==================== + +; Basic functionality tests for &- macro +(check + (&- #"" + 多行 + 保留与最后一样一致的换行和缩进(空格) + 使用 deindent/&- 来对齐缩进 + "") + => "多行\n保留与最后一样一致的换行和缩进(空格)\n使用 deindent/&- 来对齐缩进") + +; Test with empty string +(check (&- #"" + "") => "") + +(check (&- #"" + + "") => "") + +; error: Raw string must start on a new line after the opening delimiter +(check-catch 'value-error (&- #"" "")) +(check-catch 'value-error (&- #"" hello "")) + +(check + (&- #"" + 第一行 + 第二行 + 第三行 + "") + => "第一行\n第二行\n第三行") + +(check + (&- #"" + 第一行 + 第二行(缩进2格) + 第三行(缩进4格) + 第四行 + "") + => "第一行\n 第二行(缩进2格)\n 第三行(缩进4格)\n第四行") + +(check + (&- #"" + 外层 + 内层1 + 更内层 + 内层2 + 外层结束 + "") + => "外层\n 内层1\n 更内层\n 内层2\n外层结束") + +(check + (&- #"" + 第一行 + + 第三行(上下有空行) + + 第五行(上有空行) + "") + => "第一行\n\n第三行(上下有空行)\n\n第五行(上有空行)") + +; does not support tab: Line 2 does not start with the same whitespace as the closing line of the raw string +(check-catch 'value-error + (&- #"" + 第一行 + 第二行(使用制表符) + 第三行(混合缩进) + "")) + +; Test with special characters +(check + (&- #"" + "引号内的文本" + '单引号' + `反引号` + 特殊字符:!@#$%^&*()_+-={}[]|\:;"'<>,.?/ + "") + => "\"引号内的文本\"\n'单引号'\n`反引号`\n特殊字符:!@#$%^&*()_+-={}[]|\\:;\"'<>,.?/") + +; Test with Unicode characters +(check + (&- #"" + Hello 世界 + 🌍🌎🌏 + Emoji测试 🚀🎉 + 中文、English、にほんご + "") + => "Hello 世界\n🌍🌎🌏\nEmoji测试 🚀🎉\n中文、English、にほんご") + +(check + (&- #"" + (define (factorial n) + (if (<= n 1) + 1 + (* n (factorial (- n 1))))) + + (define (fibonacci n) + (cond + ((= n 0) 0) + ((= n 1) 1) + (else (+ (fibonacci (- n 1)) + (fibonacci (- n 2)))))) + "") + => "(define (factorial n)\n (if (<= n 1)\n 1\n (* n (factorial (- n 1)))))\n\n(define (fibonacci n)\n (cond\n ((= n 0) 0)\n ((= n 1) 1)\n (else (+ (fibonacci (- n 1))\n (fibonacci (- n 2))))))") + +(check + (&- #"HTML" +
+

标题

+

段落内容

+
    +
  • 项目1
  • +
  • 项目2
  • +
+
+ "HTML") + => "
\n

标题

\n

段落内容

\n
    \n
  • 项目1
  • \n
  • 项目2
  • \n
\n
") + +(check + (&- #"" + SELECT + users.id, + users.name, + COUNT(orders.id) as order_count + FROM users + LEFT JOIN orders ON users.id = orders.user_id + WHERE users.active = TRUE + GROUP BY users.id, users.name + ORDER BY order_count DESC + "") + => "SELECT \n users.id,\n users.name,\n COUNT(orders.id) as order_count\nFROM users\nLEFT JOIN orders ON users.id = orders.user_id\nWHERE users.active = TRUE\nGROUP BY users.id, users.name\nORDER BY order_count DESC") + +(check + (&- #"" + { + "name": "测试", + "version": "1.0.0", + "dependencies": { + "library1": "^1.2.3", + "library2": "~2.0.0" + }, + "scripts": { + "start": "node index.js", + "test": "jest" + } + } + "") + => "{\n \"name\": \"测试\",\n \"version\": \"1.0.0\",\n \"dependencies\": {\n \"library1\": \"^1.2.3\",\n \"library2\": \"~2.0.0\"\n },\n \"scripts\": {\n \"start\": \"node index.js\",\n \"test\": \"jest\"\n }\n}") + +(check + (&- #"" + 第一行(无缩进) + 第二行(缩进2格) + 第三行(缩进3格) + 第四行(又回到2格) + 第五行(回到无缩进) + 第六行(缩进8格,移除 closing line 的缩进) + "") ; closing line + => "第一行(无缩进)\n 第二行(缩进2格)\n 第三行(缩进3格)\n 第四行(又回到2格)\n第五行(回到无缩进)\n 第六行(缩进8格,移除 closing line 的缩进)") + +; Test with trailing spaces in lines +(check + (&- #"" + 行尾有空格 + 行尾有多个空格 + 正常行 + "") + => "行尾有空格 \n行尾有多个空格 \n正常行") + +(check + (&- #"" + 这行前面有0个空格后面也有4个空格 + 这行前面有0个空格 + 这行前面有2个空格 + "") + => " 这行前面有0个空格后面也有4个空格 \n 这行前面有0个空格\n 这行前面有2个空格") + +(check + (&- #"" + 所有行应该对齐到最小缩进级别: + 这一行缩进2格 + 这一行缩进4格 + 这一行又回到2格 + 这一行没有缩进 + "") + => "所有行应该对齐到最小缩进级别:\n 这一行缩进2格\n 这一行缩进4格\n 这一行又回到2格\n这一行没有缩进") + +(check + (&- #"" + + 只有这一行 + + "") + => "\n只有这一行\n") + +(check + (&- #"" + 这是一个较长的文本段落, + 用来测试&-宏处理多行长文本的能力。 + 文本可以包含各种标点符号, + 如逗号、句号、问号?感叹号! + 也可以包含数字:1234567890 + 以及各种括号:()[]{}<> + 测试结束。 + "") + => "这是一个较长的文本段落,\n用来测试&-宏处理多行长文本的能力。\n文本可以包含各种标点符号,\n如逗号、句号、问号?感叹号!\n也可以包含数字:1234567890\n以及各种括号:()[]{}<>\n测试结束。") + +(check + (&- #"" + 行1 + 行2 + 行3 + 行4 + 行5 + 行6 + 行7 + 行8 + 行9 + 行10 + "") + => "行1\n行2\n行3\n行4\n行5\n行6\n行7\n行8\n行9\n行10") + +(check-report) diff --git a/tests/goldfish/liii/rich-either-test.scm b/tests/goldfish/liii/rich-either-test.scm new file mode 100644 index 0000000000000000000000000000000000000000..d5fe6ac6920234c1713e9d1688bad5a37a0ee3c9 --- /dev/null +++ b/tests/goldfish/liii/rich-either-test.scm @@ -0,0 +1,398 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(import (liii check) + (liii rich-either) + (liii option) + (liii lang) + (liii error)) + +(check-set-mode! 'report-failed) +(define either rich-either) +;; ========================================== +;; 1. 基础构造与类型判断测试 +;; ========================================== + +#| +left / right +构造函数:创建 Rich Either 实例。 + +语法 +---- +(left value) +(right value) + +描述 +---- +- `left`: 创建一个表示失败或错误的 Either 实例。 +- `right`: 创建一个表示成功或有效值的 Either 实例。 +|# + +#| +:left? / :right? +成员方法:判断 Either 的类型。 + +语法 +---- +(obj :left?) +(obj :right?) + +返回值 +------ +boolean +|# + +#| +:get +成员方法:提取内部值。 + +语法 +---- +(obj :get) + +描述 +---- +无论对象是 Left 还是 Right,直接返回其内部存储的值。 +|# +;; 测试 left 和 right 构造函数 +(check-true ((left "error") :left?)) +(check-false ((left "error") :right?)) + +(check-true ((right "success") :right?)) +(check-false ((right "success") :left?)) + +;; 测试 get 方法 +(check ((left "error") :get) => "error") +(check ((right 42) :get) => 42) + +;; ========================================== +;; 2. or-else 和 get-or-else 测试 +;; ========================================== + +#| +:or-else +成员方法:对象级别的备选方案。 + +语法 +---- +(obj :or-else alternative) + +参数 +---- +alternative : rich-either + 当 obj 为 Left 时返回的备用 Either 对象。 + +返回值 +------ +rich-either + - 如果 obj 为 Right,返回 obj 自身。 + - 如果 obj 为 Left,返回 alternative。 +|# + +#| +:get-or-else +成员方法:值级别的安全提取。 + +语法 +---- +(obj :get-or-else default) + +参数 +---- +default : any | procedure + 当 obj 为 Left 时返回的默认值,或者一个无参函数(Thunk)。 + +返回值 +------ +any + - 如果 obj 为 Right,返回内部值。 + - 如果 obj 为 Left,且 default 是函数,返回 (default) 的结果;否则返回 default。 +|# +;; 测试 %or-else +(let ((right-val (right 1)) + (left-val (left 0)) + (backup (right 2))) + ;; Right 返回自身 + (check ((right-val :or-else backup) :get) => 1) + ;; Left 返回备选方案 + (check ((left-val :or-else backup) :get) => 2)) + +;; 测试 %get-or-else +(check ((right 42) :get-or-else 0) => 42) +(check ((left "error") :get-or-else 0) => 0) +;; 测试函数作为默认值 +(check ((left "error") :get-or-else (lambda () 99)) => 99) + +;; ========================================== +;; 3. filter-or-else 测试 +;; ========================================== + +#| +:filter-or-else +成员方法:条件过滤。 + +语法 +---- +(obj :filter-or-else predicate zero) + +参数 +---- +predicate : procedure (any -> boolean) + 用于测试 Right 值的谓词函数。 +zero : any + 当过滤失败(即谓词返回 false)时,用于构建新 Left 的值。 + +描述 +---- +- 如果 obj 是 Right 且 (predicate value) 为真:返回 obj 自身。 +- 如果 obj 是 Right 且 (predicate value) 为假:返回 (left zero)。 +- 如果 obj 是 Left:返回 obj 自身。 +|# +;; Right 且满足条件时返回自身 +(let ((r (right 10))) + (check ((r :filter-or-else (lambda (x) (> x 5)) 0) :get) => 10)) + +;; Right 但不满足条件时返回 left +(let ((r (right 3))) + (check-true ((r :filter-or-else (lambda (x) (> x 5)) 0) :left?)) + (check ((r :filter-or-else (lambda (x) (> x 5)) 0) :get) => 0)) + +;; Left 时返回自身 +(let ((l (left "error"))) + (check ((l :filter-or-else (lambda (x) #t) 0) :get) => "error")) + +;; ========================================== +;; 4. contains 测试 +;; ========================================== + +#| +:contains +成员方法:检查是否包含特定值。 + +语法 +---- +(obj :contains target) + +描述 +---- +仅当 obj 是 Right 类型,且其内部值与 target 相等(使用 class=? 比较)时,返回 #t。 +Left 类型总是返回 #f。 +|# +(check-true ((right 42) :contains 42)) +(check-false ((right 42) :contains 43)) +(check-false ((left "error") :contains "error")) + + +;; ========================================== +;; 5. for-each 测试 +;; ========================================== + +#| +:for-each +成员方法:副作用遍历。 + +语法 +---- +(obj :for-each proc) + +描述 +---- +如果 obj 是 Right,则对其值执行 proc。 +如果 obj 是 Left,不执行任何操作。 +|# +(let ((counter 0) + (right-val (right 5)) + (left-val (left "error"))) + ;; Right 执行副作用 + (begin + (right-val :for-each (lambda (x) (set! counter (+ counter x)))) + (check counter => 5)) + ;; Left 不执行副作用 + (begin + (left-val :for-each (lambda (x) (set! counter (+ counter 10)))) + (check counter => 5))) + +;; ========================================== +;; 6. to-option 测试 +;; ========================================== + +#| +:to-option +成员方法:类型转换。 + +语法 +---- +(obj :to-option) + +返回值 +------ +option + - Right 值转换为 (option value)。 + - Left 值转换为 (none)。 +|# +;; Right 转换为 defined option +(let ((opt ((right 42) :to-option))) + (check-true (opt :defined?)) + (check (opt :get) => 42)) + +;; Left 转换为 empty option +(let ((opt ((left "error") :to-option))) + (check-true (opt :empty?))) + +;; ========================================== +;; 7. map 测试 +;; ========================================== + +#| +:map +成员方法:Functor 映射。 + +语法 +---- +(obj :map func . args) + +描述 +---- +如果 obj 是 Right,应用 func 到其值上,并返回包装了新值的 Right。 +如果 obj 是 Left,直接返回自身。 +支持链式调用参数 args。 +|# +;; 对 Right 应用 map +(let ((result ((right 5) :map (lambda (x) (* x 2))))) + (check-true (result :right?)) + (check (result :get) => 10)) + +;; 对 Left 应用 map 返回自身 +(let ((l (left "error"))) + (check ((l :map (lambda (x) (string-append "Mapped: " x))) :get) => "error")) + +;; ========================================== +;; 8. flat-map 测试 +;; ========================================== + +#| +:flat-map +成员方法:Monad 绑定。 + +语法 +---- +(obj :flat-map func . args) + +描述 +---- +如果 obj 是 Right,应用 func(必须返回 Either)到其值上,并返回该结果。 +如果 obj 是 Left,直接返回自身。 +|# +;; 对 Right 应用 flat-map +(let ((result ((right 5) :flat-map (lambda (x) (right (* x 2)))))) + (check-true (result :right?)) + (check (result :get) => 10)) + +;; 对 Left 应用 flat-map 返回自身 +(let ((l (left "error"))) + (check ((l :flat-map (lambda (x) (right (string-length x)))) :get) => "error")) + +;; ========================================== +;; 9. forall 和 exists 测试 +;; ========================================== + +#| +:forall +成员方法:全称量词检查。 + +语法 +---- +(obj :forall predicate) + +描述 +---- +- 如果 obj 是 Right,返回 (predicate value)。 +- 如果 obj 是 Left,返回 #t (真空真)。 +|# + +#| +:exists +成员方法:存在量词检查。 + +语法 +---- +(obj :exists predicate) + +描述 +---- +- 如果 obj 是 Right,返回 (predicate value)。 +- 如果 obj 是 Left,返回 #f。 +|# +;; forall: Right 且满足条件时为真 +(check-true ((right 10) :forall (lambda (x) (> x 5)))) +(check-false ((right 3) :forall (lambda (x) (> x 5)))) +;; forall: Left 总是为真 +(check-true ((left "error") :forall (lambda (x) #f))) + +;; exists: Right 且满足条件时为真 +(check-true ((right 10) :exists (lambda (x) (> x 5)))) +(check-false ((right 3) :exists (lambda (x) (> x 5)))) +;; exists: Left 总是为假 +(check-false ((left "error") :exists (lambda (x) #t))) + +;; ========================================== +;; 10. 类型兼容性测试 +;; ========================================== + +;; 测试 either 是 rich-either 的别名 +(check-true (either :is-type-of (left "test"))) +(check-true (either :is-type-of (right "test"))) + +;; ========================================== +;; 11. 错误处理测试 +;; ========================================== + +#| +错误处理 +检查非法参数是否能正确抛出 type-error。 +|# + +;; 测试 %or-else 参数类型检查 +(check-catch 'type-error ((right 1) :or-else "not-an-either")) + +;; 测试 %filter-or-else 参数类型检查 +(check-catch 'type-error ((right 1) :filter-or-else "not-a-procedure" 0)) + +;; 测试 %forall 参数类型检查 +(check-catch 'type-error ((right 1) :forall "not-a-procedure")) + +;; 测试 %exists 参数类型检查 +(check-catch 'type-error ((right 1) :exists "not-a-procedure")) + +;; ========================================== +;; 12. 综合流程测试 +;; ========================================== + +;; 测试链式操作 +(let* ((val1 (right 10)) + (val2 (val1 :map (lambda (x) (+ x 5)))) ;; Right 15 + (val3 (val2 :flat-map (lambda (x) (right (* x 2)))))) ;; Right 30 + (check-true (val3 :right?)) + (check (val3 :get) => 30)) + +;; 测试错误处理流程 +(let* ((error-val (left "network error")) + ;; 下面的 map 不应执行,因为输入已经是 Left + (mapped-error (error-val :map (lambda (x) (string-append "Error: " x))))) + (check-true (mapped-error :left?)) + (check (mapped-error :get) => "network error")) + +(check-report) \ No newline at end of file diff --git a/tests/goldfish/liii/rich-json-test.scm b/tests/goldfish/liii/rich-json-test.scm new file mode 100644 index 0000000000000000000000000000000000000000..d1bb4afc3511dde73db1cae8becb29a7723e8223 --- /dev/null +++ b/tests/goldfish/liii/rich-json-test.scm @@ -0,0 +1,1080 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(import (liii check) + (liii rich-json) + (liii rich-char) + (liii base) + (liii error)) + +; comment this line to show detailed check reports +; (check-set-mode! 'report-failed) + +; shared test data +(define bob-j (rich-json + '((bob . ((age . 18) + (sex . male) + (name . "Bob")))))) + + +#| +rich-json%get +获取JSON对象的值。 + +语法 +---- +(rich-json-instance :get) + +参数 +---- +无参数。 + +返回值 +----- +返回JSON对象的值。 + +功能 +---- +- 对于null类型JSON对象,返回'null +- 对于其他类型JSON对象,返回其原始值 + +性能特征 +-------- +- 时间复杂度:O(1) +- 空间复杂度:O(1) +|# + +;; null +(check (rich-json :apply '() :get) => 'null) +;; 布尔值 +(check (rich-json :apply #t :get) => 'true) +(check (rich-json :apply #f :get) => 'false) +;; 数字 +(check (rich-json :apply 2 :get) => '2) +;; 字典。 +(check (rich-json :apply `(("name" . "Bob") ("age" . 21)) :get) => `(("name" . "Bob") ("age" . 21))) +;; 数组 +(check (rich-json :apply #(1 2 3) :get) => #(1 2 3)) + +#| +rich-json%get-or-else +获取JSON对象的值,如果为null则返回默认值。 + +语法 +---- +(rich-json-instance :get-or-else default-value) + +参数 +---- +default-value : any +当JSON对象为null时返回的默认值。 + +返回值 +----- +- 如果JSON对象不为null,返回其值 +- 如果JSON对象为null,返回default-value + +功能 +---- +提供安全的默认值机制,避免处理null值时出错。 +|# + +(check (rich-json :null :get-or-else bob-j) => bob-j) + +#| +rich-json%keys +获取JSON对象的所有键名。 + +语法 +---- +(rich-json-instance :keys) + +参数 +---- +无参数。 + +返回值 +----- +返回JSON对象的所有键名列表。 + +功能 +---- +- 对于对象类型JSON,返回所有键名的列表 +- 对于数组、null、布尔值等非对象类型,返回空列表 +|# + + + +(let1 j (rich-json '((bob . ((age . 18) (sex . male))))) + (check (j :keys) => '(bob)) + (check ((j 'bob) :keys) => '(age sex))) + +(check ((rich-json :null) :keys) => '()) +(check ((rich-json :true) :keys) => '()) +(check ((rich-json :false) :keys) => '()) +(check ((rich-json :parse "[1,2,3]") :keys) => '()) + +#| +rich-json%apply +通过键路径访问JSON对象的嵌套值。 + +语法 +---- +(rich-json-instance key1 key2 ...) + +参数 +---- +key1, key2, ... : symbol | string | number | boolean +用于访问嵌套值的键路径。 + +返回值 +----- +返回指定键路径对应的JSON对象。 + +功能 +---- +- 支持多层嵌套访问 +- 如果键不存在,返回null类型的JSON对象 +- 支持符号、字符串、数字和布尔值作为键 +|# +(check (bob-j 'bob 'age) => (rich-json 18)) +(check (bob-j 'bob 'sex) => (rich-json 'male)) +(check (bob-j 'alice) => (rich-json :null)) +(check (bob-j 'alice 'age) => (rich-json :null)) +(check (bob-j 'bob 'name) => (rich-json "Bob")) + +(let1 j (rich-json '((bob . ((age . 18) (sex . male))))) + (check ((j 'alice) :null?) => #t) + (check ((j 'bob) :null?) => #f)) +(let1 j (rich-json '((alice . ((age . 18) (sex . male))))) + (check ((j 'alice) :null?) => #f) + (check ((j 'bob) :null?) => #t)) + + +#| +rich-json%contains-key? +检查JSON对象是否包含指定的键。 + +语法 +---- +(rich-json-instance :contains-key? key) + +参数 +---- +key : symbol | string | number | boolean +要检查的键名。 + +返回值 +----- +返回布尔值: +- #t:如果键存在 +- #f:如果键不存在 + +功能 +---- +- 仅检查当前层级的键 +- 不检查嵌套对象中的键 +- 对于非对象类型JSON,总是返回#f +|# + +(let1 j (rich-json '((bob . ((age . 18) (sex . male))))) + (check-false (j :contains-key? 'alice)) + (check-true (j :contains-key? 'bob)) + (check-false (j :contains-key? 'age)) + (check-false (j :contains-key? 'sex))) + +(let1 j (rich-json #(1 2 3)) + (check (j :to-string) => "[1,2,3]")) + +(check (rich-json '((a (b . 1) (c . 2)))) => (rich-json :parse "{a:{b:1,c:2}}")) +(check (rich-json '((a . ((b . 1) (c . 2))))) => (rich-json :parse "{a:{b:1,c:2}}")) +(check '((a . ((b . 1) (c . 2)))) => '((a (b . 1) (c . 2)))) + +(check-catch 'value-error (json->string '((a)))) + +(check (rich-json 'null)=> (rich-json :null)) + + +#| +rich-json@parse +将json格式的字符串的转化成JSON对象。 + +语法 +---- +(rich-json :parse json_string) + +参数 +---- +json_string : json格式的字符串 + + +返回值 +----- +返回对应的JSON对象。 + +功能 +---- +- 将json格式的字符串的转化成JSON对象。 +- 包含object、array、string、number、“true”、“false”、“null” +|# +(check (rich-json :parse "[]") => (rich-json :apply #())) +(check (rich-json :parse "[true]") => (rich-json :apply #(true))) +(check (rich-json :parse "[false]") => (rich-json :apply #(false))) +(check (rich-json :parse "[1,2,3]") => (rich-json :apply #(1 2 3))) +(check (rich-json :parse "[{data: 1},{}]") => (rich-json :apply #(((data . 1)) (()))));; 数组里面有对象 +(check (rich-json :parse "{}") => (rich-json :apply '(()))) +(check (rich-json :parse "{args: {}}") => (rich-json :apply '((args ())))) +(check (rich-json :parse "{\"args\": {}}") => (rich-json :apply '(("args" ())))) +(check (rich-json :parse "{\"args\": {}, data: 1}") => (rich-json :apply '(("args" ()) (data . 1)))) +(check (rich-json :parse "{\"args\": {}, data: [1,2,3]}") => (rich-json :apply '(("args" ()) (data . #(1 2 3))))) ;;JSON对象的值是数组 +(check (rich-json :parse "{\"args\": {}, data: true}") => (rich-json :apply '(("args" ()) (data . true)))) +(check (rich-json :parse "{\"args\": {}, data: null}") => (rich-json :apply '(("args" ()) (data . null)))) + +;; todo bug需要修复 +; (check (rich-json :parse "[null]") => (rich-json :apply #())) +; (check (rich-json :parse "[true],[true]") => (rich-json :apply #t)) +; (check (rich-json :parse "{\"args\": {}, data: [true]}") => (rich-json :apply '(("args" ()) (data . #t)))) +; (check (rich-json :parse "{\"args\": {}, data: [null]}") => (rich-json :apply '(("args" ()) (data . '())))) + + +#| +json-string-escape +将字符串转换为JSON格式的转义字符串。 + +语法 +---- +(json-string-escape string) + +参数 +---- +string : string +要转义的原始字符串。 + +返回值 +----- +返回JSON格式的转义字符串,包含双引号。 + +功能 +---- +- 转义JSON特殊字符:\" \\ \/ \b \f \n \r \t +- 添加双引号包围 +- 支持Unicode字符 +- 优化处理Base64等安全字符串 + +边界条件 +-------- +- 空字符串返回"" +- 包含特殊字符的字符串会被正确转义 +- 支持中文字符和Unicode转义 + +性能特征 +-------- +- 时间复杂度:O(n),n为字符串长度 +- 空间复杂度:O(n),存储转义后的字符串 + +兼容性 +------ +- 符合JSON标准 +- 支持所有可打印Unicode字符 +|# +; Basic json-string-escape tests +(check (json-string-escape "hello") => "\"hello\"") +(check (json-string-escape "hello\"world") => "\"hello\\\"world\"") +(check (json-string-escape "hello\\world") => "\"hello\\\\world\"") +(check (json-string-escape "hello/world") => "\"hello\\/world\"") +(check (json-string-escape "hello\bworld") => "\"hello\\bworld\"") +(check (json-string-escape "hello\fworld") => "\"hello\\fworld\"") +(check (json-string-escape "hello\nworld") => "\"hello\\nworld\"") +(check (json-string-escape "hello\rworld") => "\"hello\\rworld\"") +(check (json-string-escape "hello\tworld") => "\"hello\\tworld\"") + +; Extended json-string-escape tests for optimized version +; Test empty string +(check (json-string-escape "") => "\"\"") + +; Test single character strings +(check (json-string-escape "A") => "\"A\"") +(check (json-string-escape "\"") => "\"\\\"\"") +(check (json-string-escape "\\") => "\"\\\\\"") + +; Test Base64-like strings (should use fast path for large strings) +(check (json-string-escape "ABC") => "\"ABC\"") +(check (json-string-escape "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=") + => "\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=\"") + +; Test typical Base64 encoded string (short) - these will use slow path now +(check (json-string-escape "SGVsbG8gV29ybGQ=") => "\"SGVsbG8gV29ybGQ=\"") +(check (json-string-escape "VGhpcyBpcyBhIHRlc3Q=") => "\"VGhpcyBpcyBhIHRlc3Q=\"") + +; Test Base64 with padding +(check (json-string-escape "QWxhZGRpbjpvcGVuIHNlc2FtZQ==") => "\"QWxhZGRpbjpvcGVuIHNlc2FtZQ==\"") + +; Test large Base64-like string WITHOUT slashes (should trigger fast path optimization) +; This is a 1024-character Base64-like string without slashes +(let ((large-base64 + (string-append + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+=" + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567"))) + (check (json-string-escape large-base64) + => (string-append "\"" large-base64 "\""))) + +; Test mixed content strings (should NOT use fast path) +(check (json-string-escape "Hello123+=") => "\"Hello123+=\"") +(check (json-string-escape "Base64WithNewline\nText") => "\"Base64WithNewline\\nText\"") +(check (json-string-escape "Base64With\"Quote") => "\"Base64With\\\"Quote\"") + +; Test edge cases for sampling logic +; String exactly 1000 chars of Base64 (at threshold, should use slow path) +(let ((threshold-base64 + (make-string 1000 #\A))) ; 1000 'A' characters + (check (json-string-escape threshold-base64) + => (string-append "\"" threshold-base64 "\""))) + +; String just over 1000 chars with Base64 content (should use fast path) +(let ((large-base64-1001 + (string-append (make-string 1001 #\A)))) ; 1001 'A' characters + (check (json-string-escape large-base64-1001) + => (string-append "\"" large-base64-1001 "\""))) + +; String over 1000 chars but with non-Base64 in first 100 chars (should use slow path) +(let ((mixed-large + (string-append "Quote\"InFirst100" (make-string 990 #\A)))) + (check (json-string-escape mixed-large) + => (string-append "\"Quote\\\"InFirst100" (make-string 990 #\A) "\""))) + +; Test numeric strings (Base64-like) +(check (json-string-escape "1234567890") => "\"1234567890\"") +(check (json-string-escape "0123456789ABCDEFabcdef") => "\"0123456789ABCDEFabcdef\"") + +; Test URL-safe Base64 characters (but no slashes) +(check (json-string-escape "URLsafe_Base64chars") => "\"URLsafe_Base64chars\"") + +; Test performance edge case: very long string with escape chars +(let ((long-escaped + (make-string 50 #\"))) ; 50 quote characters + (check (string-length (json-string-escape long-escaped)) => 102)) ; 50*2 + 2 quotes = 102 + +; Test that all Base64 safe characters are recognized correctly (no slashes) +(check (json-string-escape "ABCDEFGHIJKLMNOPQRSTUVWXYZ") => "\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"") +(check (json-string-escape "abcdefghijklmnopqrstuvwxyz") => "\"abcdefghijklmnopqrstuvwxyz\"") +(check (json-string-escape "0123456789") => "\"0123456789\"") +(check (json-string-escape "+=") => "\"+=\"") + +#| +string->json +将JSON字符串解析为Scheme数据结构。 + +语法 +---- +(string->json json-string) + +参数 +---- +json-string : string +要解析的JSON格式字符串。 + +返回值 +----- +返回对应的Scheme数据结构。 + +功能 +---- +- 解析JSON对象、数组、字符串、数字、布尔值、null +- 支持Unicode转义序列 +- 支持代理对解析 +- 自动处理转义字符 + +边界条件 +-------- +- 无效JSON字符串抛出parse-error异常 +- 支持宽松语法(不带引号的键名) +- 支持中文字符和Unicode转义 + +性能特征 +-------- +- 时间复杂度:O(n),n为字符串长度 +- 空间复杂度:O(n),存储解析后的数据结构 + +兼容性 +------ +- 符合JSON标准 +- 支持扩展语法(不带引号的键名) +|# +(check (string->json "{\"age\":18}") => `(("age" . 18))) +(check (string->json "{age:18}") => `((age . 18))) +(check (string->json "{\"name\":\"中文\"}") => `(("name" . "中文"))) + +(check (string->json "{\"name\":\"Alice\\nBob\"}") => '(("name" . "Alice\nBob"))) +(check (string->json "{\"name\":\"Alice\\tBob\"}") => '(("name" . "Alice\tBob"))) +(check (string->json "{\"name\":\"Alice\\rBob\"}") => '(("name" . "Alice\rBob"))) +(check (string->json "{\"name\":\"Alice\\bBob\"}") => '(("name" . "Alice\bBob"))) +(check (string->json "{\"name\":\"Alice\\fBob\"}") => '(("name" . "Alice\fBob"))) +(check (string->json "{\"name\":\"Alice\\\\Bob\"}") => '(("name" . "Alice\\Bob"))) +(check (string->json "{\"name\":\"Alice\\\/Bob\"}") => '(("name" . "Alice/Bob"))) +(check (string->json "{\"name\":\"Alice\\\"Bob\"}") => '(("name" . "Alice\"Bob"))) + +(check (string->json "[\"\\u0041\"]") => #("A")) +(check (string->json "[\"\\u0041\\u0042\"]") => #("AB")) ; 多个 \u 转义字符 +(check (string->json "[\"\\u4E2D\\u6587\"]") => #("中文")) ; 中文字符 +(check (string->json "[\"\\uD83D\\uDE00\"]") => #("😀")) ; 代理对(假设支持代理对) + +(check (string->json "{\"name\":\"\\u4E2D\\u6587\"}") => '(("name" . "中文"))) ; 嵌套 JSON +(check (string->json "{\"emoji\":\"\\uD83D\\uDE00\"}") => '(("emoji" . "😀"))) ; 嵌套 JSON 中的代理对 + +(check-catch 'parse-error (string->json "[\"\\u004G\"]")) ; \u 后包含非十六进制字符 +(check-catch 'parse-error (string->json "[\"\\a\"]")) + +#| +json->string +将Scheme数据结构转换为JSON字符串。 + +语法 +---- +(json->string data) + +参数 +---- +data : any +要转换的Scheme数据结构。 + +返回值 +----- +返回JSON格式的字符串。 + +功能 +---- +- 支持对象、数组、字符串、数字、布尔值、null +- 自动转义特殊字符 +- 支持嵌套数据结构 +- 符合JSON标准格式 + +边界条件 +-------- +- 无效数据结构抛出value-error异常 +- 空对象转换为{} +- 空数组转换为[] + +性能特征 +-------- +- 时间复杂度:O(n),n为数据结构大小 +- 空间复杂度:O(n),存储生成的JSON字符串 + +兼容性 +------ +- 符合JSON标准 +- 支持所有标准JSON数据类型 +|# +(check (json->string '(("age" . 18))) => "{\"age\":18}") +(check (json->string #(0 1 2 3)) => "[0,1,2,3]") + +#| +rich-json%set +设置JSON对象中指定键的值。 + +语法 +---- +(rich-json-instance :set key1 key2 ... value) + +参数 +---- +key1, key2, ... : symbol | string | number | boolean +用于定位嵌套值的键路径。 + +value : any | function +要设置的值或转换函数。 + +返回值 +----- +返回新的JSON对象,包含更新后的值。 + +功能 +---- +- 支持多层嵌套设置 +- 如果键不存在,会创建新的键值对 +- 支持函数作为最后一个参数,用于转换现有值 +- 支持符号、字符串、数字和布尔值作为键 + +边界条件 +-------- +- 不存在的键路径会创建新的嵌套结构 +- 函数参数接收当前值并返回新值 +- 支持任意深度的嵌套设置 + +性能特征 +-------- +- 时间复杂度:O(k),k为键路径长度 +- 空间复杂度:O(n),创建新的JSON对象 + +兼容性 +------ +- 支持链式调用 +|# +; rich-json%set +; 单层,键为符号 +(let* ((j0 (rich-json `((age . 18) (sex . male)))) + (j1 (j0 :set 'age 19)) + (j2 (j0 :set 'age 'null))) + (check (j0 'age) => (rich-json 18)) + (check (j1 'age) => (rich-json 19)) + (check (j2 'age) => (rich-json :null))) + +; 单层,键为字符串 +(let* ((j0-raw `(("age" . 18) ("sex" . male))) + (j0 (rich-json j0-raw)) + (j1 (j0 :set "age" 19))) + (check (j1 :get-number "age" 0) => 19) + (check (j0 "age") => (rich-json 18))) + +#| +rich-json%get-boolean +获取JSON对象中的布尔值,如果值不是布尔类型则返回默认值。 + +语法 +---- +(rich-json-instance :get-boolean key default-value) + +参数 +---- +key : symbol | string | number | boolean +要获取的键名。 + +default-value : boolean +当值不是布尔类型或键不存在时返回的默认值。 + +返回值 +----- +返回布尔值: +- 如果键存在且值为布尔类型,返回该值 +- 否则返回default-value + +功能 +---- +- 安全地获取布尔值,避免类型错误 +- 支持符号、字符串、数字和布尔值作为键 +|# +; rich-json%get-boolean +(let* ((j0 (rich-json '((active . #t) (verified . #f) (name . "Alice"))))) + (check (j0 :get-boolean 'active #f) => #t) + (check (j0 :get-boolean 'verified #t) => #f) + (check (j0 :get-boolean 'name #f) => #f) + (check (j0 :get-boolean 'nonexistent #t) => #t)) + +; 单层,键为整数 +(let* ((j0 (rich-json #(red green blue))) + (j1 (j0 :set 0 'black))) + (check (j0 :get) => #(red green blue)) + (check (j1 :get) => #(black green blue))) + +; 单层,键为布尔值 +(let* ((j0 (rich-json '((bob . 18) (jack . 16)))) + (j1 (j0 :set #t 3)) + (j2 (j0 :set #t (lambda (x) (+ x 1))))) + (check (j1 :get) => '((bob . 3) (jack . 3))) + (check (j2 :get) => '((bob . 19) (jack . 17)))) + +; 多层,键为符号 +(let* ((j0 (rich-json '((person . ((name . "Alice") (age . 25)))))) + (j1 (j0 :set 'person 'age 26))) + (check (j1 'person 'age) => (rich-json 26))) + +; 多层,键为字符串 +(let* ((j0 (rich-json + '((person . ((name . "Alice") + (age . 25) + (address . ((city . "Wonderland") + (zip . "12345")))))))) + (j1 (j0 :set 'person 'address 'city "Newland"))) + (check (j1 'person 'address 'city) => (rich-json "Newland"))) + +; 单层,最后一个参数不是值,而是一个函数 +(let* ((j0 (rich-json '((name . "Alice") (age . 25)))) + (j1 (j0 :set 'age (lambda (x) (+ x 1))))) + (check (j1 'age) => (rich-json 26))) + +; 多层,最后一个参数不是值,而是一个函数 +(let* ((j0 (rich-json '((person . ((name . "Alice") (age . 25)))))) + (j1 (j0 :set 'person 'age (lambda (x) (+ x 1))))) + (check (j1 'person 'age) => (rich-json 26))) + +; rich-json%set with rich-json object +(let* ((j0 (rich-json `((age . 18) (sex . male)))) + (j1 (rich-json 20)) + (j2 (j0 :set 'age j1))) + (check (j2 'age) => (rich-json 20))) + +(let* ((j0 (rich-json `((person . ((name . "Alice") (age . 25)))))) + (j1 (rich-json 26)) + (j2 (j0 :set 'person 'age j1))) + (check (j2 'person 'age) => (rich-json 26))) + +(let* ((j0 (rich-json `((person . ((name . "Alice") (age . 25)))))) + (j1 (rich-json `((name . "Bob") (age . 30)))) + (j2 (j0 :set 'person j1))) + (check (j2 'person 'name) => (rich-json "Bob")) + (check (j2 'person 'age) => (rich-json 30))) + +#| +rich-json%transform +转换JSON对象中指定键的值。 + +语法 +---- +(rich-json-instance :transform key1 key2 ... transform-fn) +(rich-json-instance :transform predicate-fn transform-fn) +(rich-json-instance :transform #t transform-fn) +(rich-json-instance :transform #f transform-fn) + +参数 +---- +key1, key2, ... : symbol | string | number | boolean +用于定位嵌套值的键路径。 + +predicate-fn : function +用于选择要转换的键的谓词函数。 + +transform-fn : function +转换函数,接收键和值,返回新值。 + +返回值 +----- +返回新的JSON对象,包含转换后的值。 + +功能 +---- +- 支持多层嵌套转换 +- 支持谓词函数选择要转换的键 +- 支持#t转换所有键,#f不转换任何键 +- 转换函数接收键和值作为参数 +|# +; rich-json%transform instance method +(let* ((j0 (rich-json '((name . "Alice") (age . 25)))) + (j1 (j0 :transform 'name (lambda (k v) (string-upcase v))))) + (check (j1 'name) => (rich-json "ALICE")) + (check (j1 'age) => (rich-json 25))) + +(let* ((j0 (rich-json '((person . ((name . "Alice") (age . 25)))))) + (j1 (j0 :transform 'person (lambda (k v) v)))) + (check (j1 'person) => (rich-json '((name . "Alice") (age . 25))))) + +(let* ((j0 (rich-json '((name . "Alice") (age . 25)))) + (j1 (j0 :transform (lambda (k) (equal? k 'age)) (lambda (k v) (+ v 1))))) + (check (j1 'age) => (rich-json 26)) + (check (j1 'name) => (rich-json "Alice"))) + +(let* ((j0 (rich-json '((name . "Alice") (age . 25)))) + (j1 (j0 :transform #t (lambda (k v) (if (string? v) (string-upcase v) v))))) + (check (j1 'name) => (rich-json "ALICE")) + (check (j1 'age) => (rich-json 25))) + +(let* ((j0 (rich-json '((name . "Alice") (age . 25)))) + (j1 (j0 :transform #f (lambda (k v) v)))) + (check (j1 'name) => (rich-json "Alice")) + (check (j1 'age) => (rich-json 25))) + + +; Test rich-json%transform with multiple nested levels +(let* ((j0 (rich-json '((user . ((profile . ((contact . ((email . "alice@example.com") + (phone . "123-456-7890")))))))))) + (j1 (j0 :transform 'user 'profile 'contact 'email (lambda (k v) (string-append v ".verified"))))) + (check (j1 'user 'profile 'contact 'email) => (rich-json "alice@example.com.verified"))) + +; Test rich-json%transform for conditional transformation with predicate function +(let* ((j0 (rich-json '((user . ((data . ((scores . #(85 90 78 92 88)) + (settings . ((notifications . #t) + (theme . "dark")))))))))) + (j1 (j0 :transform 'user 'data (lambda (k) (equal? k 'scores)) (lambda (k v) + (vector-map (lambda (score) (+ score 5)) v))))) + (check (j1 'user 'data 'scores) => (rich-json #(90 95 83 97 93))) + (check (j1 'user 'data 'settings 'theme) => (rich-json "dark"))) + + +; Compare transform and set +(let* ((j0 (rich-json '((user . ((profile . ((name . "Alice") + (age . 25) + (scores . #(85 90 78))))))))) + (j1 (j0 :transform 'user 'profile 'scores (lambda (k v) + (vector-map (lambda (score) (+ score 5)) v)))) + (j2 (j0 :set 'user 'profile 'scores #(90 95 83)))) + (check (j1 'user 'profile 'scores) => (rich-json #(90 95 83))) + (check (j2 'user 'profile 'scores) => (rich-json #(90 95 83))) + (check (j1 'user 'profile 'name) => (rich-json "Alice")) + (check (j2 'user 'profile 'name) => (rich-json "Alice"))) + + + +#| +rich-json%push +向JSON对象中添加新的键值对。 + +语法 +---- +(rich-json-instance :push key1 key2 ... value) + +参数 +---- +key1, key2, ... : symbol | string | number | boolean +用于定位嵌套位置的键路径。 + +value : any +要添加的值。 + +返回值 +----- +返回新的JSON对象,包含新增的键值对。 + +功能 +---- +- 支持多层嵌套添加 +- 如果键已存在,会覆盖原有值 +- 如果键不存在,会创建新的键值对 +- 支持符号、字符串、数字和布尔值作为键 +|# +; rich-json%push +; 多层,键为符号 +(let* ((j0 (rich-json '((person . ((name . "Alice") (age . 25)))))) + (j1 (j0 :push 'person 'city "Wonderland"))) + (check (j1 'person 'city) => (rich-json "Wonderland"))) + +; 多层,键为字符串 +(let* ((j0 (rich-json '(("person" . (("name" . "Alice") ("age" . 25)))))) + (j1 (j0 :push "person" "city" "Wonderland"))) + (check (j1 "person" "city") => (rich-json "Wonderland"))) + +; 多层,键为符号 +(let* ((j0 (rich-json '((person . ((name . "Alice") + (age . 25) + (address . ((city . "Oldland") + (zip . "12345")))))))) + (j1 (j0 :push 'person 'address 'street "Main St"))) + (check (j1 'person 'address 'street) => (rich-json "Main St"))) + +; 多层,JSON是向量 +(let* ((j0 (rich-json '((data . #(1 2 3))))) + (j1 (j0 :push 'data 3 4))) + (check (j1 :get) => '((data . #(1 2 3 4))))) + +; 多层,JSON是二维向量 +(let* ((j0 (rich-json '((data . #(#(1 2) #(3 4)))))) + (j1 (j0 :push 'data 1 2 5))) + (check (j1 :get) => '((data . #(#(1 2) #(3 4 5)))))) + +; 多层,JSON的Key是数字 +; (let* ((j0 (rich-json '((data . ((0 . "zero") (1 . "one")))))) +; (j1 (j0 :push 'data 2 "two"))) +; (check (j1 'data 2) => (rich-json "two"))) +; 因为 (j1 'data) 实际上是 rich-json 的主体,所以这里是错误的,暂时不修复 + +(let* ((j0 (rich-json '((flags . ((#t . "true") (#f . "false")))))) + (j1 (j0 :push 'flags #t "yes"))) + (check (j1 'flags #t) => (rich-json "yes"))) + +#| +json-drop +从JSON数据结构中删除指定的键。 + +语法 +---- +(json-drop json key) +(json-drop json predicate-fn) + +参数 +---- +json : list | vector +JSON数据结构。 + +key : symbol | string | number | boolean +要删除的键名。 + +predicate-fn : function +用于选择要删除的键的谓词函数。 + +返回值 +----- +返回新的JSON数据结构,不包含被删除的键。 + +功能 +---- +- 从JSON对象中删除指定键 +- 支持谓词函数选择要删除的键 +- 如果键不存在,返回原始数据结构 +|# +; json-drop +(let* ((json '((name . "Alice") (age . 25)))) + (let ((updated-json (json-drop json 'age))) + (check (json-ref updated-json 'age) => '()))) + +(let* ((json '((name . "Alice") + (age . 25) + (address . ((city . "Wonderland") + (zip . "12345")))))) + (let ((updated-json (json-drop* json 'address 'city))) + (check (json-ref* updated-json 'address 'city) => '()))) + +(let* ((json '((name . "Alice") + (age . 25) + (address . ((city . "Wonderland") + (zip . "12345")))))) + (let1 j1 (json-drop json (lambda (k) (equal? k 'city))) + (check (json-ref* j1 'address 'city) => "Wonderland")) + (let1 j2 (json-drop json (lambda (k) (equal? k 'name))) + (check (json-ref* j2 'name) => '())) + (let1 j3 (json-drop* json 'address (lambda (k) (equal? k 'city))) + (check (json-ref* j3 'address 'city) => '()))) + +#| +rich-json%drop +从 JSON 数据结构中删除指定的元素。 + +语法 +---- +1. 路径删除模式(Deep Delete): + (rich-json-instance :drop key1 key2 ... target-key) + +2. 谓词删除模式(Shallow Filter): + (rich-json-instance :drop predicate-fn) + +参数 +---- +key1, key2, ... : symbol | string | number | boolean + 用于定位要删除元素的层级路径。 + - 最后一个参数是要删除的目标键(对象)或索引(数组)。 + - 前面的参数是导航路径。 + +predicate-fn : function (lambda (key) ...) + 用于筛选要删除项的谓词函数。 + - 接收参数: + * 对于对象 (Object):接收 **键名 (Key)** (通常是 Symbol)。 + * 对于数组 (Array):接收 **索引 (Index)** (整数)。 + - 返回值:如果不希望保留该项(即希望删除),返回 #t;否则返回 #f。 + - 注意:此函数**不接收**元素的值 (Value)。 + +返回值 +----- +返回一个新的 rich-json 对象,其中指定的元素已被移除。 + +功能 +---- +1. **路径删除**: + - 支持多层嵌套定位。 + - 就像文件系统路径一样,精准打击并删除路径末端的一个元素。 + - 如果路径不存在,操作无效,返回原对象。 + +2. **谓词删除**: + - 仅作用于**当前层级**(浅层)。 + - 批量删除所有满足条件的项。 + - **陷阱提示**:对于数组,它判断的是下标(0, 1, 2...)而不是数组里的内容。 + +示例 +---- +;; 路径删除:删除 person 下 address 里的 zip 字段 +(j :drop 'person 'address 'zip) + +;; 谓词删除(对象):删除所有键名为 string 类型或特定名称的键 +(j :drop (lambda (k) (eq? k 'age))) + +;; 谓词删除(数组):删除所有索引为偶数的元素(即删除第 0, 2, 4... 项) +(j :drop even?) +|# +(let* ((j0 (rich-json '((name . "Alice") (age . 25) (city . "Wonderland")))) + (j1 (j0 :drop 'age))) + (check (j1 'age) => (rich-json :null)) + (check (j1 'name) => (rich-json "Alice")) + (check (j1 'city) => (rich-json "Wonderland"))) + +(let* ((j0 (rich-json '((user . ((profile . ((name . "Alice") + (age . 25) + (scores . #(85 90 78))))))))) + (j1 (j0 :drop 'user 'profile 'scores))) + (check (j1 'user 'profile 'scores) => (rich-json :null)) + (check (j1 'user 'profile 'name) => (rich-json "Alice")) + (check (j1 'user 'profile 'age) => (rich-json 25))) + +(let* ((j0 (rich-json '((data . #(1 2 3 4 5))))) + (j1 (j0 :drop 'data (lambda (k) (and (number? k) (even? k)))))) + (check (j1 'data) => (rich-json #(2 4)))) + +(let* ((j0 (rich-json '((settings . (("theme" . "dark") + (notifications . #t) + ("language" . "en")))))) + (j1 (j0 :drop 'settings (lambda (k) (string? k))))) + (check (j1 'settings "theme") => (rich-json :null)) + (check (j1 'settings "language") => (rich-json :null))) + +(let* ((j0 (rich-json '((a . 1) (b . 2) (c . 3)))) + (j1 (j0 :drop (lambda (k) (member k '(a c)))))) + (check (j1 'a) => (rich-json :null)) + (check (j1 'b) => (rich-json 2)) + (check (j1 'c) => (rich-json :null))) + + +(let* ((j0 (rich-json #())) + (j1 (j0 :drop 0))) + (check (j1 :get) => #())) + +#| +json-reduce +转换JSON数据结构中指定键的值。 + +语法 +---- +(json-reduce json key transform-fn) +(json-reduce json predicate-fn transform-fn) + +参数 +---- +json : list | vector +JSON数据结构。 + +key : symbol | string | number | boolean +要转换的键名。 + +predicate-fn : function +用于选择要转换的键的谓词函数。 + +transform-fn : function +转换函数,接收键和值,返回新值。 + +返回值 +----- +返回新的JSON数据结构,包含转换后的值。 + +功能 +---- +- 转换JSON对象中指定键的值 +- 支持谓词函数选择要转换的键 +- 转换函数接收键和值作为参数 +- 支持多层嵌套转换 +|# +; json-reduce +(let ((json '((name . "Alice") (age . 25)))) + (check (json-reduce json 'name (lambda (k v) (string-upcase v))) + => '((name . "ALICE") (age . 25)))) + +(let ((json '((person . ((name . "Alice") (age . 25)))))) + (check (json-reduce json 'person (lambda (k v) v)) + => '((person . ((name . "Alice") (age . 25)))))) + +(let ((json '((name . "Alice") (age . 25)))) + (check (json-reduce json (lambda (k) (equal? k 'age)) (lambda (k v) (+ v 1))) + => '((name . "Alice") (age . 26)))) + +(let ((json '((person . ((name . "Alice") (age . 25)))))) + (check (json-reduce json (lambda (k) (equal? k 'person)) (lambda (k v) v)) + => '((person . ((name . "Alice") (age . 25)))))) + +(let ((json '((name . "Alice") (age . 25)))) + (check (json-reduce json #t (lambda (k v) (if (string? v) (string-upcase v) v))) + => '((name . "ALICE") (age . 25)))) + +(let ((json '((name . "Alice") (age . 25)))) + (check (json-reduce json #f (lambda (k v) v)) + => '((name . "Alice") (age . 25)))) + +(let ((json '())) + (check (json-reduce json 'name (lambda (k v) v)) + => '())) + +(let ((json #())) + (check (json-reduce json 'name (lambda (k v) v)) + => #())) + +(let1 json '((person . ((name . "Alice") + (age . 25) + (address . ((city . "Wonderland") + (zip . "12345")))))) + (let1 updated-json (json-reduce* json 'person 'address 'city (lambda (x y) (string-upcase y))) + (check (json-ref* updated-json 'person 'address 'city) => "WONDERLAND"))) + + +; rich-json%push with rich-json object +(let* ((j0 (rich-json `((person . ((name . "Alice") (age . 25)))))) + (j1 (rich-json "Wonderland")) + (j2 (j0 :push 'person 'city j1))) + (check (j2 'person 'city) => (rich-json "Wonderland"))) + +(let* ((j0 (rich-json `((person . ((name . "Alice") (age . 25)))))) + (j1 (rich-json `((city . "Wonderland") (zip . "12345")))) + (j2 (j0 :push 'person 'address j1))) + (check (j2 'person 'address 'city) => (rich-json "Wonderland")) + (check (j2 'person 'address 'zip) => (rich-json "12345"))) + + + +; Test with nested rich-json objects +(let* ((j0 (rich-json `((person . ((name . "Alice") (age . 25)))))) + (j1 (rich-json `((address . ((city . "Wonderland") (zip . "12345")))))) + (j2 (j0 :set 'person j1))) + (check (j2 'person 'address 'city) => (rich-json "Wonderland")) + (check (j2 'person 'address 'zip) => (rich-json "12345"))) + +; Test with mixed rich-json objects and primitive values +(let* ((j0 (rich-json `((person . ((name . "Alice") (age . 25)))))) + (j1 (rich-json "Wonderland")) + (j2 ((j0 :push 'person 'city j1) :set 'person 'age 26))) + (check (j2 'person 'city) => (rich-json "Wonderland")) + (check (j2 'person 'age) => (rich-json 26))) + +; Test with null rich-json object +(let* ((j0 (rich-json `((person . ((name . "Alice") (age . 25)))))) + (j1 (rich-json :null)) + (j2 (j0 :set 'person 'age j1))) + (check (j2 'person 'age) => (rich-json :null))) + +; Test with boolean rich-json object +(let* ((j0 (rich-json `((person . ((name . "Alice") (age . 25)))))) + (j1 (rich-json :true)) + (j2 (j0 :push 'person 'active j1))) + (check (j2 'person 'active) => (rich-json :true))) + +; Test with array rich-json object +(let* ((j0 (rich-json `((person . ((name . "Alice") (age . 25)))))) + (j1 (rich-json #(1 2 3))) + (j2 (j0 :push 'person 'scores j1))) + (check (j2 'person 'scores) => (rich-json #(1 2 3)))) + +(check + (json->string + `(("messages" . #((("role" . "user") ("content" . #(1 2 3))) + (("role" . "user") ("content" . "中文")))))) + => "{\"messages\":[{\"role\":\"user\",\"content\":[1,2,3]},{\"role\":\"user\",\"content\":\"中文\"}]}") + +(check + (json->string + `(("messages" . #( + (("role" . "user") ("content" . #( + (("text" . "1") ("type" . "text")) + (("text" . "2") ("type" . "text")) + ))) + (("role" . "user") ("content" . "中文")) + )))) + => "{\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"1\",\"type\":\"text\"},{\"text\":\"2\",\"type\":\"text\"}]},{\"role\":\"user\",\"content\":\"中文\"}]}" +) + +(check-report) \ No newline at end of file diff --git a/tests/goldfish/liii/rich-set-test.scm b/tests/goldfish/liii/rich-set-test.scm new file mode 100644 index 0000000000000000000000000000000000000000..b063809e646770b35e770a083b62b844faacb502 --- /dev/null +++ b/tests/goldfish/liii/rich-set-test.scm @@ -0,0 +1,104 @@ +; +; Copyright (C) 2025 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(import (liii base) (liii rich-set) (liii check)) + +(check-set-mode! 'report-failed) + +;; Test factory methods +(check ((rich-hash-set :empty) :size) => 0) +(check ((rich-hash-set :empty) :empty?) => #t) + +;; Test basic operations +(let1 ht (make-hash-table) + (hash-table-set! ht 'a #t) + (hash-table-set! ht 'b #t) + (hash-table-set! ht 'c #t) + (check ((rich-hash-set ht) :size) => 3)) + +(let1 ht (make-hash-table) + (check ((rich-hash-set ht) :empty?) => #t) + (hash-table-set! ht 'a #t) + (check ((rich-hash-set ht) :empty?) => #f)) + +(let1 ht (make-hash-table) + (hash-table-set! ht 'a #t) + (hash-table-set! ht 'b #t) + (check ((rich-hash-set ht) :contains 'a) => #t) + (check ((rich-hash-set ht) :contains 'c) => #f)) + +;; Test non-destructive operations +(let1 ht (make-hash-table) + (hash-table-set! ht 'a #t) + (hash-table-set! ht 'b #t) + (let1 s (rich-hash-set ht) + (check (s :add-one 'c) => (let1 new-ht (make-hash-table) + (hash-table-set! new-ht 'a #t) + (hash-table-set! new-ht 'b #t) + (hash-table-set! new-ht 'c #t) + (rich-hash-set new-ht))) + (check (s :add-one 'd) => (let1 new-ht (make-hash-table) + (hash-table-set! new-ht 'a #t) + (hash-table-set! new-ht 'b #t) + (hash-table-set! new-ht 'd #t) + (rich-hash-set new-ht))))) + +(let1 ht (make-hash-table) + (hash-table-set! ht 'a #t) + (hash-table-set! ht 'b #t) + (let1 s (rich-hash-set ht) + (check (s :remove 'a) => (let1 new-ht (make-hash-table) + (hash-table-set! new-ht 'b #t) + (rich-hash-set new-ht))) + (check (s :remove 'b) => (let1 new-ht (make-hash-table) + (hash-table-set! new-ht 'a #t) + (rich-hash-set new-ht))))) + +;; Test destructive operations +(let1 ht (make-hash-table) + (hash-table-set! ht 'a #t) + (hash-table-set! ht 'b #t) + (let1 s (rich-hash-set ht) + (check (s :add-one! 'c) => (let1 new-ht (make-hash-table) + (hash-table-set! new-ht 'a #t) + (hash-table-set! new-ht 'b #t) + (hash-table-set! new-ht 'c #t) + (rich-hash-set new-ht))) + (check (s :add-one! 'd) => (let1 new-ht (make-hash-table) + (hash-table-set! new-ht 'a #t) + (hash-table-set! new-ht 'b #t) + (hash-table-set! new-ht 'c #t) + (hash-table-set! new-ht 'd #t) + (rich-hash-set new-ht))))) + +(let1 ht (make-hash-table) + (hash-table-set! ht 'a #t) + (hash-table-set! ht 'b #t) + (let1 s (rich-hash-set ht) + (check (s :remove! 'a) => (let1 new-ht (make-hash-table) + (hash-table-set! new-ht 'b #t) + (rich-hash-set new-ht))) + (check (s :remove! 'b) => (let1 new-ht (make-hash-table) + (rich-hash-set new-ht))))) + +(let1 ht (make-hash-table) + (hash-table-set! ht 'a #t) + (hash-table-set! ht 'b #t) + (let1 s (rich-hash-set ht) + (check (s :clear!) => (rich-hash-set (make-hash-table))))) + +(check-report) + diff --git a/tests/goldfish/liii/set-test.scm b/tests/goldfish/liii/set-test.scm index acfa5f119cbd0792f450438789a1a69a531ad4a9..b9ffe382716eb184fd2dfc3f43287ac8247be6a6 100644 --- a/tests/goldfish/liii/set-test.scm +++ b/tests/goldfish/liii/set-test.scm @@ -1,5 +1,5 @@ ; -; Copyright (C) 2025 The Goldfish Scheme Authors +; Copyright (C) 2026 The Goldfish Scheme Authors ; ; Licensed under the Apache License, Version 2.0 (the "License"); ; you may not use this file except in compliance with the License. @@ -14,91 +14,1913 @@ ; under the License. ; -(import (liii base) (liii set) (liii check)) +(import (scheme base) + (scheme char) + (liii check) + (liii set) + (srfi srfi-128) + (liii error)) (check-set-mode! 'report-failed) -;; Test factory methods -(check ((hash-set :empty) :size) => 0) -(check ((hash-set :empty) :empty?) => #t) - -;; Test basic operations -(let1 ht (make-hash-table) - (hash-table-set! ht 'a #t) - (hash-table-set! ht 'b #t) - (hash-table-set! ht 'c #t) - (check ((hash-set ht) :size) => 3)) - -(let1 ht (make-hash-table) - (check ((hash-set ht) :empty?) => #t) - (hash-table-set! ht 'a #t) - (check ((hash-set ht) :empty?) => #f)) - -(let1 ht (make-hash-table) - (hash-table-set! ht 'a #t) - (hash-table-set! ht 'b #t) - (check ((hash-set ht) :contains 'a) => #t) - (check ((hash-set ht) :contains 'c) => #f)) - -;; Test non-destructive operations -(let1 ht (make-hash-table) - (hash-table-set! ht 'a #t) - (hash-table-set! ht 'b #t) - (let1 s (hash-set ht) - (check (s :add-one 'c) => (let1 new-ht (make-hash-table) - (hash-table-set! new-ht 'a #t) - (hash-table-set! new-ht 'b #t) - (hash-table-set! new-ht 'c #t) - (hash-set new-ht))) - (check (s :add-one 'd) => (let1 new-ht (make-hash-table) - (hash-table-set! new-ht 'a #t) - (hash-table-set! new-ht 'b #t) - (hash-table-set! new-ht 'd #t) - (hash-set new-ht))))) - -(let1 ht (make-hash-table) - (hash-table-set! ht 'a #t) - (hash-table-set! ht 'b #t) - (let1 s (hash-set ht) - (check (s :remove 'a) => (let1 new-ht (make-hash-table) - (hash-table-set! new-ht 'b #t) - (hash-set new-ht))) - (check (s :remove 'b) => (let1 new-ht (make-hash-table) - (hash-table-set! new-ht 'a #t) - (hash-set new-ht))))) - -;; Test destructive operations -(let1 ht (make-hash-table) - (hash-table-set! ht 'a #t) - (hash-table-set! ht 'b #t) - (let1 s (hash-set ht) - (check (s :add-one! 'c) => (let1 new-ht (make-hash-table) - (hash-table-set! new-ht 'a #t) - (hash-table-set! new-ht 'b #t) - (hash-table-set! new-ht 'c #t) - (hash-set new-ht))) - (check (s :add-one! 'd) => (let1 new-ht (make-hash-table) - (hash-table-set! new-ht 'a #t) - (hash-table-set! new-ht 'b #t) - (hash-table-set! new-ht 'c #t) - (hash-table-set! new-ht 'd #t) - (hash-set new-ht))))) - -(let1 ht (make-hash-table) - (hash-table-set! ht 'a #t) - (hash-table-set! ht 'b #t) - (let1 s (hash-set ht) - (check (s :remove! 'a) => (let1 new-ht (make-hash-table) - (hash-table-set! new-ht 'b #t) - (hash-set new-ht))) - (check (s :remove! 'b) => (let1 new-ht (make-hash-table) - (hash-set new-ht))))) - -(let1 ht (make-hash-table) - (hash-table-set! ht 'a #t) - (hash-table-set! ht 'b #t) - (let1 s (hash-set ht) - (check (s :clear!) => (hash-set (make-hash-table))))) +;; --- Data Setup --- +(define s-empty (set)) +(define comp (set-element-comparator s-empty)) +(define s-1 (set 1)) +(define s-1-2 (set 1 2)) +(define s-1-2-3 (set 1 2 3)) +(define s-2-3-4 (set 2 3 4)) +(define s-4-5 (set 4 5)) -(check-report) +#| +set? +检查对象是否为 set。 + +语法 +---- +(set? obj) + +参数 +---- +obj : any +要检查的对象。 + +返回值 +----- +如果 obj 是 set,返回 #t;否则返回 #f。 +|# +(check-true (set? s-empty)) +(check-true (set? s-1)) +(check-false (set? "not a set")) +(check-false (set? '())) +(check-false (set? #(1 2 3))) + + +#| +set +创建一个新的 set。 + +语法 +---- +(set element ...) + +参数 +---- +element ... : any +初始元素。 + +返回值 +----- +返回包含指定元素的 set。 +|# +(check-true (set? (set 1 2 3))) +(check-true (set-contains? (set 1) 1)) + +#| +list->set +将列表转换为 set。 + +语法 +---- +(list->set list) + +参数 +---- + +list : list +要转换的列表。 + +返回值 +----- +返回包含列表中所有元素的新 set(使用默认比较器,重复元素会被去重)。 +|# +(define s-list-1 (list->set '(1 2 3))) +(check-true (set? s-list-1)) +(check-true (eq? (set-element-comparator s-list-1) comp)) +(check-true (set=? s-1-2-3 s-list-1)) +(check-false (eq? s-1-2-3 s-list-1)) + +(define s-list-empty (list->set '())) +(check-true (set=? s-empty s-list-empty)) +(check (set-size s-list-empty) => 0) + +;; Duplicates in list should be handled +(define s-list-dup (list->set '(1 2 2 1))) +(check-true (set=? s-1-2 s-list-dup)) +(check (set-size s-list-dup) => 2) + + +#| +list->set! +将列表元素并入 set(可变操作)。 + +语法 +---- +(list->set! set list) + +参数 +---- +set : set +目标 set。 + +list : list +要并入的元素列表。 + +返回值 +------ +返回修改后的 set(与传入的 set 是同一个对象)。 +|# + +;; 测试 list->set! 基本行为 +(define s-list-merge (set 1 2)) +(define s-list-merge-result (list->set! s-list-merge '(2 3 4))) +(check-true (eq? s-list-merge-result s-list-merge)) +(check (set-size s-list-merge) => 4) +(check-true (set-contains? s-list-merge 1)) +(check-true (set-contains? s-list-merge 2)) +(check-true (set-contains? s-list-merge 3)) +(check-true (set-contains? s-list-merge 4)) + +;; 测试空列表 +(define s-list-empty (set 1 2)) +(list->set! s-list-empty '()) +(check (set-size s-list-empty) => 2) + +;; 测试类型错误 +(check-catch 'type-error (list->set! "not a set" '(1 2))) + + + +#| +set-copy +复制一个 set。 + +语法 +---- +(set-copy set) + +参数 +---- +set : set +要复制的 set。 + +返回值 +----- +返回一个新的 set,包含原 set 的所有元素,且比较器相同。 + +异常 +---- +如果参数不是 set,抛出 error。 +|# +(let ((copy (set-copy s-1-2))) + (check-true (set=? s-1-2 copy)) + (check-false (eq? s-1-2 copy))) ; Ensure new instance +(check-true (set-empty? (set-copy s-empty))) +(check-catch 'type-error (set-copy "not a set")) + +#| +set-unfold +使用 unfold 模式创建 set。 + +语法 +---- +(set-unfold stop? mapper successor seed comparator) + +参数 +---- +stop? : procedure +停止谓词。接收当前种子,返回布尔值。 + +mapper : procedure +映射函数。接收当前种子,返回要添加到 set 的元素。 + +successor : procedure +后继函数。接收当前种子,返回下一个种子。 + +seed : any +初始种子值。 + +comparator : comparator +元素比较器。 + +返回值 +----- +返回生成的 set。 +|# +;; Create set {0, 1, 2, ..., 9} +(define s-10 (set-unfold + (lambda (x) (= x 10)) + (lambda (x) x) + (lambda (x) (+ x 1)) + 0 + comp)) +(check-true (set-contains? s-10 0)) +(check-true (set-contains? s-10 9)) +(check-false (set-contains? s-10 10)) + +#| +set-contains? +检查 set 是否包含指定元素。 + +语法 +---- +(set-contains? set element) + +参数 +---- +set : set +目标 set。 + +element : any +要检查的元素。 + +返回值 +----- +如果 set 包含 element,返回 #t;否则返回 #f。 + +异常 +---- +如果参数不是 set,抛出 error。 +|# +(check-true (set-contains? s-1 1)) +(check-false (set-contains? s-1 2)) +(check-false (set-contains? s-empty 1)) +(check-catch 'type-error (set-contains? "not a set" 1)) + +#| +set-empty? +检查 set 是否为空。 + +语法 +---- +(set-empty? set) + +参数 +---- +set : set +要检查的 set。 + +返回值 +----- +如果 set 为空,返回 #t;否则返回 #f。 + +异常 +---- +如果参数不是 set,抛出 error。 +|# +(check-true (set-empty? s-empty)) +(check-false (set-empty? s-1)) +(check-catch 'type-error (set-empty? "not a set")) + +#| +set-disjoint? +检查两个 set 是否不相交(没有共同元素)。 + +语法 +---- +(set-disjoint? set1 set2) + +参数 +---- +set1, set2 : set +要检查的 set。 + +返回值 +----- +如果两个 set 没有共同元素,返回 #t;否则返回 #f。 + +异常 +---- +如果任一参数不是 set,抛出 error。 +如果两个 set 的比较器不同,抛出 value-error。 +|# +(check-true (set-disjoint? s-1-2-3 s-4-5)) +(check-false (set-disjoint? s-1-2-3 s-2-3-4)) ; share 2, 3 +(check-true (set-disjoint? s-empty s-1)) +(check-true (set-disjoint? s-1 s-empty)) +(check-true (set-disjoint? s-empty s-empty)) +(check-catch 'type-error (set-disjoint? "not a set" s-1)) +(check-catch 'type-error (set-disjoint? s-1 "not a set")) +;; Note: Comparator mismatch test is at the end of the file, but we should verify it here too or move it. +(define str-comp (make-comparator string? string=? stringset-with-comparator str-comp '("apple" "banana"))) +(check-catch 'value-error (set-disjoint? s-1 s-str)) + +#| +set=? +检查两个或多个 set 是否相等。 + +语法 +---- +(set=? set1 set2 ...) + +参数 +---- +set1, set2, ... : set +要比较的 set。 + +返回值 +----- +如果所有 set 都包含相同的元素,返回 #t;否则返回 #f。 +注意:比较器必须相同。 + +异常 +---- +如果任一参数不是 set,抛出 error。 +如果 set 的比较器不同,抛出 value-error。 +|# +(check-true (set=? s-empty s-empty)) +(check-true (set=? s-1 s-1)) +(check-true (set=? s-1 (set 1))) +(check-false (set=? s-1 s-empty)) +(check-false (set=? s-1 s-1-2)) +;; Multiple arguments +(check-true (set=? s-1 (set 1) (list->set '(1)))) +(check-false (set=? s-1 s-1 s-empty)) +(check-catch 'type-error (set=? "not a set" s-1)) +(check-catch 'value-error (set=? s-1 s-str)) + +#| +set<=? +检查一个 set 是否为另一个 set 的子集。 + +语法 +---- +(set<=? set1 set2 ...) + +参数 +---- +set1, set2, ... : set +要检查的 set。 + +返回值 +----- +如果每个 set 都是其后一个 set 的子集,返回 #t;否则返回 #f。 + +异常 +---- +如果任一参数不是 set,抛出 error。 +如果 set 的比较器不同,抛出 value-error。 +|# +(check-true (set<=? s-empty s-1)) +(check-true (set<=? s-1 s-1-2)) +(check-true (set<=? s-1-2 s-1-2-3)) +(check-true (set<=? s-1-2 s-1-2)) +(check-false (set<=? s-1-2 s-1)) +;; Chain +(check-true (set<=? s-empty s-1 s-1-2 s-1-2-3)) +(check-false (set<=? s-empty s-1-2 s-1)) ; Broken chain +(check-catch 'type-error (set<=? "not a set" s-1)) +(check-catch 'value-error (set<=? s-1 s-str)) + +#| +set=? +检查一个 set 是否为另一个 set 的超集。 + +语法 +---- +(set>=? set1 set2 ...) + +参数 +---- +set1, set2, ... : set +要检查的 set。 + +返回值 +----- +如果每个 set 都是其后一个 set 的超集,返回 #t;否则返回 #f。 + +异常 +---- +如果任一参数不是 set,抛出 error。 +如果 set 的比较器不同,抛出 value-error。 +|# +(check-true (set>=? s-1 s-empty)) +(check-true (set>=? s-1-2 s-1)) +(check-true (set>=? s-1 s-1)) +(check-false (set>=? s-1 s-1-2)) +;; Chain +(check-true (set>=? s-1-2-3 s-1-2 s-1 s-empty)) +(check-catch 'type-error (set>=? "not a set" s-1)) +(check-catch 'value-error (set>=? s-1 s-str)) + +#| +set>? +检查一个 set 是否为另一个 set 的真超集。 + +语法 +---- +(set>? set1 set2 ...) + +参数 +---- +set1, set2, ... : set +要检查的 set。 + +返回值 +----- +如果每个 set 都是其后一个 set 的真超集,返回 #t;否则返回 #f。 + +异常 +---- +如果任一参数不是 set,抛出 error。 +如果 set 的比较器不同,抛出 value-error。 +|# +(check-true (set>? s-1 s-empty)) +(check-true (set>? s-1-2 s-1)) +(check-false (set>? s-1 s-1)) +(check-false (set>? s-1 s-1-2)) +;; Chain +(check-true (set>? s-1-2 s-1 s-empty)) +(check-catch 'type-error (set>? "not a set" s-1)) +(check-catch 'value-error (set>? s-1 s-str)) + +;; --- Different Data Types --- +(define s-sym (set 'a 'b 'c)) +(check-true (set-contains? s-sym 'a)) +(check-false (set-contains? s-sym 'd)) +(check-true (set=? s-sym (list->set '(c b a)))) + +;(define str-comp (make-comparator string? string=? stringset '("apple" "banana"))) +(check-true (set-contains? s-str "apple")) +(check-false (set-contains? s-str "pear")) + +;; --- Large Set Test --- +(define (range n) + (let loop ((i 0) (acc '())) + (if (= i n) (reverse acc) + (loop (+ i 1) (cons i acc))))) + +(define big-n 1000000) +(define big-list (range big-n)) +(define s-big (list->set big-list)) +;; Check basic existence +(check-true (set-contains? s-big 0)) +(check-true (set-contains? s-big (- big-n 1))) +(check-false (set-contains? s-big big-n)) +;; Check subset logic on large sets +(define s-big-minus-one (set-copy s-big)) +;; (We can't easily remove elements with current API, so let's build a smaller one) +(define s-small-big (list->set (range (- big-n 1)))) +(check-true (setset big-list))) + +#| +set-size +获取 set 中元素的数量。 + +语法 +---- +(set-size set) + +参数 +---- +set : set +要获取大小的 set。 + +返回值 +----- +返回 set 中元素的数量(整数)。 + +异常 +---- +如果参数不是 set,抛出 error。 +|# +(check (set-size s-empty) => 0) +(check (set-size s-1) => 1) +(check (set-size s-1-2) => 2) +(check (set-size s-1-2-3) => 3) +(check (set-size s-2-3-4) => 3) +(check (set-size s-4-5) => 2) +(check (set-size s-big) => big-n) +(check (set-size s-small-big) => (- big-n 1)) +(check-catch 'type-error (set-size "not a set")) + +#| +set-any? +检查 set 中是否有元素满足谓词。 + +语法 +---- +(set-any? predicate set) + +参数 +---- +predicate : procedure +一个接受一个参数并返回布尔值的函数。 + +set : set +要检查的 set。 + +返回值 +------ +如果 set 中至少有一个元素满足 predicate,返回 #t;否则返回 #f。 + +注意 +---- +与 SRFI 1 的 any 函数不同,此函数不返回满足谓词的元素,只返回布尔值。 + +异常 +---- +如果 set 参数不是 set,抛出 error。 +|# + +;; 测试 set-any? 函数 +(check-false (set-any? (lambda (x) (> x 10)) s-empty)) +(check-false (set-any? (lambda (x) (> x 10)) s-1)) +(check-false (set-any? (lambda (x) (> x 10)) s-1-2)) +(check-false (set-any? (lambda (x) (> x 10)) s-1-2-3)) + +(check-true (set-any? (lambda (x) (= x 1)) s-1)) +(check-true (set-any? (lambda (x) (= x 1)) s-1-2)) +(check-true (set-any? (lambda (x) (= x 1)) s-1-2-3)) +(check-true (set-any? (lambda (x) (= x 2)) s-1-2)) +(check-true (set-any? (lambda (x) (= x 3)) s-1-2-3)) + +;; 测试多个元素满足谓词的情况 +(check-true (set-any? (lambda (x) (> x 0)) s-1-2-3)) +(check-true (set-any? (lambda (x) (< x 10)) s-1-2-3)) + +;; 测试边界情况 +(check-true (set-any? (lambda (x) (even? x)) s-1-2)) +(check-false (set-any? (lambda (x) (even? x)) s-1)) +(check-true (set-any? (lambda (x) (odd? x)) s-1)) +(check-true (set-any? (lambda (x) (odd? x)) s-1-2)) + +;; 测试类型错误 +(check-catch 'type-error (set-any? (lambda (x) #t) "not a set")) + +#| +set-every? +检查 set 中是否所有元素都满足谓词。 + +语法 +---- +(set-every? predicate set) + +参数 +---- +predicate : procedure +一个接受一个参数并返回布尔值的函数。 + +set : set +要检查的 set。 + +返回值 +------ +如果 set 中所有元素都满足 predicate,返回 #t;否则返回 #f。 + +注意 +---- +与 SRFI 1 的 every 函数不同,此函数不返回满足谓词的元素,只返回布尔值。 +空 set 返回 #t。 + +异常 +---- +如果 set 参数不是 set,抛出 error。 +|# + +;; 测试 set-every? 函数 +(check-true (set-every? (lambda (x) (> x 0)) s-empty)) +(check-true (set-every? (lambda (x) (> x 0)) s-1)) +(check-true (set-every? (lambda (x) (> x 0)) s-1-2)) +(check-true (set-every? (lambda (x) (> x 0)) s-1-2-3)) + +(check-false (set-every? (lambda (x) (> x 1)) s-1)) +(check-false (set-every? (lambda (x) (> x 1)) s-1-2)) +(check-false (set-every? (lambda (x) (> x 1)) s-1-2-3)) + +(check-true (set-every? (lambda (x) (number? x)) s-1-2-3)) + +;; 测试边界情况 +(check-true (set-every? (lambda (x) (odd? x)) s-1)) +(check-false (set-every? (lambda (x) (odd? x)) s-1-2)) ; 2 is even +(check-false (set-every? (lambda (x) (even? x)) s-1-2)) ; 1 is odd + +;; 测试类型错误 +(check-catch 'type-error (set-every? (lambda (x) #t) "not a set")) + +#| +set-find +查找 set 中满足谓词的元素。 + +语法 +---- +(set-find predicate set failure) + +参数 +---- +predicate : procedure +一个接受一个参数并返回布尔值的函数。 + +set : set +要检查的 set。 + +failure : procedure +一个无参函数,当没有找到满足谓词的元素时调用。 + +返回值 +------ +如果找到满足 predicate 的元素,返回该元素;否则返回 failure 的调用结果。 + +注意 +---- +如果有多个元素满足谓词,返回其中任意一个。 + +异常 +---- +如果 set 参数不是 set,抛出 error。 +|# + +;; 测试 set-find 函数 +(check (set-find (lambda (x) (= x 1)) s-1 (lambda () 'not-found)) => 1) +(check (set-find (lambda (x) (= x 1)) s-1-2 (lambda () 'not-found)) => 1) +(check (set-find (lambda (x) (= x 2)) s-1-2 (lambda () 'not-found)) => 2) + +(check (set-find (lambda (x) (> x 10)) s-1 (lambda () 'not-found)) => 'not-found) +(check (set-find (lambda (x) (> x 10)) s-empty (lambda () 'not-found)) => 'not-found) + +;; 测试多个元素满足谓词的情况(返回任意一个) +(let ((res (set-find (lambda (x) (> x 0)) s-1-2 (lambda () 'not-found)))) + (check-true (or (= res 1) (= res 2)))) + +;; 测试类型错误 +(check-catch 'type-error (set-find (lambda (x) #t) "not a set" (lambda () #f))) + +#| +set-count +计算 set 中满足谓词的元素个数。 + +语法 +---- +(set-count predicate set) + +参数 +---- +predicate : procedure +一个接受一个参数并返回布尔值的函数。 + +set : set +要检查的 set。 + +返回值 +------ +返回满足 predicate 的元素个数(精确整数)。 + +异常 +---- +如果 set 参数不是 set,抛出 error。 +|# + +;; 测试 set-count 函数 +(check (set-count (lambda (x) (> x 0)) s-empty) => 0) +(check (set-count (lambda (x) (> x 0)) s-1) => 1) +(check (set-count (lambda (x) (> x 0)) s-1-2) => 2) +(check (set-count (lambda (x) (> x 0)) s-1-2-3) => 3) + +(check (set-count (lambda (x) (> x 1)) s-1) => 0) +(check (set-count (lambda (x) (> x 1)) s-1-2) => 1) +(check (set-count (lambda (x) (> x 1)) s-1-2-3) => 2) + +(check (set-count (lambda (x) (even? x)) s-1-2-3) => 1) ; only 2 +(check (set-count (lambda (x) (odd? x)) s-1-2-3) => 2) ; 1 and 3 + +;; 测试类型错误 +(check-catch 'type-error (set-count (lambda (x) #t) "not a set")) + +#| +set-member +查找 set 中与指定元素相等的元素。 + +语法 +---- +(set-member set element default) + +参数 +---- +set : set +要检查的 set。 + +element : any +要查找的元素。 + +default : any +如果 element 不在 set 中,返回的值。 + +返回值 +------ +如果 element 在 set 中,返回 set 中存储的那个元素(可能与 element 并不是同一个对象,但比较结果相等)。 +如果 element 不在 set 中,返回 default。 + +异常 +---- +如果 set 参数不是 set,抛出 error。 +|# + +;; 测试 set-member 函数 +(check (set-member s-1 1 'not-found) => 1) +(check (set-member s-1 2 'not-found) => 'not-found) +(check (set-member s-empty 1 'not-found) => 'not-found) + +;; 测试通过比较器相等但对象不同的情况 +;; 构造一个大小写不敏感的字符串集合 +(define (my-string-ci-hash s) + (string-hash (string-map char-downcase s))) +(define string-ci-comparator (make-comparator string? string-ci=? string-ciset-with-comparator string-ci-comparator '("Apple" "Banana"))) + +(check (set-contains? s-str-ci "apple") => #t) +;; set-member 应该返回集合中存储的 "Apple",而不是查询用的 "apple" +(check (set-member s-str-ci "apple" 'not-found) => "Apple") +(check (set-member s-str-ci "banana" 'not-found) => "Banana") +(check (set-member s-str-ci "pear" 'not-found) => 'not-found) + +;; 测试类型错误 +(check-catch 'type-error (set-member "not a set" 1 'default)) + +#| +set-search! +在 set 中搜索指定元素,并通过 continuation 决定更新方式(可变操作)。 + +语法 +---- +(set-search! set element failure success) + +参数 +---- +set : set +目标 set。 + +element : any +要搜索的元素。 + +failure : procedure +当元素不存在时调用,接收两个 continuation:insert 与 ignore。 + +success : procedure +当元素存在时调用,接收 matching-element、update 与 remove。 + +返回值 +------ +返回两个值:可能更新后的 set 和 obj。 + +注意 +---- +continuation 的效果: +(insert obj) 插入 element。 +(ignore obj) 不修改 set。 +(update new-element obj) 用 new-element 替换匹配元素。 +(remove obj) 移除匹配元素。 +|# + +;; 测试 set-search! 插入 +(define s-search-1 (set 1 2)) +(call-with-values + (lambda () + (set-search! s-search-1 3 + (lambda (insert ignore) + (insert 'payload)) + (lambda (found update remove) + (error "unexpected success")))) + (lambda (result obj) + (check-true (eq? result s-search-1)) + (check (set-size s-search-1) => 3) + (check-true (set-contains? s-search-1 3)) + (check-false (set-contains? s-search-1 'payload)) + (check obj => 'payload))) + +;; 测试 set-search! 忽略 +(define s-search-2 (set 1 2)) +(call-with-values + (lambda () + (set-search! s-search-2 3 + (lambda (insert ignore) + (ignore 'ignored)) + (lambda (found update remove) + (error "unexpected success")))) + (lambda (result obj) + (check-true (eq? result s-search-2)) + (check (set-size s-search-2) => 2) + (check-false (set-contains? s-search-2 3)) + (check obj => 'ignored))) + +;; 测试 set-search! 更新(equals 但 not eq?) +(define s-search-ci (list->set-with-comparator string-ci-comparator '("Apple" "Banana"))) +(call-with-values + (lambda () + (set-search! s-search-ci "apple" + (lambda (insert ignore) + (error "unexpected failure")) + (lambda (found update remove) + (check found => "Apple") + (update "apple" 'updated)))) + (lambda (result obj) + (check-true (eq? result s-search-ci)) + (check (set-size s-search-ci) => 2) + (check (set-member s-search-ci "apple" 'not-found) => "apple") + (check obj => 'updated))) + +;; 测试 set-search! 删除 +(define s-search-3 (set 1 2 3)) +(call-with-values + (lambda () + (set-search! s-search-3 2 + (lambda (insert ignore) + (error "unexpected failure")) + (lambda (found update remove) + (remove 'removed)))) + (lambda (result obj) + (check-true (eq? result s-search-3)) + (check (set-size s-search-3) => 2) + (check-false (set-contains? s-search-3 2)) + (check obj => 'removed))) + +;; 测试类型错误 +(check-catch 'type-error + (set-search! "not a set" 1 + (lambda (insert ignore) (ignore 'x)) + (lambda (found update remove) (remove 'x)))) + +#| +set-map +对 set 中的每个元素应用 proc,并返回一个新 set。 + +语法 +---- +(set-map comparator proc set) + +参数 +---- +comparator : comparator +结果 set 的比较器。 + +proc : procedure +映射函数。 + +set : set +源 set。 + +返回值 +------ +返回新的 set,其中元素为 proc 的映射结果。 + +注意 +---- +如果映射后出现等价元素(基于 comparator),重复元素会被去重。 +|# + +;; 测试 set-map 基本映射 +(define s-map-1 (set 1 2 3)) +(define s-map-2 (set-map comp (lambda (x) (+ x 10)) s-map-1)) +(check-true (set? s-map-2)) +(check-true (eq? (set-element-comparator s-map-2) comp)) +(check (set-size s-map-2) => 3) +(check-true (set-contains? s-map-2 11)) +(check-true (set-contains? s-map-2 12)) +(check-true (set-contains? s-map-2 13)) +(check-true (set=? s-map-1 (set 1 2 3))) ; 原 set 不变 + +;; 测试 set-map 去重行为 +(define s-map-dup (set 1 2 3 4 5)) +(define s-map-dup-result (set-map comp (lambda (x) (quotient x 2)) s-map-dup)) +(check (set-size s-map-dup-result) => 3) +(check-true (set-contains? s-map-dup-result 0)) +(check-true (set-contains? s-map-dup-result 1)) +(check-true (set-contains? s-map-dup-result 2)) + +;; 测试 set-map 使用不同 comparator +(define s-map-sym (list->set-with-comparator (make-eq-comparator) '(foo bar baz))) +(define s-map-str (set-map string-ci-comparator symbol->string s-map-sym)) +(check (set-member s-map-str "FOO" 'not-found) => "foo") +(check (set-member s-map-str "BAR" 'not-found) => "bar") +(check (set-member s-map-str "BAZ" 'not-found) => "baz") + +;; 测试类型错误 +(check-catch 'type-error (set-map comp (lambda (x) x) "not a set")) + +#| +set-for-each +对 set 中的每个元素应用 proc,忽略返回值。 + +语法 +---- +(set-for-each proc set) + +参数 +---- +proc : procedure +要应用的函数。 + +set : set +目标 set。 + +返回值 +------ +返回值未指定。 +|# + +;; 测试 set-for-each 基本行为 +(define s-foreach (set 1 2 3)) +(define foreach-collected '()) +(set-for-each (lambda (x) (set! foreach-collected (cons x foreach-collected))) s-foreach) +(check-true (set=? (list->set foreach-collected) s-foreach)) + +;; 测试空集合不触发调用 +(define foreach-count 0) +(set-for-each (lambda (x) (set! foreach-count (+ foreach-count 1))) s-empty) +(check (set-size s-empty) => 0) +(check foreach-count => 0) + +;; 测试类型错误 +(check-catch 'type-error (set-for-each (lambda (x) x) "not a set")) + +#| +set-fold +对 set 中的每个元素应用 proc,累积结果并返回。 + +语法 +---- +(set-fold proc nil set) + +参数 +---- +proc : procedure +接收元素与累积值。 + +nil : any +初始累积值。 + +set : set +目标 set。 + +返回值 +------ +返回最后一次调用的结果,若 set 为空则返回 nil。 +|# + +;; 测试 set-fold 求和 +(check (set-fold (lambda (x acc) (+ x acc)) 0 s-1-2-3) => 6) +(check (set-fold (lambda (x acc) (+ x acc)) 0 s-empty) => 0) + +;; 测试 set-fold 累积为列表(顺序不保证) +(define fold-list (set-fold (lambda (x acc) (cons x acc)) '() s-1-2)) +(check-true (set=? (list->set fold-list) s-1-2)) + +;; 测试类型错误 +(check-catch 'type-error (set-fold (lambda (x acc) acc) '() "not a set")) +#| +set-filter +返回一个新的 set,仅包含满足 predicate 的元素。 + +语法 +---- +(set-filter predicate set) + +参数 +---- +predicate : procedure +筛选条件。 + +set : set +源 set。 + +返回值 +------ +返回新的 set,比较器与原 set 相同。 +|# + +;; 测试 set-filter 基本筛选 +(define s-filter-1 (set 1 2 3 4)) +(define s-filter-2 (set-filter even? s-filter-1)) +(check-true (set? s-filter-2)) +(check-true (eq? (set-element-comparator s-filter-2) (set-element-comparator s-filter-1))) +(check (set-size s-filter-2) => 2) +(check-true (set-contains? s-filter-2 2)) +(check-true (set-contains? s-filter-2 4)) +(check-true (set-contains? s-filter-1 1)) ; 原 set 不变 +(check-true (set-contains? s-filter-1 3)) + +;; 测试空集合 +(define s-filter-empty (set-filter even? s-empty)) +(check (set-size s-filter-empty) => 0) + +;; 测试类型错误 +(check-catch 'type-error (set-filter even? "not a set")) + +#| +set-filter! +可变筛选,返回仅包含满足 predicate 的元素的 set。 + +语法 +---- +(set-filter! predicate set) + +参数 +---- +predicate : procedure +筛选条件。 + +set : set +目标 set。 + +返回值 +------ +返回修改后的 set(与传入的 set 是同一个对象)。 +|# + +;; 测试 set-filter! 基本行为 +(define s-filter-mut (set 1 2 3 4)) +(define s-filter-mut-result (set-filter! odd? s-filter-mut)) +(check-true (eq? s-filter-mut-result s-filter-mut)) +(check (set-size s-filter-mut) => 2) +(check-true (set-contains? s-filter-mut 1)) +(check-true (set-contains? s-filter-mut 3)) +(check-false (set-contains? s-filter-mut 2)) +(check-false (set-contains? s-filter-mut 4)) + +;; 测试空集合 +(define s-filter-mut-empty (set-copy s-empty)) +(set-filter! even? s-filter-mut-empty) +(check (set-size s-filter-mut-empty) => 0) + +;; 测试类型错误 +(check-catch 'type-error (set-filter! even? "not a set")) + +#| +set-remove +返回一个新的 set,仅包含不满足 predicate 的元素。 + +语法 +---- +(set-remove predicate set) + +参数 +---- +predicate : procedure +筛选条件。 + +set : set +源 set。 + +返回值 +------ +返回新的 set,比较器与原 set 相同。 +|# + +;; 测试 set-remove 基本筛选 +(define s-remove-1 (set 1 2 3 4)) +(define s-remove-2 (set-remove even? s-remove-1)) +(check-true (set? s-remove-2)) +(check-true (eq? (set-element-comparator s-remove-2) (set-element-comparator s-remove-1))) +(check (set-size s-remove-2) => 2) +(check-true (set-contains? s-remove-2 1)) +(check-true (set-contains? s-remove-2 3)) +(check-false (set-contains? s-remove-2 2)) +(check-false (set-contains? s-remove-2 4)) +(check-true (set-contains? s-remove-1 2)) ; 原 set 不变 +(check-true (set-contains? s-remove-1 4)) + +;; 测试空集合 +(define s-remove-empty (set-remove even? s-empty)) +(check (set-size s-remove-empty) => 0) + +;; 测试类型错误 +(check-catch 'type-error (set-remove even? "not a set")) + +#| +set-remove! +可变筛选,返回仅包含不满足 predicate 的元素的 set。 + +语法 +---- +(set-remove! predicate set) + +参数 +---- +predicate : procedure +筛选条件。 + +set : set +目标 set。 + +返回值 +------ +返回修改后的 set(与传入的 set 是同一个对象)。 +|# + +;; 测试 set-remove! 基本行为 +(define s-remove-mut (set 1 2 3 4)) +(define s-remove-mut-result (set-remove! even? s-remove-mut)) +(check-true (eq? s-remove-mut-result s-remove-mut)) +(check (set-size s-remove-mut) => 2) +(check-true (set-contains? s-remove-mut 1)) +(check-true (set-contains? s-remove-mut 3)) +(check-false (set-contains? s-remove-mut 2)) +(check-false (set-contains? s-remove-mut 4)) + +;; 测试空集合 +(define s-remove-mut-empty (set-copy s-empty)) +(set-remove! even? s-remove-mut-empty) +(check (set-size s-remove-mut-empty) => 0) + +;; 测试类型错误 +(check-catch 'type-error (set-remove! even? "not a set")) + +#| +set-partition +将 set 划分为满足 predicate 与不满足 predicate 的两个新 set。 + +语法 +---- +(set-partition predicate set) + +参数 +---- +predicate : procedure +划分条件。 + +set : set +源 set。 + +返回值 +------ +返回两个值:满足 predicate 的新 set 与不满足 predicate 的新 set。 +|# + +;; 测试 set-partition 基本行为 +(define s-partition-1 (set 1 2 3 4)) +(call-with-values + (lambda () (set-partition even? s-partition-1)) + (lambda (yes no) + (check-true (set? yes)) + (check-true (set? no)) + (check-true (eq? (set-element-comparator yes) (set-element-comparator s-partition-1))) + (check-true (eq? (set-element-comparator no) (set-element-comparator s-partition-1))) + (check (set-size yes) => 2) + (check (set-size no) => 2) + (check-true (set-contains? yes 2)) + (check-true (set-contains? yes 4)) + (check-true (set-contains? no 1)) + (check-true (set-contains? no 3)) + (check-true (set-contains? s-partition-1 2)) ; 原 set 不变 + (check-true (set-contains? s-partition-1 4)))) + +;; 测试空集合 +(call-with-values + (lambda () (set-partition even? s-empty)) + (lambda (yes no) + (check (set-size yes) => 0) + (check (set-size no) => 0))) + +;; 测试类型错误 +(check-catch 'type-error (set-partition even? "not a set")) + +#| +set-partition! +可变划分,返回满足 predicate 的 set(原 set)与不满足 predicate 的新 set。 + +语法 +---- +(set-partition! predicate set) + +参数 +---- +predicate : procedure +划分条件。 + +set : set +目标 set。 + +返回值 +------ +返回两个值:修改后的 set 与不满足 predicate 的新 set。 +|# + +;; 测试 set-partition! 基本行为 +(define s-partition-mut (set 1 2 3 4)) +(call-with-values + (lambda () (set-partition! even? s-partition-mut)) + (lambda (yes no) + (check-true (eq? yes s-partition-mut)) + (check (set-size yes) => 2) + (check (set-size no) => 2) + (check-true (set-contains? yes 2)) + (check-true (set-contains? yes 4)) + (check-true (set-contains? no 1)) + (check-true (set-contains? no 3)))) + +;; 测试空集合 +(define s-partition-empty (set-copy s-empty)) +(call-with-values + (lambda () (set-partition! even? s-partition-empty)) + (lambda (yes no) + (check (set-size yes) => 0) + (check (set-size no) => 0))) + +;; 测试类型错误 +(check-catch 'type-error (set-partition! even? "not a set")) + +#| +set-union +返回多个 set 的并集。 + +语法 +---- +(set-union set1 set2 ...) + +参数 +---- +set1, set2 ... : set +参与并集的 set。 + +返回值 +------ +返回新的 set,元素来自它们首次出现的 set。 +|# + +;; 测试 set-union 基本行为 +(define s-union-1 (set-union s-1-2-3 s-2-3-4)) +(check (set-size s-union-1) => 4) +(check-true (set-contains? s-union-1 1)) +(check-true (set-contains? s-union-1 2)) +(check-true (set-contains? s-union-1 3)) +(check-true (set-contains? s-union-1 4)) +(check-true (eq? (set-element-comparator s-union-1) comp)) + +;; 测试多集合并集 +(define s-union-2 (set-union s-1 s-2-3-4 s-4-5)) +(check (set-size s-union-2) => 5) +(check-true (set-contains? s-union-2 1)) +(check-true (set-contains? s-union-2 5)) + +;; 测试元素来源(使用大小写不敏感比较器) +(define s-union-ci-1 (list->set-with-comparator string-ci-comparator '("Apple"))) +(define s-union-ci-2 (list->set-with-comparator string-ci-comparator '("apple" "Banana"))) +(define s-union-ci (set-union s-union-ci-1 s-union-ci-2)) +(check (set-member s-union-ci "apple" 'not-found) => "Apple") +(check (set-member s-union-ci "banana" 'not-found) => "Banana") + +;; 测试类型与比较器错误 +(check-catch 'type-error (set-union "not a set" s-1)) +(check-catch 'value-error (set-union s-1 s-str-ci)) + +#| +set-intersection +返回多个 set 的交集。 + +语法 +---- +(set-intersection set1 set2 ...) + +参数 +---- +set1, set2 ... : set +参与交集的 set。 + +返回值 +------ +返回新的 set,元素来自第一个 set。 +|# + +;; 测试 set-intersection 基本行为 +(define s-inter-1 (set-intersection s-1-2-3 s-2-3-4)) +(check (set-size s-inter-1) => 2) +(check-true (set-contains? s-inter-1 2)) +(check-true (set-contains? s-inter-1 3)) + +;; 测试多集合交集 +(define s-inter-2 (set-intersection s-1-2-3 s-2-3-4 (set 2 3))) +(check (set-size s-inter-2) => 2) +(check-true (set-contains? s-inter-2 2)) +(check-true (set-contains? s-inter-2 3)) + +;; 测试元素来源(使用大小写不敏感比较器) +(define s-inter-ci-1 (list->set-with-comparator string-ci-comparator '("Apple" "Banana"))) +(define s-inter-ci-2 (list->set-with-comparator string-ci-comparator '("apple" "Pear"))) +(define s-inter-ci (set-intersection s-inter-ci-1 s-inter-ci-2)) +(check (set-member s-inter-ci "apple" 'not-found) => "Apple") +(check (set-size s-inter-ci) => 1) + +;; 测试类型与比较器错误 +(check-catch 'type-error (set-intersection "not a set" s-1)) +(check-catch 'value-error (set-intersection s-1 s-str-ci)) + +#| +set-difference +返回第一个 set 与其余 set 的差集。 + +语法 +---- +(set-difference set1 set2 ...) + +参数 +---- +set1, set2 ... : set +参与差集的 set。 + +返回值 +------ +返回新的 set,元素来自第一个 set。 +|# + +;; 测试 set-difference 基本行为 +(define s-diff-1 (set-difference s-1-2-3 s-2-3-4)) +(check (set-size s-diff-1) => 1) +(check-true (set-contains? s-diff-1 1)) +(check-false (set-contains? s-diff-1 2)) +(check-false (set-contains? s-diff-1 3)) + +;; 测试多集合差集 +(define s-diff-2 (set-difference s-1-2-3 s-2-3-4 s-4-5)) +(check (set-size s-diff-2) => 1) +(check-true (set-contains? s-diff-2 1)) + +;; 测试元素来源(使用大小写不敏感比较器) +(define s-diff-ci-1 (list->set-with-comparator string-ci-comparator '("Apple" "Banana"))) +(define s-diff-ci-2 (list->set-with-comparator string-ci-comparator '("apple"))) +(define s-diff-ci (set-difference s-diff-ci-1 s-diff-ci-2)) +(check (set-size s-diff-ci) => 1) +(check (set-member s-diff-ci "banana" 'not-found) => "Banana") + +;; 测试类型与比较器错误 +(check-catch 'type-error (set-difference "not a set" s-1)) +(check-catch 'value-error (set-difference s-1 s-str-ci)) + +#| +set-xor +返回两个 set 的对称差集。 + +语法 +---- +(set-xor set1 set2) + +参数 +---- +set1, set2 : set +参与对称差集的 set。 + +返回值 +------ +返回新的 set。 +|# + +;; 测试 set-xor 基本行为 +(define s-xor-1 (set-xor s-1-2-3 s-2-3-4)) +(check (set-size s-xor-1) => 2) +(check-true (set-contains? s-xor-1 1)) +(check-true (set-contains? s-xor-1 4)) + +;; 测试元素来源(使用大小写不敏感比较器) +(define s-xor-ci (set-xor s-union-ci-1 s-union-ci-2)) +(check (set-size s-xor-ci) => 1) +(check (set-member s-xor-ci "banana" 'not-found) => "Banana") +(check-true (set-contains? s-xor-ci "banana")) +(check-false (set-contains? s-xor-ci "apple")) + +;; 测试类型与比较器错误 +(check-catch 'type-error (set-xor "not a set" s-1)) +(check-catch 'value-error (set-xor s-1 s-str-ci)) + +#| +set-union! +将多个 set 并入 set1(可变操作)。 + +语法 +---- +(set-union! set1 set2 ...) + +参数 +---- +set1, set2 ... : set +参与并集的 set。 + +返回值 +------ +返回修改后的 set1,元素来自它们首次出现的 set。 +|# + +;; 测试 set-union! 基本行为 +(define s-union!-1 (set 1 2 3)) +(define s-union!-result (set-union! s-union!-1 s-2-3-4 s-4-5)) +(check-true (eq? s-union!-result s-union!-1)) +(check (set-size s-union!-1) => 5) +(check-true (set-contains? s-union!-1 1)) +(check-true (set-contains? s-union!-1 5)) + +;; 测试元素来源(使用大小写不敏感比较器) +(define s-union!-ci-1 (list->set-with-comparator string-ci-comparator '("Apple"))) +(define s-union!-ci-2 (list->set-with-comparator string-ci-comparator '("apple" "Banana"))) +(define s-union!-ci (set-union! s-union!-ci-1 s-union!-ci-2)) +(check-true (eq? s-union!-ci s-union!-ci-1)) +(check (set-member s-union!-ci "apple" 'not-found) => "Apple") +(check (set-member s-union!-ci "banana" 'not-found) => "Banana") + +;; 测试类型与比较器错误 +(check-catch 'type-error (set-union! "not a set" s-1)) +(check-catch 'value-error (set-union! s-1 s-str-ci)) + +#| +set-intersection! +就地更新 set1,使其成为多个 set 的交集。 + +语法 +---- +(set-intersection! set1 set2 ...) + +参数 +---- +set1, set2 ... : set +参与交集的 set。 + +返回值 +------ +返回修改后的 set1,元素来自第一个 set。 +|# + +;; 测试 set-intersection! 基本行为 +(define s-inter!-1 (set 1 2 3 4)) +(define s-inter!-result (set-intersection! s-inter!-1 s-2-3-4)) +(check-true (eq? s-inter!-result s-inter!-1)) +(check (set-size s-inter!-1) => 3) +(check-true (set-contains? s-inter!-1 2)) +(check-true (set-contains? s-inter!-1 3)) +(check-true (set-contains? s-inter!-1 4)) + +;; 测试多集合交集 +(define s-inter!-2 (set 1 2 3 4)) +(set-intersection! s-inter!-2 s-2-3-4 (set 2 3)) +(check (set-size s-inter!-2) => 2) +(check-true (set-contains? s-inter!-2 2)) +(check-true (set-contains? s-inter!-2 3)) + +;; 测试元素来源(使用大小写不敏感比较器) +(define s-inter!-ci-1 (list->set-with-comparator string-ci-comparator '("Apple" "Banana"))) +(define s-inter!-ci-2 (list->set-with-comparator string-ci-comparator '("apple" "Pear"))) +(set-intersection! s-inter!-ci-1 s-inter!-ci-2) +(check (set-member s-inter!-ci-1 "apple" 'not-found) => "Apple") +(check (set-size s-inter!-ci-1) => 1) + +;; 测试类型与比较器错误 +(check-catch 'type-error (set-intersection! "not a set" s-1)) +(check-catch 'value-error (set-intersection! s-1 s-str-ci)) + +#| +set-difference! +就地更新 set1,使其成为与其余 set 的差集。 + +语法 +---- +(set-difference! set1 set2 ...) + +参数 +---- +set1, set2 ... : set +参与差集的 set。 + +返回值 +------ +返回修改后的 set1,元素来自第一个 set。 +|# + +;; 测试 set-difference! 基本行为 +(define s-diff!-1 (set 1 2 3 4)) +(define s-diff!-result (set-difference! s-diff!-1 s-2-3-4)) +(check-true (eq? s-diff!-result s-diff!-1)) +(check (set-size s-diff!-1) => 1) +(check-true (set-contains? s-diff!-1 1)) +(check-false (set-contains? s-diff!-1 2)) + +;; 测试多集合差集 +(define s-diff!-2 (set 1 2 3 4 5)) +(set-difference! s-diff!-2 s-2-3-4 s-4-5) +(check (set-size s-diff!-2) => 1) +(check-true (set-contains? s-diff!-2 1)) + +;; 测试元素来源(使用大小写不敏感比较器) +(define s-diff!-ci-1 (list->set-with-comparator string-ci-comparator '("Apple" "Banana"))) +(define s-diff!-ci-2 (list->set-with-comparator string-ci-comparator '("apple"))) +(set-difference! s-diff!-ci-1 s-diff!-ci-2) +(check (set-size s-diff!-ci-1) => 1) +(check (set-member s-diff!-ci-1 "banana" 'not-found) => "Banana") + +;; 测试类型与比较器错误 +(check-catch 'type-error (set-difference! "not a set" s-1)) +(check-catch 'value-error (set-difference! s-1 s-str-ci)) + +#| +set-xor! +就地更新 set1,使其成为与 set2 的对称差集。 + +语法 +---- +(set-xor! set1 set2) + +参数 +---- +set1, set2 : set +参与对称差集的 set。 + +返回值 +------ +返回修改后的 set1。 +|# + +;; 测试 set-xor! 基本行为 +(define s-xor!-1 (set 1 2 3)) +(define s-xor!-result (set-xor! s-xor!-1 s-2-3-4)) +(check-true (eq? s-xor!-result s-xor!-1)) +(check (set-size s-xor!-1) => 2) +(check-true (set-contains? s-xor!-1 1)) +(check-true (set-contains? s-xor!-1 4)) + +;; 测试元素来源(使用大小写不敏感比较器) +(define s-xor!-ci-1 (list->set-with-comparator string-ci-comparator '("Apple"))) +(define s-xor!-ci-2 (list->set-with-comparator string-ci-comparator '("apple" "Banana"))) +(set-xor! s-xor!-ci-1 s-xor!-ci-2) +(check (set-size s-xor!-ci-1) => 1) +(check (set-member s-xor!-ci-1 "banana" 'not-found) => "Banana") +(check-false (set-contains? s-xor!-ci-1 "apple")) + +;; 测试类型与比较器错误 +(check-catch 'type-error (set-xor! "not a set" s-1)) +(check-catch 'value-error (set-xor! s-1 s-str-ci)) + +#| +set->list +将 set 转换为列表(顺序未指定)。 + +语法 +---- +(set->list set) + +参数 +---- +set : set +源 set。 + +返回值 +------ +返回包含 set 元素的新列表。 +|# + +;; 测试 set->list 基本行为 +(define s-to-list (set 1 2 3)) +(define l-to-list (set->list s-to-list)) +(check (length l-to-list) => 3) +(check-true (set=? (list->set l-to-list) s-to-list)) + +;; 测试类型错误 +(check-catch 'type-error (set->list "not a set")) + + +#| +set-adjoin +返回一个新的 set,包含原 set 的所有元素以及新增的元素。 + +语法 +---- +(set-adjoin set element ...) + +参数 +---- +set : set +初始 set。 + +element ... : any +要添加的元素。 + +返回值 +------ +返回一个新的 set。 + +注意 +---- +此函数不修改原 set。 +|# + +;; 测试 set-adjoin 函数 +(define s-adjoin-1 (set-adjoin s-empty 1)) +(check (set-size s-adjoin-1) => 1) +(check-true (set-contains? s-adjoin-1 1)) +(check-true (set-empty? s-empty)) ; 原 set 不变 + +(define s-adjoin-2 (set-adjoin s-1 2 3)) +(check (set-size s-adjoin-2) => 3) +(check-true (set-contains? s-adjoin-2 1)) +(check-true (set-contains? s-adjoin-2 2)) +(check-true (set-contains? s-adjoin-2 3)) + +;; 测试添加已存在的元素 +(define s-adjoin-3 (set-adjoin s-1 1)) +(check (set-size s-adjoin-3) => 1) +(check-true (set-contains? s-adjoin-3 1)) + +;; 测试类型错误 +(check-catch 'type-error (set-adjoin "not a set" 1)) + +#| +set-adjoin! +向 set 中添加一个或多个元素(可变操作)。 + +语法 +---- +(set-adjoin! set element ...) + +参数 +---- +set : set +目标 set。 + +element ... : any +要添加的元素。 + +返回值 +------ +返回修改后的 set(与传入的 set 是同一个对象)。 + +注意 +---- +此函数会修改原 set。 +|# + +;; 测试 set-adjoin! 函数 +(define s-mut (set-copy s-empty)) +(set-adjoin! s-mut 1) +(check (set-size s-mut) => 1) +(check-true (set-contains? s-mut 1)) + +(set-adjoin! s-mut 2 3) +(check (set-size s-mut) => 3) +(check-true (set-contains? s-mut 1)) +(check-true (set-contains? s-mut 2)) +(check-true (set-contains? s-mut 3)) + +;; 测试添加已存在的元素 +(set-adjoin! s-mut 1) +(check (set-size s-mut) => 3) + +;; 测试类型错误 +(check-catch 'type-error (set-adjoin! "not a set" 1)) + +#| +set-replace +返回一个新的 set,其中指定的元素被替换。 + +语法 +---- +(set-replace set element) + +参数 +---- +set : set +初始 set。 + +element : any +用来替换的元素。 + +返回值 +------ +返回一个新的 set。 +如果 set 中包含与 element 相等的元素(根据比较器),则该元素被 element 替换(对于 equals 但 not eq? 的情况很有用)。 +如果 set 中不包含 element,则返回原 set(不做任何修改)。 + +注意 +---- +此函数不修改原 set。 +|# + +;; 测试 set-replace 函数 +(define s-replace-1 (set-replace s-1 1)) +(check (set-size s-replace-1) => 1) +(check-true (set-contains? s-replace-1 1)) +(check-true (set=? s-replace-1 s-1)) ; 内容相同 +(check-false (eq? s-replace-1 s-1)) ; 但应该是新分配的 set (因为确实发生了替换逻辑) + +(define s-replace-2 (set-replace s-1 2)) +(check-true (eq? s-replace-2 s-1)) ; 不包含 2,返回原 set + +;; 测试替换 equals 但 not eq? 的元素 +(define s-str-ci-2 (list->set-with-comparator string-ci-comparator '("Apple" "Banana"))) +(check (set-member s-str-ci-2 "apple" 'not-found) => "Apple") + +(define s-replace-3 (set-replace s-str-ci-2 "apple")) +(check (set-member s-replace-3 "apple" 'not-found) => "apple") ; 应该被替换为 "apple" +(check-false (eq? s-replace-3 s-str-ci-2)) ; 应该是新 set + +;; 测试类型错误 +(check-catch 'type-error (set-replace "not a set" 1)) + +#| +set-replace! +修改 set,将其中指定的元素替换(可变操作)。 + +语法 +---- +(set-replace! set element) + +参数 +---- +set : set +目标 set。 + +element : any +用来替换的元素。 + +返回值 +------ +返回修改后的 set(与传入的 set 是同一个对象)。 + +注意 +---- +此函数会修改原 set。 +|# + +;; 测试 set-replace! 函数 +(define s-mut-replace (set-copy s-1)) +(set-replace! s-mut-replace 1) +(check (set-size s-mut-replace) => 1) +(check-true (set-contains? s-mut-replace 1)) + +;; 测试替换 equals 但 not eq? 的元素 +(define s-str-ci-mut (list->set-with-comparator string-ci-comparator '("Apple" "Banana"))) +(check (set-member s-str-ci-mut "apple" 'not-found) => "Apple") + +(set-replace! s-str-ci-mut "apple") +(check (set-member s-str-ci-mut "apple" 'not-found) => "apple") ; 应该被替换为 "apple" + +;; 测试不存在的元素 +(set-replace! s-str-ci-mut "Pear") +(check (set-size s-str-ci-mut) => 2) ; 不变 + +;; 测试类型错误 +(check-catch 'type-error (set-replace! "not a set" 1)) + +#| +set-delete +返回一个新的 set,其中指定的元素被移除。 + +语法 +---- +(set-delete set element ...) + +参数 +---- +set : set +初始 set。 + +element ... : any +要移除的元素。 + +返回值 +------ +返回一个新的 set。 +如果元素不存在,则忽略。 + +注意 +---- +此函数不修改原 set。 +|# + +;; 测试 set-delete 函数 +(define s-del-1 (set-delete s-1-2-3 1)) +(check (set-size s-del-1) => 2) +(check-false (set-contains? s-del-1 1)) +(check-true (set-contains? s-del-1 2)) +(check-true (set-contains? s-del-1 3)) + +(define s-del-2 (set-delete s-1-2-3 4)) ; 移除不存在的元素 +(check (set-size s-del-2) => 3) +(check-true (set=? s-del-2 s-1-2-3)) + +(define s-del-3 (set-delete s-1-2-3 1 2)) +(check (set-size s-del-3) => 1) +(check-false (set-contains? s-del-3 1)) +(check-false (set-contains? s-del-3 2)) +(check-true (set-contains? s-del-3 3)) + +#| +set-delete! +从 set 中移除指定的元素(可变操作)。 + +语法 +---- +(set-delete! set element ...) + +参数 +---- +set : set +目标 set。 + +element ... : any +要移除的元素。 + +返回值 +------ +返回修改后的 set(与传入的 set 是同一个对象)。 + +注意 +---- +此函数会修改原 set。 +|# + +;; 测试 set-delete! 函数 +(define s-mut-del (set-copy s-1-2-3)) +(set-delete! s-mut-del 1) +(check (set-size s-mut-del) => 2) +(check-false (set-contains? s-mut-del 1)) + +(set-delete! s-mut-del 2 3) +(check (set-size s-mut-del) => 0) +(check-true (set-empty? s-mut-del)) + +#| +set-delete-all +返回一个新的 set,其中指定列表中的元素被移除。 + +语法 +---- +(set-delete-all set element-list) + +参数 +---- +set : set +初始 set。 + +element-list : list +要移除的元素列表。 + +返回值 +------ +返回一个新的 set。 +|# + +;; 测试 set-delete-all 函数 +(define s-del-all (set-delete-all s-1-2-3 '(1 2))) +(check (set-size s-del-all) => 1) +(check-false (set-contains? s-del-all 1)) +(check-false (set-contains? s-del-all 2)) +(check-true (set-contains? s-del-all 3)) + +#| +set-delete-all! +从 set 中移除指定列表中的元素(可变操作)。 + +语法 +---- +(set-delete-all! set element-list) + +参数 +---- +set : set +目标 set。 + +element-list : list +要移除的元素列表。 + +返回值 +------ +返回修改后的 set。 +|# + +;; 测试 set-delete-all! 函数 +(define s-mut-del-all (set-copy s-1-2-3)) +(set-delete-all! s-mut-del-all '(1 2)) +(check (set-size s-mut-del-all) => 1) +(check-false (set-contains? s-mut-del-all 1)) +(check-false (set-contains? s-mut-del-all 2)) +(check-true (set-contains? s-mut-del-all 3)) + +(check-report) diff --git a/tests/goldfish/liii/string-test.scm b/tests/goldfish/liii/string-test.scm index 2c62303e2dbc484a46ef283f1ca88ca44facac85..f42b9f54e26ddf7226fdfb2b371ff919245fafc6 100644 --- a/tests/goldfish/liii/string-test.scm +++ b/tests/goldfish/liii/string-test.scm @@ -1285,6 +1285,415 @@ out-of-range 当start/end超出字符串索引范围时 (check (string-trim-both "123hello123" char-numeric? 3 8) => "hello") (check (string-trim-both "123hello123" char-numeric? 3) => "hello") +#| +string-index-right +在字符串中从右向左查找指定字符或满足条件的第一个字符的位置。 + +语法 +---- +(string-index-right str char/pred?) +(string-index-right str char/pred? start) +(string-index-right str char/pred? start end) + +参数 +---- +str : string? +要搜索的源字符串。 + +char/pred? : char? 或 procedure? +- 字符(char):要查找的目标字符 +- 谓词(procedure):接受单个字符作为参数的函数,返回布尔值指示是否匹配 + +start : integer? 可选 +搜索的起始位置(包含),默认为0。 + +end : integer? 可选 +搜索的结束位置(不包含),默认为字符串长度。 + +返回值 +---- +integer 或 #f +- 如果找到匹配的字符,返回其索引位置(从0开始计数) +- 如果未找到匹配的字符,返回#f + +注意 +---- +string-index-right从字符串的右侧(末尾)开始搜索,返回第一个匹配字符的索引位置。 +搜索范围由start和end参数限定。空字符串或未找到匹配项时返回#f。 + +该函数支持使用字符和谓词两种方式进行查找: +- 字符匹配:查找与指定字符相等的字符 +- 谓词匹配:查找使谓词返回#t的第一个字符 + +与string-index的主要区别是搜索方向:string-index从左向右搜索,string-index-right从右向左搜索。 + +示例 +---- +(string-index-right "hello" #\l) => 3 (从右向左第一个'l'在索引3处) +(string-index-right "hello" #\z) => #f (没有找到字符'z') +(string-index-right "abc123" char-numeric?) => 5 (最后一个数字'3'在索引5处) +(string-index-right "hello" char-alphabetic?) => 4 (最后一个字母'o'在索引4处) +(string-index-right "hello" #\l 0 4) => 2 (在0到4范围内从右向左找字符'l') +(string-index-right "" #\x) => #f (空字符串返回#f) + +错误处理 +---- +wrong-type-arg 当str不是字符串类型时 +wrong-type-arg 当char/pred?不是字符或谓词时 +out-of-range 当start/end超出字符串索引范围时 +|# + +; Basic functionality tests for string-index-right +(check (string-index-right "hello" #\l) => 3) +(check (string-index-right "hello" #\z) => #f) +(check (string-index-right "hello" #\l) => 3) +(check (string-index-right "hello" #\l 0 3) => 2) +(check (string-index-right "abc123" char-numeric?) => 5) +(check (string-index-right "abc123" char-alphabetic?) => 2) +(check (string-index-right "" #\x) => #f) + +; Character parameter tests for string-index-right +(check (string-index-right "0123456789" #\2) => 2) +(check (string-index-right "0123456789" #\2 0 3) => 2) +(check (string-index-right "0123456789" #\2 0 2) => #f) +(check (string-index-right "abccba" #\a) => 5) +(check (string-index-right "hello world" #\space) => 5) + +; Extended comprehensive string-index-right tests +(check (string-index-right "hello" #\h) => 0) +(check (string-index-right "hello" #\o) => 4) +(check (string-index-right "hello hello" #\space) => 5) +(check (string-index-right "hello" #\H) => #f) ; case-sensitive +(check (string-index-right "" #\a) => #f) +(check (string-index-right "a" #\a) => 0) +(check (string-index-right "aaaa" #\a) => 3) +(check (string-index-right "0123456789" #\0) => 0) +(check (string-index-right "0123456789" #\9) => 9) + +; Predicate parameter tests for string-index-right +(check (string-index-right "0123456789" char-numeric?) => 9) +(check (string-index-right "abc123" char-numeric?) => 5) +(check (string-index-right "123abc" char-alphabetic?) => 5) +(check (string-index-right "Hello123" char-upper-case?) => 0) +(check (string-index-right "hello123" char-upper-case?) => #f) +(check (string-index-right "123!@#" char-alphabetic?) => #f) +(check (string-index-right "hello\n\t " char-whitespace?) => 7) +(check (string-index-right "hello" (lambda (c) (char=? c #\l))) => 3) + +; Single character edge cases +(check (string-index-right "a" #\a) => 0) +(check (string-index-right "a" #\b) => #f) +(check (string-index-right " " #\space) => 0) +(check (string-index-right "\t" char-whitespace?) => 0) + +; Start and end parameter tests +(check (string-index-right "hello" #\l 0) => 3) +(check (string-index-right "hello" #\l 1) => 3) +(check (string-index-right "hello" #\l 2) => 3) +(check (string-index-right "hello" #\l 3) => 3) +(check (string-index-right "hello" #\l 4) => #f) +(check (string-index-right "hello" #\l 0 3) => 2) +(check (string-index-right "hello" #\l 0 2) => #f) +(check (string-index-right "hello" #\l 1 4) => 3) +(check (string-index-right "hello" #\l 2 4) => 3) +(check (string-index-right "hello" #\l 3 4) => 3) +(check (string-index-right "hello" #\l 3 3) => #f) +(check (string-index-right "hello" #\l 0 1) => #f) + +; Special characters and edge cases +(check (string-index-right "_test" #\_) => 0) +(check (string-index-right "a@b" #\@) => 1) +(check (string-index-right "hello,world" #\,) => 5) +(check (string-index-right "a-b-c" #\-) => 3) + +; Complex predicates +(check (string-index-right "123abc!@#" (lambda (c) (or (char-alphabetic? c) (char-numeric? c)))) => 5) +(check (string-index-right "!@#abc123" (lambda (c) (or (char-alphabetic? c) (char-numeric? c)))) => 8) +(check (string-index-right "abc123" char-upper-case?) => #f) +(check (string-index-right "ABC123" char-upper-case?) => 2) +(check (string-index-right "abcABC" char-upper-case?) => 5) + +; Error handling tests for string-index-right +(check-catch 'wrong-type-arg (string-index-right 123 #\a)) +(check-catch 'wrong-type-arg (string-index-right "hello" "a")) +(check-catch 'wrong-type-arg (string-index-right "hello" 123)) +(check-catch 'wrong-type-arg (string-index-right "hello" '(a))) +(check-catch 'out-of-range (string-index-right "hello" #\a -1)) +(check-catch 'out-of-range (string-index-right "hello" #\a 0 6)) +(check-catch 'out-of-range (string-index-right "hello" #\a 3 2)) +(check-catch 'out-of-range (string-index-right "" #\a 1)) +(check-catch 'out-of-range (string-index-right "abc" #\a 5)) + +#| +string-skip +在字符串中从左向右跳过指定字符或满足条件的字符,返回第一个不满足条件的字符位置。 + +语法 +---- +(string-skip str char/pred?) +(string-skip str char/pred? start) +(string-skip str char/pred? start end) + +参数 +---- +str : string? +要搜索的源字符串。 + +char/pred? : char? 或 procedure? +- 字符(char):要跳过的目标字符 +- 谓词(procedure):接受单个字符作为参数的函数,返回布尔值指示是否跳过该字符 + +start : integer? 可选 +搜索的起始位置(包含),默认为0。 + +end : integer? 可选 +搜索的结束位置(不包含),默认为字符串长度。 + +返回值 +---- +integer 或 #f +- 如果找到不匹配的字符,返回其索引位置(从0开始计数) +- 如果所有字符都匹配(都满足跳过条件),返回#f + +注意 +---- +string-skip从字符串的左侧(开头)开始搜索,跳过所有满足条件的字符,返回第一个不满足条件的字符索引。 +搜索范围由start和end参数限定。如果指定范围内的所有字符都满足跳过条件,则返回#f。 + +该函数支持使用字符和谓词两种方式: +- 字符匹配:跳过与指定字符相等的字符 +- 谓词匹配:跳过使谓词返回#t的字符 + +string-skip是string-index的补充:string-index查找满足条件的字符,string-skip查找不满足条件的字符。 + +示例 +---- +(string-skip " hello" #\space) => 3 (跳过空格,第一个非空格字符'h'在索引3处) +(string-skip "aaaa" #\a) => #f (所有字符都是'a',没有不匹配的字符) +(string-skip "123abc" char-numeric?) => 3 (跳过数字,第一个非数字字符'a'在索引3处) +(string-skip " " #\space) => #f (所有字符都是空格) +(string-skip "hello" #\h 1) => 1 (从索引1开始,第一个字符'e'不是'h',所以在索引1处) + +错误处理 +---- +wrong-type-arg 当str不是字符串类型时 +wrong-type-arg 当char/pred?不是字符或谓词时 +out-of-range 当start/end超出字符串索引范围时 +|# + +; Basic functionality tests for string-skip +(check (string-skip " hello" #\space) => 3) +(check (string-skip "aaaa" #\a) => #f) +(check (string-skip "123abc" char-numeric?) => 3) +(check (string-skip "abc123" char-alphabetic?) => 3) +(check (string-skip "" #\space) => #f) + +; Character parameter tests for string-skip +(check (string-skip "xxxabc" #\x) => 3) +(check (string-skip "xxxabc" #\x 2) => 3) +(check (string-skip "xxxabc" #\x 4) => 4) +(check (string-skip " \t\n " char-whitespace?) => #f) + +; Extended comprehensive string-skip tests +(check (string-skip "hello" #\h) => 1) +(check (string-skip "hhhello" #\h) => 3) +(check (string-skip "hhh" #\h) => #f) +(check (string-skip "hello world" #\h) => 1) +(check (string-skip "hello" #\x) => 0) +(check (string-skip "" #\a) => #f) +(check (string-skip "a" #\a) => #f) +(check (string-skip "a" #\b) => 0) +(check (string-skip "0123456789" #\0) => 1) +(check (string-skip "0123456789" #\1) => 0) + +; Predicate parameter tests for string-skip +(check (string-skip "0123456789" char-numeric?) => #f) +(check (string-skip "abc123" char-numeric?) => 0) +(check (string-skip "123abc" char-alphabetic?) => 0) +(check (string-skip "Hello123" char-upper-case?) => 1) +(check (string-skip "HELLO" char-upper-case?) => #f) +(check (string-skip "123!@#" char-alphabetic?) => 0) +(check (string-skip " hello" char-whitespace?) => 3) +(check (string-skip "hello" (lambda (c) (char=? c #\h))) => 1) + +; Single character edge cases +(check (string-skip "a" #\a) => #f) +(check (string-skip "a" #\b) => 0) +(check (string-skip " " #\space) => #f) +(check (string-skip "\t" char-whitespace?) => #f) + +; Start and end parameter tests +(check (string-skip "xxxabc" #\x 0) => 3) +(check (string-skip "xxxabc" #\x 1) => 3) +(check (string-skip "xxxabc" #\x 2) => 3) +(check (string-skip "xxxabc" #\x 3) => 3) +(check (string-skip "xxxabc" #\x 4) => 4) +(check (string-skip "xxxabc" #\x 0 3) => #f) +(check (string-skip "xxxabc" #\x 0 4) => 3) +(check (string-skip "xxxabc" #\x 1 4) => 3) +(check (string-skip "xxxabc" #\x 2 4) => 3) +(check (string-skip "xxxabc" #\x 3 4) => 3) +(check (string-skip "xxxabc" #\x 3 3) => #f) + +; Special characters and edge cases +(check (string-skip "___test" #\_) => 3) +(check (string-skip "a@@b" #\@) => 0) +(check (string-skip "---a" #\-) => 3) +(check (string-skip ",,hello" #\,) => 2) + +; Complex predicates +(check (string-skip "123abc!@#" (lambda (c) (or (char-alphabetic? c) (char-numeric? c)))) => 6) +(check (string-skip " abc123" char-whitespace?) => 3) +(check (string-skip "abc123" char-upper-case?) => 0) +(check (string-skip "ABC123" char-upper-case?) => 3) +(check (string-skip "ABCabc" char-upper-case?) => 3) + +; Error handling tests for string-skip +(check-catch 'wrong-type-arg (string-skip 123 #\a)) +(check-catch 'wrong-type-arg (string-skip "hello" "a")) +(check-catch 'wrong-type-arg (string-skip "hello" 123)) +(check-catch 'wrong-type-arg (string-skip "hello" '(a))) +(check-catch 'out-of-range (string-skip "hello" #\a -1)) +(check-catch 'out-of-range (string-skip "hello" #\a 0 6)) +(check-catch 'out-of-range (string-skip "hello" #\a 3 2)) +(check-catch 'out-of-range (string-skip "" #\a 1)) +(check-catch 'out-of-range (string-skip "abc" #\a 5)) + +#| +string-skip-right +在字符串中从右向左跳过指定字符或满足条件的字符,返回第一个不满足条件的字符位置。 + +语法 +---- +(string-skip-right str char/pred?) +(string-skip-right str char/pred? start) +(string-skip-right str char/pred? start end) + +参数 +---- +str : string? +要搜索的源字符串。 + +char/pred? : char? 或 procedure? +- 字符(char):要跳过的目标字符 +- 谓词(procedure):接受单个字符作为参数的函数,返回布尔值指示是否跳过该字符 + +start : integer? 可选 +搜索的起始位置(包含),默认为0。 + +end : integer? 可选 +搜索的结束位置(不包含),默认为字符串长度。 + +返回值 +---- +integer 或 #f +- 如果找到不匹配的字符,返回其索引位置(从0开始计数) +- 如果所有字符都匹配(都满足跳过条件),返回#f + +注意 +---- +string-skip-right从字符串的右侧(末尾)开始搜索,跳过所有满足条件的字符,返回第一个不满足条件的字符索引。 +搜索范围由start和end参数限定。如果指定范围内的所有字符都满足跳过条件,则返回#f。 + +该函数支持使用字符和谓词两种方式: +- 字符匹配:跳过与指定字符相等的字符 +- 谓词匹配:跳过使谓词返回#t的字符 + +string-skip-right是string-index-right的补充:string-index-right查找满足条件的字符,string-skip-right查找不满足条件的字符。 + +示例 +---- +(string-skip-right "hello " #\space) => 4 (从右向左跳过空格,第一个非空格字符'o'在索引4处) +(string-skip-right "aaaa" #\a) => #f (所有字符都是'a',没有不匹配的字符) +(string-skip-right "abc123" char-numeric?) => 2 (从右向左跳过数字,第一个非数字字符'c'在索引2处) +(string-skip-right " " #\space) => #f (所有字符都是空格) +(string-skip-right "helloh" #\h 1) => 4 (从索引1开始,从右向左第一个不是'h'的字符在索引4处) + +错误处理 +---- +wrong-type-arg 当str不是字符串类型时 +wrong-type-arg 当char/pred?不是字符或谓词时 +out-of-range 当start/end超出字符串索引范围时 +|# + +; Basic functionality tests for string-skip-right +(check (string-skip-right "hello " #\space) => 4) +(check (string-skip-right "aaaa" #\a) => #f) +(check (string-skip-right "abc123" char-numeric?) => 2) +(check (string-skip-right "123abc" char-alphabetic?) => 2) +(check (string-skip-right "" #\space) => #f) + +; Character parameter tests for string-skip-right +(check (string-skip-right "abcxxx" #\x) => 2) +(check (string-skip-right "abcxxx" #\x 0 5) => 2) +(check (string-skip-right "abcxxx" #\x 0 4) => 2) +(check (string-skip-right " \t\n " char-whitespace?) => #f) + +; Extended comprehensive string-skip-right tests +(check (string-skip-right "helloh" #\h) => 4) +(check (string-skip-right "hellohh" #\h) => 4) +(check (string-skip-right "hhh" #\h) => #f) +(check (string-skip-right "hello world" #\d) => 9) +(check (string-skip-right "hello" #\x) => 4) +(check (string-skip-right "" #\a) => #f) +(check (string-skip-right "a" #\a) => #f) +(check (string-skip-right "a" #\b) => 0) +(check (string-skip-right "0123456789" #\9) => 8) +(check (string-skip-right "0123456789" #\8) => 9) + +; Predicate parameter tests for string-skip-right +(check (string-skip-right "0123456789" char-numeric?) => #f) +(check (string-skip-right "abc123" char-numeric?) => 2) +(check (string-skip-right "123abc" char-alphabetic?) => 2) +(check (string-skip-right "Hello123" char-upper-case?) => 7) +(check (string-skip-right "HELLO" char-upper-case?) => #f) +(check (string-skip-right "123!@#" char-alphabetic?) => 5) +(check (string-skip-right "hello " char-whitespace?) => 4) +(check (string-skip-right "helloh" (lambda (c) (char=? c #\h))) => 4) + +; Single character edge cases +(check (string-skip-right "a" #\a) => #f) +(check (string-skip-right "a" #\b) => 0) +(check (string-skip-right " " #\space) => #f) +(check (string-skip-right "\t" char-whitespace?) => #f) + +; Start and end parameter tests +(check (string-skip-right "abcxxx" #\x 0) => 2) +(check (string-skip-right "abcxxx" #\x 1) => 2) +(check (string-skip-right "abcxxx" #\x 2) => 2) +(check (string-skip-right "abcxxx" #\x 3) => #f) +(check (string-skip-right "abcxxx" #\x 4) => #f) +(check (string-skip-right "abcxxx" #\x 0 3) => 2) +(check (string-skip-right "abcxxx" #\x 0 4) => 2) +(check (string-skip-right "abcxxx" #\x 1 4) => 2) +(check (string-skip-right "abcxxx" #\x 2 4) => 2) +(check (string-skip-right "abcxxx" #\x 3 4) => #f) +(check (string-skip-right "abcxxx" #\x 3 3) => #f) + +; Special characters and edge cases +(check (string-skip-right "test___" #\_) => 3) +(check (string-skip-right "b@@a" #\@) => 3) +(check (string-skip-right "a---" #\-) => 0) +(check (string-skip-right "hello,," #\,) => 4) + +; Complex predicates +(check (string-skip-right "!@#abc123" (lambda (c) (or (char-alphabetic? c) (char-numeric? c)))) => 2) +(check (string-skip-right "abc123 " char-whitespace?) => 5) +(check (string-skip-right "abc123" char-upper-case?) => 5) +(check (string-skip-right "ABC123" char-upper-case?) => 5) +(check (string-skip-right "abcABC" char-upper-case?) => 2) + +; Error handling tests for string-skip-right +(check-catch 'wrong-type-arg (string-skip-right 123 #\a)) +(check-catch 'wrong-type-arg (string-skip-right "hello" "a")) +(check-catch 'wrong-type-arg (string-skip-right "hello" 123)) +(check-catch 'wrong-type-arg (string-skip-right "hello" '(a))) +(check-catch 'out-of-range (string-skip-right "hello" #\a -1)) +(check-catch 'out-of-range (string-skip-right "hello" #\a 0 6)) +(check-catch 'out-of-range (string-skip-right "hello" #\a 3 2)) +(check-catch 'out-of-range (string-skip-right "" #\a 1)) +(check-catch 'out-of-range (string-skip-right "abc" #\a 5)) + #| string-index 在字符串中查找指定字符或满足条件的第一个字符的位置。 diff --git a/tests/goldfish/scheme/file-test.scm b/tests/goldfish/scheme/file-test.scm new file mode 100644 index 0000000000000000000000000000000000000000..d814992f8d86241e5eadff2ac23e23bd2a651b2d --- /dev/null +++ b/tests/goldfish/scheme/file-test.scm @@ -0,0 +1,135 @@ +; +; Copyright (C) 2026 The Goldfish Scheme Authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +; License for the specific language governing permissions and limitations +; under the License. +; + +(import (liii check)) + +(check-set-mode! 'report-failed) + +(define test-filename "file-test-中文.txt") +(define test-filenames + '("中文.txt" + "測試.txt" + "日本語.txt" + "한글.txt" + " ελληνικά.txt" + "ملف.txt")) + +(define (clean-filename filename) + (lambda () (delete-file filename))) +(define clean-test-filename (clean-filename test-filename)) + +;; with-output-to-file + +; 中文文件名,中文内容 +(check + (dynamic-wind + #f ; before + (lambda () + (with-output-to-file test-filename + (lambda () (display "测试内容"))) + + (call-with-input-file test-filename + (lambda (port) + (read-line port)))) + clean-test-filename) ; after + => "测试内容") + +; 中文文件名,英文内容 +(check + (dynamic-wind + #f ; before + (lambda () + (with-output-to-file test-filename + (lambda () (display "ok"))) + + (call-with-input-file test-filename + (lambda (port) + (read-line port)))) + clean-test-filename) ; after + => "ok") + +; 中文文件名,多行中文内容 +(check + (dynamic-wind + #f ; before + (lambda () + (with-output-to-file test-filename + (lambda () + (display "第一行\n") + (display "第二行"))) + + (call-with-input-file test-filename + (lambda (port) + (list (read-line port) + (read-line port))))) + clean-test-filename) ; after + => '("第一行" "第二行")) + +; 测试文件是否确实被创建 +(for-each + (lambda (filename) + (dynamic-wind + #f ; before + (lambda () + ; 确保测试文件还不存在 + (check (file-exists? filename) => #f) + + ; 测试文件创建 + (with-output-to-file filename + (lambda () (display "test"))) + + ; 验证文件存在 + ; NOTE: 若写入文件名时编码不对应,file-exists? 会返回 #f + ; 如 `中文` 被直接写作文件名,由 Windows 解释为 GBK,会显示为 `涓枃` + (check-true (file-exists? filename))) + (clean-filename filename))) ; after + test-filenames) + +;; load + +(define test-content + '(begin + (define 测试变量 "你好,世界!") + (define (测试函数 x) (+ x 1)) + #t)) + +(dynamic-wind + (lambda () ; before + (with-output-to-file test-filename + (lambda () (display "(+ 21 21)")))) + (lambda () + (check (load test-filename) => 42)) + clean-test-filename) ; after + +; 测试文件是否确实被创建 +(for-each + (lambda (filename) + (dynamic-wind + #f ; before + (lambda () + ; 确保测试文件还不存在 + (check (file-exists? filename) => #f) + + ; 测试文件创建 + (with-output-to-file filename + (lambda () (display "(+ 21 21)"))) + + ; 验证能够正常 load + (check (load filename) => 42)) + (clean-filename filename))) ; after + test-filenames) + +(check-report) diff --git a/xmake.lua b/xmake.lua index d87fa55a7fde91056d18a56229425ad9c9f053f5..2af5da9415284d8dda80e232707180a9f21d135a 100644 --- a/xmake.lua +++ b/xmake.lua @@ -1,4 +1,4 @@ -set_version ("17.11.23") +set_version ("17.11.24") -- mode set_allowedmodes("releasedbg", "release", "debug", "profile") @@ -21,7 +21,7 @@ option_end() option("repl") set_description("Enable REPL (isocline) support") - set_default(false) + set_default(false) -- repl-anchor set_values(false, true) option_end() @@ -38,6 +38,16 @@ option("pin-deps") set_values(false, true) option_end() +option("http") + set_description("Enable http") + set_default(true) + set_values(false, true) +option_end() + +if has_config("http") then + add_requires("cpr") +end + local S7_VERSION = "20250922" if has_config("pin-deps") then add_requires("s7 "..S7_VERSION, {system=system}) @@ -77,7 +87,7 @@ end add_requires("argh v1.3.2") target ("goldfish") do - set_languages("c++11") + set_languages("c++17") set_targetdir("$(projectdir)/bin/") if is_plat("linux") then add_syslinks("stdc++") @@ -90,6 +100,7 @@ target ("goldfish") do add_packages("s7") add_packages("tbox") add_packages("argh") + add_packages("cpr") -- only enable REPL if repl option is enabled if has_config("repl") then @@ -100,26 +111,27 @@ target ("goldfish") do add_installfiles("$(projectdir)/goldfish/(scheme/*.scm)", {prefixdir = "share/goldfish"}) add_installfiles("$(projectdir)/goldfish/(srfi/*.scm)", {prefixdir = "share/goldfish"}) add_installfiles("$(projectdir)/goldfish/(liii/*.scm)", {prefixdir = "share/goldfish"}) + add_installfiles("$(projectdir)/goldfish/(guenchi/*.scm)", {prefixdir = "share/goldfish"}) end +if is_plat("wasm") then target("goldfish_repl_wasm") set_kind("binary") - set_languages("c++11") + set_languages("c++17") set_targetdir("$(projectdir)/repl/") add_files("src/goldfish_repl.cpp") - add_packages("s7", "tbox") - if is_plat("wasm") then - add_defines("GOLDFISH_ENABLE_REPL") - add_ldflags("--preload-file goldfish@/goldfish") - -- 导出 REPL 相关函数 - add_ldflags("-sEXPORTED_FUNCTIONS=['_eval_string','_get_out','_get_err','_malloc','_free']", {force = true}) - add_ldflags("-sEXPORTED_RUNTIME_METHODS=['UTF8ToString','allocateUTF8']", {force = true}) - add_ldflags("-sINITIAL_MEMORY=134217728", {force = true}) - add_ldflags("-sALLOW_MEMORY_GROWTH=1", {force = true}) - add_ldflags("-sASSERTIONS=1", {force = true}) - -- 生成 js glue code - set_extension(".js") - end + add_packages("s7", "tbox", "argh") + add_defines("GOLDFISH_ENABLE_REPL") + add_ldflags("--preload-file goldfish@/goldfish") + -- 导出 REPL 相关函数 + add_ldflags("-sEXPORTED_FUNCTIONS=['_eval_string','_get_out','_get_err','_malloc','_free']", {force = true}) + add_ldflags("-sEXPORTED_RUNTIME_METHODS=['UTF8ToString','allocateUTF8']", {force = true}) + add_ldflags("-sINITIAL_MEMORY=134217728", {force = true}) + add_ldflags("-sALLOW_MEMORY_GROWTH=1", {force = true}) + add_ldflags("-sASSERTIONS=1", {force = true}) + -- 生成 js glue code + set_extension(".js") +end includes("@builtin/xpack") @@ -135,6 +147,7 @@ xpack ("goldfish") add_sourcefiles("xmake.lua") add_sourcefiles("(src/**)") add_sourcefiles("(goldfish/**)") + add_sourcefiles("(3rdparty/**)") on_load(function (package) if package:with_source() then package:set("basename", "goldfish-$(plat)-src-v$(version)") diff --git a/xmake/packages/c/cpr/xmake.lua b/xmake/packages/c/cpr/xmake.lua new file mode 100644 index 0000000000000000000000000000000000000000..9ca1bddab51b9da3eb1833ecf2023c3b3fefb529 --- /dev/null +++ b/xmake/packages/c/cpr/xmake.lua @@ -0,0 +1,57 @@ +package("cpr") + + set_homepage("https://docs.libcpr.org/") + set_description("C++ Requests is a simple wrapper around libcurl inspired by the excellent Python Requests project.") + set_license("MIT") + + set_sourcedir(path.join(os.scriptdir(), "../../../../3rdparty/cpr")) + + add_configs("ssl", {description = "Enable SSL.", default = false, type = "boolean"}) + + add_deps("cmake") + if is_plat("mingw", "linux") then + add_syslinks("pthread") + end + add_links("cpr") + + on_load(function (package) + if package:config("ssl") then + package:add("deps", "libcurl", {configs = {libssh2 = true, zlib = true}}) + package:add("deps", "libssh2") + else + package:add("deps", "libcurl") + end + end) + + on_install("linux", "macosx", "windows", "mingw@windows", function (package) + local configs = {"-DCPR_BUILD_TESTS=OFF", + "-DCPR_FORCE_USE_SYSTEM_CURL=ON", + "-DCPR_USE_SYSTEM_CURL=ON"} + table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:debug() and "Debug" or "Release")) + table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF")) + table.insert(configs, "-DCPR_ENABLE_SSL=" .. (package:config("ssl") and "ON" or "OFF")) + local shflags + if package:config("shared") and package:is_plat("macosx") then + shflags = {"-framework", "CoreFoundation", "-framework", "Security", "-framework", "SystemConfiguration"} + end + local packagedeps = {"libcurl"} + if package:config("ssl") then + table.insert(packagedeps, "libssh2") + end + if package:is_plat("windows") then + -- fix find_package issue on windows + io.replace("CMakeLists.txt", "find_package%(CURL COMPONENTS .-%)", "find_package(CURL)") + end + import("package.tools.cmake").install(package, configs, {shflags = shflags, packagedeps = packagedeps}) + end) + + on_test(function (package) + assert(package:check_cxxsnippets({test = [[ + #include + #include + static void test() { + cpr::Response r = cpr::Get(cpr::Url{"https://xmake.io"}); + assert(r.status_code == 200); + } + ]]}, {configs = {languages = "c++17"}})) + end)