From c6e6d4216528a8a980cd58227a21a89be488b6b2 Mon Sep 17 00:00:00 2001 From: Sergey Kovalevich Date: Fri, 20 Jun 2025 13:20:55 +0300 Subject: [PATCH 1/5] update --- CMakeLists.txt | 4 +++ cmake/CMakeUtils.cmake | 72 +++++++++++++++++++++++++++++++++++++++++ code/CMakeLists.txt | 7 ++++ code/net/error.h | 44 +++++++++++++++++++++++++ code/net/error_test.cpp | 14 ++++++++ deps/CMakeLists.txt | 32 ++++++++++++++++++ 6 files changed, 173 insertions(+) create mode 100644 cmake/CMakeUtils.cmake create mode 100644 code/net/error.h create mode 100644 code/net/error_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a6cee7a..f742965 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,8 @@ set(VersionString "0.0.5") project(walng CXX) +include(cmake/CMakeUtils.cmake) + if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Debug") endif() @@ -12,5 +14,7 @@ add_subdirectory(deps EXCLUDE_FROM_ALL) set(TargetName walng) +enable_testing() + add_subdirectory(code) add_subdirectory(man) diff --git a/cmake/CMakeUtils.cmake b/cmake/CMakeUtils.cmake new file mode 100644 index 0000000..28c11ce --- /dev/null +++ b/cmake/CMakeUtils.cmake @@ -0,0 +1,72 @@ +include(CMakeParseArguments) + +function(CMakeUtilsRemoveMatchesFromList) + set(options) + set(oneValueArgs) + set(multiValueArgs MATCHES) + cmake_parse_arguments(TQ_PARSED "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach (TQ_LIST ${TQ_PARSED_UNPARSED_ARGUMENTS}) + foreach (TQ_ENTRY ${${TQ_LIST}}) + foreach (TQ_MATCH ${TQ_PARSED_MATCHES}) + if (${TQ_ENTRY} MATCHES ${TQ_MATCH}) + list(REMOVE_ITEM ${TQ_LIST} ${TQ_ENTRY}) + endif() + endforeach() + endforeach() + set(${TQ_LIST} ${${TQ_LIST}} PARENT_SCOPE) + endforeach() +endfunction() + +function(CMakeUtilsAddTestsFromSourceList) + set(options) + set(oneValueArgs PREFIX) + set(multiValueArgs LIBS OPTIONS DEFINITIONS) + + cmake_parse_arguments(TQ_PARSED "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach (TQ_LIST ${TQ_PARSED_UNPARSED_ARGUMENTS}) + foreach (TQ_ENTRY ${${TQ_LIST}}) + if (${TQ_ENTRY} MATCHES ".*_test.cpp") + string(REGEX REPLACE "^.*\/(.*)_test\.cpp$" "\\1" testName "${TQ_ENTRY}") + set(testName "${TQ_PARSED_PREFIX}-${testName}-test") + + add_executable(${testName} ${TQ_ENTRY}) + target_compile_options(${testName} PRIVATE ${TQ_PARSED_OPTIONS}) + target_compile_definitions(${testName} PRIVATE ${TQ_PARSED_DEFINITIONS}) + target_link_libraries(${testName} PRIVATE ${TQ_PARSED_LIBS}) + + add_test(${testName} ${testName}) + endif() + endforeach() + endforeach() +endfunction() + +function(CMakeUtilsAddBenchmarksFromSourceList) + set(options) + set(oneValueArgs PREFIX) + set(multiValueArgs LIBS COMPILE_OPTIONS LINK_OPTIONS DEFINITIONS) + + cmake_parse_arguments(TQ_PARSED "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach (TQ_LIST ${TQ_PARSED_UNPARSED_ARGUMENTS}) + foreach (TQ_ENTRY ${${TQ_LIST}}) + if (${TQ_ENTRY} MATCHES ".*_bm.cpp") + string(REGEX REPLACE "^.*\/(.*)_bm\.cpp$" "\\1" benchmarkName "${TQ_ENTRY}") + set(benchmarkName "${TQ_PARSED_PREFIX}-${benchmarkName}-bm") + + add_executable(${benchmarkName} ${TQ_ENTRY}) + target_compile_options(${benchmarkName} PRIVATE ${TQ_PARSED_COMPILE_OPTIONS}) + target_compile_definitions(${benchmarkName} PRIVATE ${TQ_PARSED_DEFINITIONS}) + target_link_options(${benchmarkName} PRIVATE ${TQ_PARSED_LINK_OPTIONS}) + target_link_libraries(${benchmarkName} PRIVATE ${TQ_PARSED_LIBS}) + endif() + endforeach() + endforeach() +endfunction() + +function(CMakeUtilsExcludeTestsAndBenchmarksFromSourceList TQ_LIST) + list(FILTER ${TQ_LIST} EXCLUDE REGEX ".*_test.cpp") + list(FILTER ${TQ_LIST} EXCLUDE REGEX ".*_bm.cpp") + set(${TQ_LIST} ${${TQ_LIST}} PARENT_SCOPE) +endfunction() diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index ca6a2b8..6493624 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -24,6 +24,13 @@ target_link_libraries(${TargetName} file(GLOB_RECURSE Sources "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") file(GLOB_RECURSE Headers "${CMAKE_CURRENT_SOURCE_DIR}/*.h") +CMakeUtilsAddTestsFromSourceList(Sources + PREFIX ${TargetName} + COMPILE_OPTIONS -Wall -Wextra -g + LIBS doctest::doctest_with_main) + +CMakeUtilsExcludeTestsAndBenchmarksFromSourceList(Sources) + target_sources(${TargetName} PUBLIC ${Headers} ${Sources}) file(COPY config.yaml DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/code/net/error.h b/code/net/error.h new file mode 100644 index 0000000..7a1130f --- /dev/null +++ b/code/net/error.h @@ -0,0 +1,44 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#pragma once + +#include + +#include + +namespace walng::net { + +enum class CurlError { EasyInit, UrlInit, EasySetOpt, EasyGetInfo, UrlGet, UrlSet }; + +struct CurlErrorCategory final : std::error_category { + char const* name() const noexcept override { + return "curl-error"; + } + std::string message(int ec) const override { + switch (static_cast(ec)) { + case CurlError::EasyInit: + return "failed to init easy handle"; + case CurlError::UrlInit: + return "failed to init url handle"; + case CurlError::EasySetOpt: + return "failed to set easy opt"; + case CurlError::EasyGetInfo: + return "failed to get easy info"; + case CurlError::UrlGet: + return "failed to get url part"; + case CurlError::UrlSet: + return "failed to set url part"; + default: + return "(unrecognized error)"; + } + } +}; + +CurlErrorCategory const theCurlErrorCategory{}; + +inline std::error_code make_error_code(CurlError e) noexcept { + return {static_cast(e), theCurlErrorCategory}; +} + +} // namespace walng::net diff --git a/code/net/error_test.cpp b/code/net/error_test.cpp new file mode 100644 index 0000000..74f18fc --- /dev/null +++ b/code/net/error_test.cpp @@ -0,0 +1,14 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#include + +#include "error.h" + +namespace walng::net { + +TEST_CASE("net-error: basic") { + CHECK(make_error_code(CurlError::UrlInit).message() == "failed to init url handle"); +} + +} // namespace walng::net diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index 6f3430b..462462e 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -1,28 +1,48 @@ include(FetchContent) +# ------------------------------------------------------------------------------------------------- +# cxxopts +# ------------------------------------------------------------------------------------------------- + FetchContent_Declare(cxxopts URL https://github.com/jarro2783/cxxopts/archive/refs/tags/v3.2.0.tar.gz DOWNLOAD_EXTRACT_TIMESTAMP ON ) FetchContent_MakeAvailable(cxxopts) +# ------------------------------------------------------------------------------------------------- +# yaml-cpp +# ------------------------------------------------------------------------------------------------- + FetchContent_Declare(yaml-cpp URL https://github.com/jbeder/yaml-cpp/archive/2f86d13775d119edbb69af52e5f566fd65c6953b.zip DOWNLOAD_EXTRACT_TIMESTAMP ON ) FetchContent_MakeAvailable(yaml-cpp) +# ------------------------------------------------------------------------------------------------- +# json +# ------------------------------------------------------------------------------------------------- + FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz DOWNLOAD_EXTRACT_TIMESTAMP ON ) FetchContent_MakeAvailable(json) +# ------------------------------------------------------------------------------------------------- +# inja +# ------------------------------------------------------------------------------------------------- + add_library(inja INTERFACE) target_include_directories(inja INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(inja INTERFACE nlohmann_json::nlohmann_json) add_library(3rdparty::inja ALIAS inja) +# ------------------------------------------------------------------------------------------------- +# curl +# ------------------------------------------------------------------------------------------------- + set(BUILD_SHARED_LIBS OFF) set(BUILD_STATIC_LIBS ON) set(BUILD_CURL_EXE OFF) @@ -46,3 +66,15 @@ FetchContent_Declare(curl DOWNLOAD_EXTRACT_TIMESTAMP ON ) FetchContent_MakeAvailable(curl) + +# ------------------------------------------------------------------------------------------------- +# doctest +# ------------------------------------------------------------------------------------------------- + +FetchContent_Declare(doctest + URL https://github.com/doctest/doctest/archive/refs/tags/v2.4.12.tar.gz + EXCLUDE_FROM_ALL + DOWNLOAD_EXTRACT_TIMESTAMP ON +) +FetchContent_MakeAvailable(doctest) + From 5eedbec213bdedfd0a9ab94eb720a40b3fe5ce10 Mon Sep 17 00:00:00 2001 From: Sergey Kovalevich Date: Fri, 20 Jun 2025 13:49:19 +0300 Subject: [PATCH 2/5] update --- cmake/CMakeUtils.cmake | 35 ++------- code/CMakeLists.txt | 11 +-- code/net/CurlEasyHandle.h | 117 +++++++++++++++++++++++++++++++ code/net/CurlEasyHandle_test.cpp | 26 +++++++ code/net/error.h | 56 ++++++++++----- code/net/error_test.cpp | 4 +- 6 files changed, 193 insertions(+), 56 deletions(-) create mode 100644 code/net/CurlEasyHandle.h create mode 100644 code/net/CurlEasyHandle_test.cpp diff --git a/cmake/CMakeUtils.cmake b/cmake/CMakeUtils.cmake index 28c11ce..29a3994 100644 --- a/cmake/CMakeUtils.cmake +++ b/cmake/CMakeUtils.cmake @@ -21,7 +21,7 @@ endfunction() function(CMakeUtilsAddTestsFromSourceList) set(options) set(oneValueArgs PREFIX) - set(multiValueArgs LIBS OPTIONS DEFINITIONS) + set(multiValueArgs LINK_LIBS COMPILE_OPTIONS COMPILE_DEFINITIONS COMPILE_FEATURES) cmake_parse_arguments(TQ_PARSED "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -32,9 +32,10 @@ function(CMakeUtilsAddTestsFromSourceList) set(testName "${TQ_PARSED_PREFIX}-${testName}-test") add_executable(${testName} ${TQ_ENTRY}) - target_compile_options(${testName} PRIVATE ${TQ_PARSED_OPTIONS}) - target_compile_definitions(${testName} PRIVATE ${TQ_PARSED_DEFINITIONS}) - target_link_libraries(${testName} PRIVATE ${TQ_PARSED_LIBS}) + target_compile_features(${testName} PRIVATE ${TQ_PARSED_COMPILE_FEATURES}) + target_compile_options(${testName} PRIVATE ${TQ_PARSED_COMPILE_OPTIONS}) + target_compile_definitions(${testName} PRIVATE ${TQ_PARSED_COMPILE_DEFINITIONS}) + target_link_libraries(${testName} PRIVATE ${TQ_PARSED_LINK_LIBS}) add_test(${testName} ${testName}) endif() @@ -42,31 +43,7 @@ function(CMakeUtilsAddTestsFromSourceList) endforeach() endfunction() -function(CMakeUtilsAddBenchmarksFromSourceList) - set(options) - set(oneValueArgs PREFIX) - set(multiValueArgs LIBS COMPILE_OPTIONS LINK_OPTIONS DEFINITIONS) - - cmake_parse_arguments(TQ_PARSED "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - foreach (TQ_LIST ${TQ_PARSED_UNPARSED_ARGUMENTS}) - foreach (TQ_ENTRY ${${TQ_LIST}}) - if (${TQ_ENTRY} MATCHES ".*_bm.cpp") - string(REGEX REPLACE "^.*\/(.*)_bm\.cpp$" "\\1" benchmarkName "${TQ_ENTRY}") - set(benchmarkName "${TQ_PARSED_PREFIX}-${benchmarkName}-bm") - - add_executable(${benchmarkName} ${TQ_ENTRY}) - target_compile_options(${benchmarkName} PRIVATE ${TQ_PARSED_COMPILE_OPTIONS}) - target_compile_definitions(${benchmarkName} PRIVATE ${TQ_PARSED_DEFINITIONS}) - target_link_options(${benchmarkName} PRIVATE ${TQ_PARSED_LINK_OPTIONS}) - target_link_libraries(${benchmarkName} PRIVATE ${TQ_PARSED_LIBS}) - endif() - endforeach() - endforeach() -endfunction() - -function(CMakeUtilsExcludeTestsAndBenchmarksFromSourceList TQ_LIST) +function(CMakeUtilsExcludeTestsFromSourceList TQ_LIST) list(FILTER ${TQ_LIST} EXCLUDE REGEX ".*_test.cpp") - list(FILTER ${TQ_LIST} EXCLUDE REGEX ".*_bm.cpp") set(${TQ_LIST} ${${TQ_LIST}} PARENT_SCOPE) endfunction() diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index 6493624..5ec6330 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -1,23 +1,18 @@ add_executable(${TargetName}) - target_compile_features(${TargetName} PRIVATE cxx_std_23) - set_target_properties(${TargetName} PROPERTIES CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) - target_compile_options(${TargetName} PRIVATE -Wall -Wextra -Wattributes -Wpedantic -Wstrict-aliasing -Wcast-align -g -fmacro-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}/= ) - target_compile_definitions(${TargetName} PRIVATE -DWALNG_VERSION="${VersionString}" ) - target_link_libraries(${TargetName} PRIVATE cxxopts::cxxopts yaml-cpp::yaml-cpp 3rdparty::inja CURL::libcurl_static) @@ -26,15 +21,15 @@ file(GLOB_RECURSE Headers "${CMAKE_CURRENT_SOURCE_DIR}/*.h") CMakeUtilsAddTestsFromSourceList(Sources PREFIX ${TargetName} + COMPILE_FEATURES cxx_std_23 COMPILE_OPTIONS -Wall -Wextra -g - LIBS doctest::doctest_with_main) + LINK_LIBS doctest::doctest_with_main CURL::libcurl_static) -CMakeUtilsExcludeTestsAndBenchmarksFromSourceList(Sources) +CMakeUtilsExcludeTestsFromSourceList(Sources) target_sources(${TargetName} PUBLIC ${Headers} ${Sources}) file(COPY config.yaml DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) include(GNUInstallDirs) - install(TARGETS ${TargetName} DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/code/net/CurlEasyHandle.h b/code/net/CurlEasyHandle.h new file mode 100644 index 0000000..a69663e --- /dev/null +++ b/code/net/CurlEasyHandle.h @@ -0,0 +1,117 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#pragma once + +#include +#include + +#include + +#include "error.h" + +namespace walng::net { + +class CurlEasyHandle { +private: + CURL* handle_ = nullptr; + +public: + CurlEasyHandle(CurlEasyHandle const&) = delete; + CurlEasyHandle& operator=(CurlEasyHandle const&) = delete; + + CurlEasyHandle() = default; + + CurlEasyHandle(CurlEasyHandle&& other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {} + + ~CurlEasyHandle() noexcept { + if (handle_) { + ::curl_easy_cleanup(handle_); + } + } + + CurlEasyHandle& operator=(CurlEasyHandle&& other) noexcept { + if (this != &other) { + this->~CurlEasyHandle(); + new (this) CurlEasyHandle(std::move(other)); + } + return *this; + } + + explicit CurlEasyHandle(CURL* handle) noexcept : handle_(handle) {} + + /// Create and init CurlEasyHandle + static std::expected create() noexcept { + auto handle = ::curl_easy_init(); + if (!handle) [[unlikely]] { + return std::unexpected(make_error_code(CurlInitError::EasyInit)); + } + return {CurlEasyHandle(handle)}; + } + + /// Return true on handle valid + [[nodiscard]] operator bool() const noexcept { + return handle_ != nullptr; + } + + /// Wrapper around @c curl_easy_reset + void reset() noexcept { + ::curl_easy_reset(handle_); + } + + /// Wrapper around @c curl_easy_setopt + template + std::expected option(CURLoption opt, T&& value) noexcept { + if constexpr (std::is_same_v, std::string>) { + return this->optionImpl(opt, value.c_str()); + } else if constexpr (std::is_same_v, std::string_view>) { + return this->optionImpl(opt, std::string(value).c_str()); + } else { + return this->optionImpl(opt, std::forward(value)); + } + } + + /// Wrapper around @c curl_easy_getinfo + template + std::expected info(CURLINFO info) const noexcept { + if constexpr (std::is_same_v) { + return this->infoImpl(info).transform([](char const* value) { + return std::string(value); + }); + } else if constexpr (std::is_same_v) { + return this->infoImpl(info).transform([](char const* value) { + return std::string_view(value); + }); + } else { + return this->infoImpl(info); + } + } + + /// Wrapper around @c curl_easy_perform + std::expected perform() noexcept { + if (auto const rc = ::curl_easy_perform(handle_); rc != CURLE_OK) [[unlikely]] { + return std::unexpected(make_error_code(rc)); + } + return {}; + } + +private: + template + inline std::expected optionImpl(CURLoption opt, T&& value) noexcept { + if (auto const rc = ::curl_easy_setopt(handle_, opt, std::forward(value)); rc != CURLE_OK) [[unlikely]] { + return std::unexpected(make_error_code(rc)); + } + return {}; + } + + template + inline std::expected infoImpl(CURLINFO info) const noexcept { + T result; + if (auto const rc = ::curl_easy_getinfo(handle_, info, &result); rc != CURLE_OK) [[unlikely]] { + return std::unexpected(make_error_code(rc)); + } + return {result}; + } +}; + +} // namespace walng::net diff --git a/code/net/CurlEasyHandle_test.cpp b/code/net/CurlEasyHandle_test.cpp new file mode 100644 index 0000000..4318ef8 --- /dev/null +++ b/code/net/CurlEasyHandle_test.cpp @@ -0,0 +1,26 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#include + +#include "CurlEasyHandle.h" + +namespace walng::net { + +TEST_CASE("CurlEasyHandle: move-semantic") { + CurlEasyHandle handle; + CHECK(!handle); + CHECK(!handle.option(CURLOPT_FOLLOWLOCATION, 1U)); + + auto result = CurlEasyHandle::create(); + CHECK(result); + CHECK(*result); + + handle = std::move(*result); + CHECK(handle); + CHECK(!result.value()); + + CHECK(handle.option(CURLOPT_FOLLOWLOCATION, 1U)); +} + +} // namespace walng::net diff --git a/code/net/error.h b/code/net/error.h index 7a1130f..f04e3fc 100644 --- a/code/net/error.h +++ b/code/net/error.h @@ -9,36 +9,58 @@ namespace walng::net { -enum class CurlError { EasyInit, UrlInit, EasySetOpt, EasyGetInfo, UrlGet, UrlSet }; +enum class CurlInitError { EasyInit, UrlInit }; -struct CurlErrorCategory final : std::error_category { +struct CurlInitErrorCategory final : std::error_category { char const* name() const noexcept override { - return "curl-error"; + return "curl-init-error"; } std::string message(int ec) const override { - switch (static_cast(ec)) { - case CurlError::EasyInit: + switch (static_cast(ec)) { + case CurlInitError::EasyInit: return "failed to init easy handle"; - case CurlError::UrlInit: + case CurlInitError::UrlInit: return "failed to init url handle"; - case CurlError::EasySetOpt: - return "failed to set easy opt"; - case CurlError::EasyGetInfo: - return "failed to get easy info"; - case CurlError::UrlGet: - return "failed to get url part"; - case CurlError::UrlSet: - return "failed to set url part"; default: return "(unrecognized error)"; } } }; -CurlErrorCategory const theCurlErrorCategory{}; +CurlInitErrorCategory const theCurlInitErrorCategory{}; -inline std::error_code make_error_code(CurlError e) noexcept { - return {static_cast(e), theCurlErrorCategory}; +inline std::error_code make_error_code(CurlInitError e) noexcept { + return {static_cast(e), theCurlInitErrorCategory}; +} + +struct CurlEasyErrorCategory final : std::error_category { + char const* name() const noexcept override { + return "curl-easy-error"; + } + std::string message(int ec) const override { + return ::curl_easy_strerror(static_cast<::CURLcode>(ec)); + } +}; + +CurlEasyErrorCategory const theCurlEasyErrorCategory{}; + +inline std::error_code make_error_code(::CURLcode ec) { + return std::error_code(static_cast(ec), theCurlEasyErrorCategory); +} + +struct CurlUrlErrorCategory final : std::error_category { + char const* name() const noexcept override { + return "curl-url-error"; + } + std::string message(int ec) const override { + return ::curl_url_strerror(static_cast<::CURLUcode>(ec)); + } +}; + +CurlUrlErrorCategory const theCurlUrlErrorCategory{}; + +inline std::error_code make_error_code(::CURLUcode ec) { + return std::error_code(static_cast(ec), theCurlUrlErrorCategory); } } // namespace walng::net diff --git a/code/net/error_test.cpp b/code/net/error_test.cpp index 74f18fc..92ed1b8 100644 --- a/code/net/error_test.cpp +++ b/code/net/error_test.cpp @@ -7,8 +7,8 @@ namespace walng::net { -TEST_CASE("net-error: basic") { - CHECK(make_error_code(CurlError::UrlInit).message() == "failed to init url handle"); +TEST_CASE("net-error: CurlInit") { + CHECK(make_error_code(CurlInitError::UrlInit).message() == "failed to init url handle"); } } // namespace walng::net From b4d88422c80cf5fceda49b2e1a3239236b6be9e9 Mon Sep 17 00:00:00 2001 From: Sergey Kovalevich Date: Fri, 20 Jun 2025 15:45:42 +0300 Subject: [PATCH 3/5] update --- code/net/CurlEasyHandle.h | 10 ++-- code/net/CurlEasyHandle_test.cpp | 4 +- code/net/CurlUrlHandle.h | 90 ++++++++++++++++++++++++++++++++ code/net/CurlUrlHandle_test.cpp | 63 ++++++++++++++++++++++ 4 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 code/net/CurlUrlHandle.h create mode 100644 code/net/CurlUrlHandle_test.cpp diff --git a/code/net/CurlEasyHandle.h b/code/net/CurlEasyHandle.h index a69663e..115a2e0 100644 --- a/code/net/CurlEasyHandle.h +++ b/code/net/CurlEasyHandle.h @@ -61,13 +61,13 @@ class CurlEasyHandle { /// Wrapper around @c curl_easy_setopt template - std::expected option(CURLoption opt, T&& value) noexcept { + std::expected setOption(CURLoption opt, T&& value) noexcept { if constexpr (std::is_same_v, std::string>) { - return this->optionImpl(opt, value.c_str()); + return this->setOptionImpl(opt, value.c_str()); } else if constexpr (std::is_same_v, std::string_view>) { - return this->optionImpl(opt, std::string(value).c_str()); + return this->setOptionImpl(opt, std::string(value).c_str()); } else { - return this->optionImpl(opt, std::forward(value)); + return this->setOptionImpl(opt, std::forward(value)); } } @@ -97,7 +97,7 @@ class CurlEasyHandle { private: template - inline std::expected optionImpl(CURLoption opt, T&& value) noexcept { + inline std::expected setOptionImpl(CURLoption opt, T&& value) noexcept { if (auto const rc = ::curl_easy_setopt(handle_, opt, std::forward(value)); rc != CURLE_OK) [[unlikely]] { return std::unexpected(make_error_code(rc)); } diff --git a/code/net/CurlEasyHandle_test.cpp b/code/net/CurlEasyHandle_test.cpp index 4318ef8..5825739 100644 --- a/code/net/CurlEasyHandle_test.cpp +++ b/code/net/CurlEasyHandle_test.cpp @@ -10,7 +10,7 @@ namespace walng::net { TEST_CASE("CurlEasyHandle: move-semantic") { CurlEasyHandle handle; CHECK(!handle); - CHECK(!handle.option(CURLOPT_FOLLOWLOCATION, 1U)); + CHECK(!handle.setOption(CURLOPT_FOLLOWLOCATION, 1U)); auto result = CurlEasyHandle::create(); CHECK(result); @@ -20,7 +20,7 @@ TEST_CASE("CurlEasyHandle: move-semantic") { CHECK(handle); CHECK(!result.value()); - CHECK(handle.option(CURLOPT_FOLLOWLOCATION, 1U)); + CHECK(handle.setOption(CURLOPT_FOLLOWLOCATION, 1U)); } } // namespace walng::net diff --git a/code/net/CurlUrlHandle.h b/code/net/CurlUrlHandle.h new file mode 100644 index 0000000..369f910 --- /dev/null +++ b/code/net/CurlUrlHandle.h @@ -0,0 +1,90 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#pragma once + +#include +#include +#include + +#include + +#include "error.h" + +namespace walng::net { + +class CurlUrlHandle { +private: + CURLU* handle_ = nullptr; + +public: + CurlUrlHandle(CurlUrlHandle const&) = delete; + CurlUrlHandle& operator=(CurlUrlHandle const&) = delete; + + explicit CurlUrlHandle(CURLU* handle) noexcept : handle_(handle) {} + + CurlUrlHandle() = default; + + ~CurlUrlHandle() noexcept { + if (handle_) { + ::curl_url_cleanup(handle_); + } + } + + CurlUrlHandle(CurlUrlHandle&& other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {} + + CurlUrlHandle& operator=(CurlUrlHandle&& other) noexcept { + if (this != &other) { + this->~CurlUrlHandle(); + new (this) CurlUrlHandle(std::move(other)); + } + return *this; + } + + /// Create and init CurlUrlHandle + static std::expected create() noexcept { + auto handle = ::curl_url(); + if (!handle) [[unlikely]] { + return std::unexpected(make_error_code(CurlInitError::UrlInit)); + } + return {CurlUrlHandle(handle)}; + } + + /// Return true on handle valid + [[nodiscard]] operator bool() const noexcept { + return handle_ != nullptr; + } + + /// Wrapper around @c curl_url_get + std::expected part(CURLUPart part) const { + char* value = nullptr; + if (auto const rc = ::curl_url_get(handle_, part, &value, 0); rc != CURLUE_OK) [[unlikely]] { + return std::unexpected(make_error_code(rc)); + } + std::string result(value); + ::curl_free(value); + return {result}; + } + + /// Wrapper around @c curl_url_set + template + std::expected setPart(CURLUPart part, T&& value) { + if constexpr (std::is_same_v, std::string>) { + return this->setPartImpl(part, value.c_str()); + } else if constexpr (std::is_same_v, std::string_view>) { + return this->setPartImpl(part, std::string(value).c_str()); + } else { + return this->setPartImpl(part, value); + } + } + +private: + inline std::expected setPartImpl(CURLUPart part, char const* value) noexcept { + if (auto const rc = ::curl_url_set(handle_, part, value, 0); rc != CURLUE_OK) [[unlikely]] { + return std::unexpected(make_error_code(rc)); + } + return {}; + } +}; + +} // namespace walng::net diff --git a/code/net/CurlUrlHandle_test.cpp b/code/net/CurlUrlHandle_test.cpp new file mode 100644 index 0000000..99cb45d --- /dev/null +++ b/code/net/CurlUrlHandle_test.cpp @@ -0,0 +1,63 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#include + +#include "CurlUrlHandle.h" + +namespace walng::net { + +TEST_CASE("CurlUrlHandle: move-semantic") { + CurlUrlHandle handle; + CHECK(!handle); + + auto result = CurlUrlHandle::create(); + CHECK(result); + CHECK(*result); + + handle = std::move(*result); + CHECK(handle); + CHECK(!result.value()); +} + +TEST_CASE("CurlUrlHandle: ") { + auto result = CurlUrlHandle::create().and_then([](auto&& result) -> std::expected { + if (auto rc = result.setPart(CURLUPART_URL, "https://github.com/ksergey/walng/README.md"); !rc) { + return std::unexpected(rc.error()); + } + return result; + }); + + CHECK(result); + CurlUrlHandle handle = std::move(*result); + + if (auto rc = handle.part(CURLUPART_URL); rc) { + CHECK(rc.value() == "https://github.com/ksergey/walng/README.md"); + } else { + CHECK(false); + } + + if (auto rc = handle.part(CURLUPART_PATH); rc) { + CHECK(rc.value() == "/ksergey/walng/README.md"); + } else { + CHECK(false); + } + + if (auto rc = handle.setPart(CURLUPART_PATH, "/xyz"); !rc) { + CHECK(false); + } + + if (auto rc = handle.part(CURLUPART_PATH); rc) { + CHECK(rc.value() == "/xyz"); + } else { + CHECK(false); + } + + if (auto rc = handle.part(CURLUPART_URL); rc) { + CHECK(rc.value() == "https://github.com/xyz"); + } else { + CHECK(false); + } +} + +} // namespace walng::net From 1820ad1e16bfb0e8c767c4fe7783adcfb135b501 Mon Sep 17 00:00:00 2001 From: Sergey Kovalevich Date: Fri, 20 Jun 2025 21:45:10 +0300 Subject: [PATCH 4/5] update --- code/net/CurlEasyHandle.h | 4 ++-- code/net/CurlUrlHandle.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/code/net/CurlEasyHandle.h b/code/net/CurlEasyHandle.h index 115a2e0..c804ab9 100644 --- a/code/net/CurlEasyHandle.h +++ b/code/net/CurlEasyHandle.h @@ -97,7 +97,7 @@ class CurlEasyHandle { private: template - inline std::expected setOptionImpl(CURLoption opt, T&& value) noexcept { + std::expected setOptionImpl(CURLoption opt, T&& value) noexcept { if (auto const rc = ::curl_easy_setopt(handle_, opt, std::forward(value)); rc != CURLE_OK) [[unlikely]] { return std::unexpected(make_error_code(rc)); } @@ -105,7 +105,7 @@ class CurlEasyHandle { } template - inline std::expected infoImpl(CURLINFO info) const noexcept { + std::expected infoImpl(CURLINFO info) const noexcept { T result; if (auto const rc = ::curl_easy_getinfo(handle_, info, &result); rc != CURLE_OK) [[unlikely]] { return std::unexpected(make_error_code(rc)); diff --git a/code/net/CurlUrlHandle.h b/code/net/CurlUrlHandle.h index 369f910..b2191c3 100644 --- a/code/net/CurlUrlHandle.h +++ b/code/net/CurlUrlHandle.h @@ -79,7 +79,7 @@ class CurlUrlHandle { } private: - inline std::expected setPartImpl(CURLUPart part, char const* value) noexcept { + std::expected setPartImpl(CURLUPart part, char const* value) noexcept { if (auto const rc = ::curl_url_set(handle_, part, value, 0); rc != CURLUE_OK) [[unlikely]] { return std::unexpected(make_error_code(rc)); } From b6dabcf53e7bf9977f9b5120f9b478fe90183a25 Mon Sep 17 00:00:00 2001 From: Sergey Kovalevich Date: Mon, 23 Jun 2025 17:15:11 +0300 Subject: [PATCH 5/5] update --- README.md | 4 +- code/main.cpp | 66 ++++++++++++-------------- code/net/CurlEasyHandle.h | 4 +- code/net/download.cpp | 98 +++++++++++++++++++++++++++++++++++++++ code/net/download.h | 31 +++++++++++++ code/utils.cpp | 16 +++---- code/utils.h | 10 ++-- 7 files changed, 175 insertions(+), 54 deletions(-) create mode 100644 code/net/download.cpp create mode 100644 code/net/download.h diff --git a/README.md b/README.md index bdd3086..0784cf7 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ Download base16 (or base24) scheme you like from https://github.com/tinted-themi run walng: ```sh -curl -O -L https://raw.githubusercontent.com/tinted-theming/schemes/refs/heads/spec-0.11/base16/terracotta.yaml && ./walng --theme-file terracotta.yaml +curl -O -L https://raw.githubusercontent.com/tinted-theming/schemes/refs/heads/spec-0.11/base16/terracotta.yaml && ./walng --theme terracotta.yaml ``` or ```sh -walng --theme-url https://raw.githubusercontent.com/tinted-theming/schemes/refs/heads/spec-0.11/base16/terracotta.yaml +walng --theme https://raw.githubusercontent.com/tinted-theming/schemes/refs/heads/spec-0.11/base16/terracotta.yaml ``` diff --git a/code/main.cpp b/code/main.cpp index cd705d8..4574051 100644 --- a/code/main.cpp +++ b/code/main.cpp @@ -10,6 +10,7 @@ #include "Request.h" #include "generate.h" +#include "net/download.h" #include "utils.h" #include "version.h" @@ -25,10 +26,10 @@ std::expected extractFileNameFromUrl(s }); } -std::expected writeFile(std::filesystem::path const& path, std::string_view content) noexcept { +std::expected writeFile(std::filesystem::path const& path, std::string_view content) noexcept { FILE* file = ::fopen(path.c_str(), "w"); if (!file) { - return std::unexpected(std::system_error(errno, std::system_category(), "failed to open file for writting")); + return std::unexpected(std::error_code(errno, std::system_category())); } // TODO: check result @@ -38,41 +39,35 @@ std::expected writeFile(std::filesystem::path const& pa return {}; } -std::expected downloadFileOrGetFromCache(std::string const& url) noexcept { - using namespace walng; - - auto filename = extractFileNameFromUrl(url); - if (!filename) { - return std::unexpected(filename.error()); - } - - auto request = Request(); - if (auto const rc = request.prepare(url); !rc) { - return std::unexpected(rc.error()); - } - - auto response = request.perform(); +/// Download file. Return error or path where content stored +std::expected downloadFile(std::string const& url) noexcept { + auto response = walng::net::download(url); if (!response) { return std::unexpected(response.error()); } - auto cachePath = getCachePath(); + if (response->code != 200) { + return std::unexpected(std::error_code(ENOENT, std::system_category())); + } + + auto cachePath = walng::getCachePath(); if (!cachePath.has_value()) { return std::unexpected(cachePath.error()); } + auto const themesPath = cachePath.value() / "themes"; std::error_code ec; create_directories(themesPath, ec); if (ec) { - return std::unexpected(std::system_error(ec, "can't create themes cache dir")); + return std::unexpected(ec); } - auto const themeFilePath = themesPath / *filename; - if (auto const rc = writeFile(themeFilePath, response->body()); !rc) { + auto themeFilePath = themesPath / response->filename; + if (auto const rc = writeFile(themeFilePath, response->content); !rc) { return std::unexpected(response.error()); } - return themeFilePath; + return {std::move(themeFilePath)}; } int main(int argc, char* argv[]) { @@ -82,8 +77,7 @@ int main(int argc, char* argv[]) { // clang-format off options.add_options() ("config", "path to config file", cxxopts::value(), "PATH") - ("theme-file", "path to theme file", cxxopts::value(), "PATH") - ("theme-url", "url to theme file", cxxopts::value(), "URL") + ("theme", "path or url to theme file", cxxopts::value(), "PATH or URL") ("help", "prints the help and exit") ("version", "prints the version and exit") ; @@ -107,26 +101,24 @@ int main(int argc, char* argv[]) { } auto configPath = walng::getConfigPath(); if (!configPath.has_value()) { - throw configPath.error(); + throw std::system_error(configPath.error()); } return configPath.value() / "config.yaml"; }(); - if (result.count("theme-file") > 0) { - if (result.count("theme-url") > 0) { - throw std::runtime_error("argument `--theme-file` and `--theme-url` can't be set simultaneously"); - } - std::filesystem::path const themePath = result["theme-file"].as(); - walng::generate(configPath, themePath); - } else if (result.count("theme-url") > 0) { - std::string const& themeUrl = result["theme-url"].as(); - auto const downloadResult = downloadFileOrGetFromCache(themeUrl); - if (!downloadResult) { - throw downloadResult.error(); + if (result.count("theme") == 0) { + throw std::runtime_error("argument `--theme` should be set"); + } + auto const& theme = result["theme"].as(); + + if (theme.starts_with("http://") || theme.starts_with("https://")) { + if (auto rc = downloadFile(theme); rc) { + walng::generate(configPath, *rc); + } else { + throw std::system_error(rc.error()); } - walng::generate(configPath, downloadResult.value()); } else { - throw std::runtime_error("argument `--theme-file` or `--theme-url` should be set"); + walng::generate(configPath, theme); } } catch (std::exception const& e) { diff --git a/code/net/CurlEasyHandle.h b/code/net/CurlEasyHandle.h index c804ab9..f385a6f 100644 --- a/code/net/CurlEasyHandle.h +++ b/code/net/CurlEasyHandle.h @@ -73,7 +73,7 @@ class CurlEasyHandle { /// Wrapper around @c curl_easy_getinfo template - std::expected info(CURLINFO info) const noexcept { + std::expected info(CURLINFO info) const { if constexpr (std::is_same_v) { return this->infoImpl(info).transform([](char const* value) { return std::string(value); @@ -105,7 +105,7 @@ class CurlEasyHandle { } template - std::expected infoImpl(CURLINFO info) const noexcept { + std::expected infoImpl(CURLINFO info) const { T result; if (auto const rc = ::curl_easy_getinfo(handle_, info, &result); rc != CURLE_OK) [[unlikely]] { return std::unexpected(make_error_code(rc)); diff --git a/code/net/download.cpp b/code/net/download.cpp new file mode 100644 index 0000000..d9cd35d --- /dev/null +++ b/code/net/download.cpp @@ -0,0 +1,98 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#include "download.h" + +#include + +#include "CurlEasyHandle.h" +#include "CurlUrlHandle.h" + +namespace walng::net { +namespace { + +size_t curlWriteFn(char const* data, size_t size, size_t nmemb, void* userdata) { + auto const response = static_cast(userdata); + auto const chunkSize = size * nmemb; + response->content.append(data, chunkSize); + return chunkSize; +} + +std::expected extractFilename(char const* url) { + CurlUrlHandle handle; + if (auto rc = CurlUrlHandle::create(); rc) { + handle = std::move(*rc); + } else { + return std::unexpected(rc.error()); + } + + if (auto rc = handle.setPart(CURLUPART_URL, url); !rc) { + return std::unexpected(rc.error()); + } + + std::filesystem::path path; + if (auto rc = handle.part(CURLUPART_PATH); rc) { + path = std::move(*rc); + } else { + return std::unexpected(rc.error()); + } + + return {path.filename()}; +} + +} // namespace + +auto download(char const* url, std::optional timeout) + -> std::expected { + CurlEasyHandle handle; + if (auto rc = CurlEasyHandle::create(); rc) { + handle = std::move(*rc); + } else { + return std::unexpected(rc.error()); + } + + Response response; + + if (auto rc = handle.setOption(CURLOPT_WRITEDATA, &response); !rc) { + return std::unexpected(rc.error()); + } + if (auto rc = handle.setOption(CURLOPT_WRITEFUNCTION, &curlWriteFn); !rc) { + return std::unexpected(rc.error()); + } + if (auto rc = handle.setOption(CURLOPT_FOLLOWLOCATION, 1L); !rc) { + return std::unexpected(rc.error()); + } + if (auto rc = handle.setOption(CURLOPT_URL, url); !rc) { + return std::unexpected(rc.error()); + } + if (timeout) { + if (auto rc = handle.setOption(CURLOPT_TIMEOUT_MS, static_cast(timeout->count())); !rc) { + return std::unexpected(rc.error()); + } + } + + if (auto rc = handle.perform(); !rc) { + return std::unexpected(rc.error()); + } + + if (auto rc = handle.info(CURLINFO_RESPONSE_CODE); rc) { + response.code = *rc; + } else { + std::print(stderr, "CURLINFO_RESPONSE_CODE falied: {}\n", rc.error().message()); + } + + if (auto rc = handle.info(CURLINFO_EFFECTIVE_URL); rc) { + char const* effectiveUrl = *rc; + if (auto rc = extractFilename(effectiveUrl); rc) { + response.filename = *rc; + } else { + std::print(stderr, "can't extract downloaded file filename: {}\n", rc.error().message()); + } + } else { + std::print(stderr, "CURLINFO_EFFECTIVE_URL falied: {}\n", rc.error().message()); + } + + return {std::move(response)}; +} + +} // namespace walng::net diff --git a/code/net/download.h b/code/net/download.h new file mode 100644 index 0000000..9041758 --- /dev/null +++ b/code/net/download.h @@ -0,0 +1,31 @@ +// Copyright (c) Sergey Kovalevich +// SPDX-License-Identifier: AGPL-3.0 + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace walng::net { + +struct Response { + long code = 0; + std::string content; + std::filesystem::path filename; +}; + +/// Download file at url +auto download(char const* url, std::optional timeout = std::nullopt) + -> std::expected; + +/// \overload +inline auto download(std::string const& url, std::optional timeout = std::nullopt) + -> std::expected { + return download(url.c_str(), timeout); +} + +} // namespace walng::net diff --git a/code/utils.cpp b/code/utils.cpp index a4e138c..5865b69 100644 --- a/code/utils.cpp +++ b/code/utils.cpp @@ -7,14 +7,14 @@ namespace walng { -std::expected getHomePath() { +std::expected getHomePath() { if (auto const result = ::secure_getenv("HOME"); result) { - return std::filesystem::path(result); + return {std::filesystem::path(result)}; } - return std::unexpected(std::system_error(ENOENT, std::system_category(), "HOME env variable not set")); + return std::unexpected(std::error_code(ENOENT, std::system_category())); } -std::expected getConfigPath() { +std::expected getConfigPath() { if (auto const result = ::secure_getenv("XDG_CONFIG_HOME"); result) { return std::filesystem::path(result) / "walng"; } @@ -23,7 +23,7 @@ std::expected getConfigPath() { }); } -std::expected getCachePath() { +std::expected getCachePath() { if (auto const result = ::secure_getenv("XDG_CACHE_HOME"); result) { return std::filesystem::path(result) / "walng"; } @@ -32,7 +32,7 @@ std::expected getCachePath() { }); } -std::expected makeTempFilePath() { +std::expected makeTempFilePath() { static constexpr std::string_view kAllowedChars = "abcdefghijklmnaoqrstuvwxyz1234567890"; std::random_device randomDevice; @@ -47,12 +47,12 @@ std::expected makeTempFilePath() { std::error_code ec; auto tempDirPath = std::filesystem::temp_directory_path(ec); if (ec) { - return std::unexpected(std::system_error(ec, "can't obtain temp directory path")); + return std::unexpected(ec); } return tempDirPath / std::string_view(tmpName.data(), tmpName.size()); } -std::expected expandTilda(std::filesystem::path& path) { +std::expected expandTilda(std::filesystem::path& path) { if (auto const& str = path.native(); str.starts_with("~/")) { auto homePath = getHomePath(); if (!homePath.has_value()) { diff --git a/code/utils.h b/code/utils.h index d993104..2bb90b8 100644 --- a/code/utils.h +++ b/code/utils.h @@ -10,26 +10,26 @@ namespace walng { /// Get path to $HOME directory -std::expected getHomePath(); +std::expected getHomePath(); /// Get path to \c walng config directory /// /// One of: /// - $XDG_CONFIG_HOME/walng /// - $HOME/.config/walng -std::expected getConfigPath(); +std::expected getConfigPath(); /// Get path to \c walng cache directory /// /// One of: /// - $XDG_CACHE_HOME/walng /// - $HOME/.cache/walng -std::expected getCachePath(); +std::expected getCachePath(); /// Make temporary file path -std::expected makeTempFilePath(); +std::expected makeTempFilePath(); /// Expand @c ~ to $HOME -std::expected expandTilda(std::filesystem::path& path); +std::expected expandTilda(std::filesystem::path& path); } // namespace walng