diff --git a/.editorconfig b/.editorconfig index 9277f564..85b86c30 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true -[*.{cpp,h,hpp,py}] +[*.{cpp,cppm,h,hpp,py}] indent_size = 4 indent_style = space diff --git a/.gitattributes b/.gitattributes index 03c7c73e..c1d7c77d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -22,5 +22,6 @@ Doxyfile text eol=lf *.c text diff=c *.cc text diff=cpp *.cpp text diff=cpp +*.cppm text diff=cpp *.h text diff=cpp *.hpp text diff=cpp diff --git a/.github/workflows/test-ubuntu.yml b/.github/workflows/test-ubuntu.yml index 2073d76b..938cc3dc 100644 --- a/.github/workflows/test-ubuntu.yml +++ b/.github/workflows/test-ubuntu.yml @@ -14,6 +14,13 @@ jobs: strategy: matrix: include: + - name: clang++-16 C++20 module + os: ubuntu-24.04 + cxx_compiler: clang++-16 + cxx_standard: 20 + cmake_options: "-G Ninja -DUPA_MODULE=ON -DCMAKE_CXX_FLAGS=-stdlib=libc++" + install: "libc++-16-dev libc++abi-16-dev" + - name: clang++ C++20 shared-lib os: ubuntu-latest cxx_compiler: clang++ @@ -137,6 +144,12 @@ jobs: cmake_options: "-DUPA_BUILD_EXAMPLES=ON -DBUILD_SHARED_LIBS=ON" install_cmake: true + - name: g++-15 C++20 module + container: gcc:15 + cxx_standard: 20 + cmake_options: "-G Ninja -DUPA_MODULE=ON" + install_cmake: true + steps: - name: install if: ${{ matrix.install_script }} diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6a6dc7ce..456f4ad7 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -56,6 +56,12 @@ jobs: cxx_standard: 20 cmake_options: "-T ClangCL" + - name: VS 2026 C++20 module + os: windows-2025-vs2026 + generator: "Visual Studio 18 2026" + cxx_standard: 20 + cmake_options: "-DUPA_MODULE=ON" + steps: - uses: actions/checkout@v6 - name: get dependencies diff --git a/CMakeLists.txt b/CMakeLists.txt index c927c035..fd951eec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ # add_link_options() requires 3.13 -cmake_minimum_required(VERSION 3.13) +cmake_minimum_required(VERSION 3.13...3.28.2) # https://cmake.org/cmake/help/latest/policy/CMP0074.html if(POLICY CMP0074) @@ -31,11 +31,6 @@ set(upa_export_name url) # to create the library filename set(upa_lib_target upa_url) -if (NOT CMAKE_CXX_STANDARD) - set(CMAKE_CXX_STANDARD 17) - set(CMAKE_CXX_STANDARD_REQUIRED ON) -endif() - # Is the Upa URL a top-level project? if (NOT DEFINED UPA_MAIN_PROJECT) set(UPA_MAIN_PROJECT OFF) @@ -53,13 +48,21 @@ option(UPA_BUILD_EXTRACTED "Build Upa URL examples extracted from the docs." OFF option(UPA_INSTALL "Generate the install target." ON) # library options option(UPA_AMALGAMATED "Use amalgamated URL library source." OFF) +option(UPA_MODULE "Build a C++ module library." OFF) # tests build options +option(UPA_MODULE_TESTS "Build C++ module tests." ${UPA_BUILD_TESTS}) option(UPA_TEST_URL_FOR_QT "Build tests with Qt strings" OFF) option(UPA_TEST_COVERAGE "Build tests with code coverage reporting" OFF) option(UPA_TEST_COVERAGE_CLANG "Build tests with Clang source-based code coverage" OFF) option(UPA_TEST_SANITIZER "Build tests with Clang sanitizer" OFF) option(UPA_TEST_VALGRIND "Run tests with Valgrind" OFF) +# C++ standard +if (NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) + set(CMAKE_CXX_STANDARD_REQUIRED ON) +endif() + # AFL, Honggfuzz, or Clang libFuzzer if (UPA_BUILD_FUZZER) set(UPA_BUILD_TESTS OFF) @@ -117,6 +120,28 @@ endif() include_directories(deps) +# The common setup for library targets +function (configure_library_target lib_target export_name) + target_include_directories(${lib_target} PUBLIC + $ + $) + + if (BUILD_SHARED_LIBS) + target_compile_definitions(${lib_target} PRIVATE UPA_LIB_EXPORT + INTERFACE UPA_LIB_IMPORT) + set_target_properties(${lib_target} PROPERTIES + VERSION ${UPA_URL_VERSION} + SOVERSION ${UPA_URL_SOVERSION} + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON) + endif() + + # Alias target for the library + add_library(upa::${export_name} ALIAS ${lib_target}) + set_target_properties(${lib_target} PROPERTIES + EXPORT_NAME ${export_name}) +endfunction() + # Is the Upa URL library needed? if (UPA_BUILD_TESTS OR UPA_BUILD_BENCH OR UPA_BUILD_FUZZER OR UPA_BUILD_EXAMPLES OR UPA_BUILD_EXTRACTED OR UPA_INSTALL) @@ -137,28 +162,30 @@ if (UPA_BUILD_TESTS OR UPA_BUILD_BENCH OR UPA_BUILD_FUZZER OR UPA_BUILD_EXAMPLES src/url_search_params.cpp src/url_utf.cpp src/urlpattern.cpp) - target_include_directories(${upa_lib_target} PUBLIC - $ - $) + configure_library_target(${upa_lib_target} ${upa_export_name}) endif() - if (BUILD_SHARED_LIBS) - target_compile_definitions(${upa_lib_target} PRIVATE UPA_LIB_EXPORT - INTERFACE UPA_LIB_IMPORT) - set_target_properties(${upa_lib_target} PROPERTIES - VERSION ${UPA_URL_VERSION} - SOVERSION ${UPA_URL_SOVERSION} - CXX_VISIBILITY_PRESET hidden - VISIBILITY_INLINES_HIDDEN ON) - endif () - # Alias target for the library - add_library(upa::${upa_export_name} ALIAS ${upa_lib_target}) - set_target_properties(${upa_lib_target} PROPERTIES - EXPORT_NAME ${upa_export_name}) # GCC 8 requires linking with stdc++fs to use std::filesystem target_link_libraries(${upa_lib_target} PRIVATE $<$,$,9.0>>:stdc++fs>) endif() +if (UPA_MODULE) + set(CMAKE_CXX_EXTENSIONS OFF) + + add_library(upa_url_module) + target_sources(upa_url_module + PRIVATE + src/module/upa.url.cpp + PUBLIC FILE_SET CXX_MODULES + BASE_DIRS + src/module + FILES + src/module/upa.url.cppm) + target_compile_features(upa_url_module + PUBLIC cxx_std_20) + configure_library_target(upa_url_module ${upa_export_name}-module) +endif() + # Test targets if (UPA_BUILD_TESTS) @@ -216,6 +243,24 @@ if (UPA_BUILD_TESTS) endforeach() endif() +if (UPA_MODULE AND UPA_MODULE_TESTS) + enable_testing() + + set(test_files + test/test-module.cpp + ) + foreach(file ${test_files}) + get_filename_component(test_name ${file} NAME_WE) + + add_executable(${test_name} ${file}) + target_link_libraries(${test_name} upa_url_module) + + add_test(NAME ${test_name} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/test + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/${test_name}) + endforeach() +endif() + # Benchmark targets if (UPA_BUILD_BENCH) @@ -267,11 +312,25 @@ if (UPA_INSTALL AND NOT UPA_AMALGAMATED) DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) + set(upa_install_targets ${upa_lib_target}) + if (UPA_MODULE) + list(APPEND upa_install_targets upa_url_module) + set(upa_install_file_set + FILE_SET CXX_MODULES + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${upa_lib_name}/src) + set(upa_install_modules_dir + CXX_MODULES_DIRECTORY .) + install( + FILES src/module/upa.url.h + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${upa_lib_name}/src) + endif() + install( - TARGETS ${upa_lib_target} + TARGETS ${upa_install_targets} EXPORT ${upa_lib_target}-targets DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ${upa_install_file_set} ) install( @@ -279,6 +338,7 @@ if (UPA_INSTALL AND NOT UPA_AMALGAMATED) FILE ${upa_lib_name}-targets.cmake NAMESPACE upa:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${upa_lib_name} + ${upa_install_modules_dir} ) # generate the config file that includes the exports diff --git a/Doxyfile b/Doxyfile index 7dea9471..85ae866d 100644 --- a/Doxyfile +++ b/Doxyfile @@ -2606,7 +2606,9 @@ PREDEFINED = # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. EXPAND_AS_DEFINED = UPA_CONSTEXPR_20 UPA_CONSTEXPR_23 \ - UPA_LIFETIMEBOUND + UPA_EXPORT UPA_EXPORT_BEGIN UPA_EXPORT_END \ + UPA_LIFETIMEBOUND \ + UPA_SO_VISIBLE # If the SKIP_FUNCTION_MACROS tag is set to YES then Doxygen's preprocessor will # remove all references to function-like macros that are alone on a line, have diff --git a/include/upa/buffer.h b/include/upa/buffer.h index 2d5ceeeb..546f40be 100644 --- a/include/upa/buffer.h +++ b/include/upa/buffer.h @@ -12,11 +12,13 @@ #include "config.h" -#include -#include -#include -#include -#include +#ifndef UPA_MODULE +# include +# include +# include +# include +# include +#endif // UPA_MODULE namespace upa { diff --git a/include/upa/config.h b/include/upa/config.h index 5b71f67f..f2d926be 100644 --- a/include/upa/config.h +++ b/include/upa/config.h @@ -6,9 +6,11 @@ #ifndef UPA_CONFIG_H #define UPA_CONFIG_H -#if __has_include() -# include // IWYU pragma: export -#endif +#ifndef UPA_MODULE +# if __has_include() +# include // IWYU pragma: export +# endif +#endif // UPA_MODULE // Macros for compilers that support the C++20 or later standard // https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/ @@ -38,10 +40,24 @@ # elif defined(__clang__) || defined(__GNUC__) # define UPA_API __attribute__((visibility ("default"))) # endif +# if defined(__clang__) || defined(__GNUC__) +# define UPA_SO_VISIBLE __attribute__((visibility ("default"))) +# endif #endif #ifndef UPA_API # define UPA_API #endif +#ifndef UPA_SO_VISIBLE +# define UPA_SO_VISIBLE +#endif + +// The following macros have values when the library is compiled as a module + +#ifndef UPA_EXPORT +# define UPA_EXPORT +# define UPA_EXPORT_BEGIN +# define UPA_EXPORT_END +#endif // Attributes diff --git a/include/upa/public_suffix_list.h b/include/upa/public_suffix_list.h index 4048cce2..44a3c9c6 100644 --- a/include/upa/public_suffix_list.h +++ b/include/upa/public_suffix_list.h @@ -1,4 +1,4 @@ -// Copyright 2024-2025 Rimas Misevičius +// Copyright 2024-2026 Rimas Misevičius // Distributed under the BSD-style license that can be // found in the LICENSE file. // @@ -6,20 +6,25 @@ #define UPA_PUBLIC_SUFFIX_LIST_H #include "url.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include + +#ifndef UPA_MODULE +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#endif // UPA_MODULE namespace upa { +UPA_EXPORT_BEGIN + /// @brief Get label position in hostname by it's index /// /// The label separator can be any of the following characters: U+002E (.), @@ -135,7 +140,7 @@ class public_suffix_list { /// /// @param[in] filename the path to the Public Suffix List file /// @return true if the Public Suffix List was loaded successfully - bool load(const std::filesystem::path& filename) { + inline bool load(const std::filesystem::path& filename) { std::ifstream finp(filename, std::ios::in | std::ios::binary); return finp && load(finp); } @@ -195,7 +200,7 @@ class public_suffix_list { /// @param[in] opt options /// @return public suffix or registrable domain (depends on @p opt value) template = 0> - [[nodiscard]] std::string get_suffix(const StrT& str_host, + [[nodiscard]] inline std::string get_suffix(const StrT& str_host, option opt = option::public_suffix) const { try { return std::string{ get_suffix_view( @@ -215,7 +220,7 @@ class public_suffix_list { /// @param[in] url the @ref url object /// @param[in] opt options /// @return information in the public_suffix_list::result type struct - [[nodiscard]] result get_suffix_info(const url& url, + [[nodiscard]] inline result get_suffix_info(const url& url, option opt = option::public_suffix) const { if (url.host_type() == HostType::Domain) return get_host_suffix_info(url.hostname(), opt); @@ -230,7 +235,7 @@ class public_suffix_list { /// @param[in] host the url_host object /// @param[in] opt options /// @return information in the public_suffix_list::result type struct - [[nodiscard]] result get_suffix_info(const url_host& host, + [[nodiscard]] inline result get_suffix_info(const url_host& host, option opt = option::public_suffix) const { if (host.type() == HostType::Domain) return get_host_suffix_info(host.name(), opt); @@ -246,7 +251,7 @@ class public_suffix_list { /// @param[in] opt options /// @return information in the public_suffix_list::result type struct template = 0> - [[nodiscard]] result get_suffix_info(const StrT& str_host, + [[nodiscard]] inline result get_suffix_info(const StrT& str_host, option opt = option::public_suffix) const { try { auto res = get_suffix_info(upa::url_host{ str_host }, opt); @@ -273,7 +278,7 @@ class public_suffix_list { /// @param[in] url the @ref url object /// @param[in] opt options /// @return public suffix or registrable domain (depends on @p opt value) - [[nodiscard]] std::string_view get_suffix_view(const url& url UPA_LIFETIMEBOUND, + [[nodiscard]] inline std::string_view get_suffix_view(const url& url UPA_LIFETIMEBOUND, option opt = option::public_suffix) const { if (url.host_type() == HostType::Domain) return get_host_suffix_view(url.hostname(), opt); @@ -295,7 +300,7 @@ class public_suffix_list { /// @param[in] host the url_host object /// @param[in] opt options /// @return public suffix or registrable domain (depends on @p opt value) - [[nodiscard]] std::string_view get_suffix_view(const url_host& host UPA_LIFETIMEBOUND, + [[nodiscard]] inline std::string_view get_suffix_view(const url_host& host UPA_LIFETIMEBOUND, option opt = option::public_suffix) const { if (host.type() == HostType::Domain) return get_host_suffix_view(host.name(), opt); @@ -318,7 +323,7 @@ class public_suffix_list { /// @param[in] opt options /// @return public suffix or registrable domain (depends on @p opt value) template = 0> - [[nodiscard]] auto get_suffix_view(const StrT& str_host, option opt = option::public_suffix) const + [[nodiscard]] inline auto get_suffix_view(const StrT& str_host, option opt = option::public_suffix) const -> std::basic_string_view>::value_type> { try { const auto arg = make_str_arg(str_host); @@ -351,7 +356,7 @@ class public_suffix_list { private: UPA_API result get_host_suffix_info(std::string_view hostname, option opt) const; - std::string_view get_host_suffix_view(std::string_view hostname, option opt) const { + inline std::string_view get_host_suffix_view(std::string_view hostname, option opt) const { const auto res = get_host_suffix_info(hostname, opt); if (res) return hostname.substr(res.first_label_pos); @@ -366,9 +371,9 @@ class public_suffix_list { using hash_type = std::hash; using is_transparent = void; - std::size_t operator()(const char* str) const { return hash_type{}(str); } - std::size_t operator()(std::string_view str) const { return hash_type{}(str); } - std::size_t operator()(std::string const& str) const { return hash_type{}(str); } + inline std::size_t operator()(const char* str) const { return hash_type{}(str); } + inline std::size_t operator()(std::string_view str) const { return hash_type{}(str); } + inline std::size_t operator()(std::string const& str) const { return hash_type{}(str); } }; using map_type = std::unordered_map>; #else @@ -378,7 +383,7 @@ class public_suffix_list { std::uint8_t code = 0; std::unique_ptr children; - bool operator==(const label_item& b) const { + inline bool operator==(const label_item& b) const { return code == b.code && ( (!children && !b.children) || (children && b.children && *children == *b.children)); @@ -395,6 +400,9 @@ struct enable_bitmask_operators : public std::true_type {}; } // namespace idna + +UPA_EXPORT_END + } // namespace upa #endif // UPA_PUBLIC_SUFFIX_LIST_H diff --git a/include/upa/regex_engine_srell.h b/include/upa/regex_engine_srell.h index 072fdb50..f4a20149 100644 --- a/include/upa/regex_engine_srell.h +++ b/include/upa/regex_engine_srell.h @@ -1,4 +1,4 @@ -// Copyright 2025 Rimas Misevičius +// Copyright 2025-2026 Rimas Misevičius // Distributed under the BSD-style license that can be // found in the LICENSE file. // @@ -28,10 +28,10 @@ class regex_engine_srell { public: class result { public: - std::size_t size() const { + inline std::size_t size() const { return arr_.size(); } - std::optional get(std::size_t ind, std::string_view) const { + inline std::optional get(std::size_t ind, std::string_view) const { const auto& res = arr_[ind]; if (res.matched) return std::string{ res.first, res.second }; @@ -42,7 +42,7 @@ class regex_engine_srell { friend class regex_engine_srell; }; - bool init(std::string_view regex_str, bool ignore_case) { + inline bool init(std::string_view regex_str, bool ignore_case) { // 4. If options's ignore case is true then set flags to "vi". // 5. Otherwise set flags to "v" const auto commonflags = srell::regex::ECMAScript | srell::regex::vmode | @@ -53,10 +53,10 @@ class regex_engine_srell { return re_.assign(regex_str.data(), regex_str.length(), flag).ecode() == 0; } - bool exec(std::string_view input, result& res) const { + inline bool exec(std::string_view input, result& res) const { return srell::regex_match(input.begin(), input.end(), res.arr_, re_); } - bool test(std::string_view input) const { + inline bool test(std::string_view input) const { return srell::regex_match(input.begin(), input.end(), re_); } diff --git a/include/upa/regex_engine_std.h b/include/upa/regex_engine_std.h index 97ff2bb8..41e18a7b 100644 --- a/include/upa/regex_engine_std.h +++ b/include/upa/regex_engine_std.h @@ -1,4 +1,4 @@ -// Copyright 2025 Rimas Misevičius +// Copyright 2025-2026 Rimas Misevičius // Distributed under the BSD-style license that can be // found in the LICENSE file. // @@ -25,10 +25,10 @@ class regex_engine_std { public: class result { public: - std::size_t size() const { + inline std::size_t size() const { return arr_.size(); } - std::optional get(std::size_t ind, std::string_view) const { + inline std::optional get(std::size_t ind, std::string_view) const { const auto& res = arr_[ind]; if (res.matched) return std::string{ res.first, res.second }; @@ -39,7 +39,7 @@ class regex_engine_std { friend class regex_engine_std; }; - bool init(std::string_view regex_str, bool ignore_case) { + inline bool init(std::string_view regex_str, bool ignore_case) { // 4. If options's ignore case is true then set flags to "vi". // 5. Otherwise set flags to "v" // Note that the std::regex does not have the "v" flag. @@ -55,10 +55,10 @@ class regex_engine_std { } } - bool exec(std::string_view input, result& res) const { + inline bool exec(std::string_view input, result& res) const { return std::regex_match(input.begin(), input.end(), res.arr_, re_); } - bool test(std::string_view input) const { + inline bool test(std::string_view input) const { return std::regex_match(input.begin(), input.end(), re_); } diff --git a/include/upa/str_arg.h b/include/upa/str_arg.h index ac68dd7a..d18054db 100644 --- a/include/upa/str_arg.h +++ b/include/upa/str_arg.h @@ -21,13 +21,16 @@ inline void procfn(const StrT& str) { #include "config.h" #include "url_utf.h" #include "util.h" -#include -#include -#include -#include -#include -#include -#include + +#ifndef UPA_MODULE +# include +# include +# include +# include +# include +# include +# include +#endif // UPA_MODULE namespace upa { @@ -35,6 +38,8 @@ namespace upa { using string_view [[deprecated("Use std::string_view instead.")]] = std::string_view; +UPA_EXPORT_BEGIN + // Supported char and size types template @@ -134,6 +139,8 @@ using remove_cvptr_t = std::remove_cv_t>; template using remove_cvref_t = std::remove_cv_t>; +UPA_EXPORT_END + namespace detail { // Check that StrT has data() and size() members of supported types @@ -217,6 +224,7 @@ struct str_arg_char_default> { } }; +UPA_EXPORT_END // String arguments helper types @@ -291,6 +300,7 @@ using enable_if_str_arg_to_charW_t = std::enable_if_t< is_charW_type_v>, int>; +UPA_EXPORT_BEGIN UPA_CONSTEXPR_20 std::string&& make_string(std::string&& str) { return std::move(str); @@ -308,6 +318,7 @@ inline std::string make_string(const StrT& str) { return url_utf::to_utf8_string(inp.begin(), inp.end()); } +UPA_EXPORT_END // Support for optional string arguments @@ -326,7 +337,11 @@ inline constexpr bool is_just_optional_v> = true; } // namespace detail -// helpers for optional string arguments +// Helpers for optional string arguments + +// The upa::nullopt can be used instead of std::nullopt to provide a workaround for the MSVC bug +// https://developercommunity.visualstudio.com/t/The-C2039:-nullopt:-is-not-a-member-o/11099621 +UPA_EXPORT inline constexpr auto nullopt = std::nullopt; template using enable_if_optional_str_arg_t = std::enable_if_t< diff --git a/include/upa/url.h b/include/upa/url.h index cafbeb4d..9975ce21 100644 --- a/include/upa/url.h +++ b/include/upa/url.h @@ -26,20 +26,23 @@ #include "url_search_params.h" // IWYU pragma: export #include "url_version.h" // IWYU pragma: export #include "util.h" -#include -#include -#include -#include -#include // uint8_t -#include -#include // std::hash -#include -#include -#include -#include -#include -#include -#include + +#ifndef UPA_MODULE +# include +# include +# include +# include +# include // uint8_t +# include +# include // std::hash +# include +# include +# include +# include +# include +# include +# include +#endif // UPA_MODULE // not yet // #define UPA_URL_USE_ENCODING @@ -71,6 +74,8 @@ inline constexpr const char* kBaseURLParseError = "Base URL parse error"; } // namespace detail +UPA_EXPORT_BEGIN + /// @brief URL class /// /// Follows specification in @@ -142,7 +147,7 @@ class url { /// @param[in] str_url URL string to parse /// @param[in] pbase pointer to base URL, may be `nullptr` template = 0> - explicit url(const T& str_url, const url* pbase = nullptr) + inline explicit url(const T& str_url, const url* pbase = nullptr) : url{ str_url, pbase, detail::kURLParseError } {} @@ -153,7 +158,7 @@ class url { /// @param[in] str_url URL string to parse /// @param[in] base base URL template = 0> - explicit url(const T& str_url, const url& base) + inline explicit url(const T& str_url, const url& base) : url{ str_url, &base, detail::kURLParseError } {} @@ -164,7 +169,7 @@ class url { /// @param[in] str_url URL string to parse /// @param[in] str_base base URL string template = 0, enable_if_str_arg_t = 0> - explicit url(const T& str_url, const TB& str_base) + inline explicit url(const T& str_url, const TB& str_base) : url{ str_url, url{ str_base, nullptr, detail::kBaseURLParseError } } {} @@ -191,7 +196,7 @@ class url { /// @param[in] base pointer to base URL, may be nullptr /// @return error code (@a validation_errc::ok on success) template = 0> - validation_errc parse(const T& str_url, const url* base = nullptr) { + inline validation_errc parse(const T& str_url, const url* base = nullptr) { const auto inp = make_str_arg(str_url); return do_parse(inp.begin(), inp.end(), base); } @@ -202,7 +207,7 @@ class url { /// @param[in] base base URL /// @return error code (@a validation_errc::ok on success) template = 0> - validation_errc parse(const T& str_url, const url& base) { + inline validation_errc parse(const T& str_url, const url& base) { return parse(str_url, &base); } @@ -212,7 +217,7 @@ class url { /// @param[in] str_base base URL string /// @return error code (@a validation_errc::ok on success) template = 0, enable_if_str_arg_t = 0> - validation_errc parse(const T& str_url, const TB& str_base) { + inline validation_errc parse(const T& str_url, const TB& str_base) { upa::url base; const auto res = base.parse(str_base, nullptr); return res == validation_errc::ok @@ -229,7 +234,7 @@ class url { /// @param[in] pbase pointer to base URL, may be `nullptr` /// @return true if given @a str_url can be parsed against @a *pbase template = 0> - [[nodiscard]] static bool can_parse(const T& str_url, const url* pbase = nullptr) { + [[nodiscard]] static inline bool can_parse(const T& str_url, const url* pbase = nullptr) { upa::url url; return url.for_can_parse(str_url, pbase) == validation_errc::ok; } @@ -243,7 +248,7 @@ class url { /// @param[in] base base URL /// @return true if given @a str_url can be parsed against base URL template = 0> - [[nodiscard]] static bool can_parse(const T& str_url, const url& base) { + [[nodiscard]] static inline bool can_parse(const T& str_url, const url& base) { return can_parse(str_url, &base); } @@ -257,7 +262,7 @@ class url { /// @param[in] str_base base URL string /// @return true if given @a str_url can be parsed against @a str_base URL string template = 0, enable_if_str_arg_t = 0> - [[nodiscard]] static bool can_parse(const T& str_url, const TB& str_base) { + [[nodiscard]] static inline bool can_parse(const T& str_url, const TB& str_base) { upa::url base; return base.for_can_parse(str_base, nullptr) == validation_errc::ok && @@ -277,7 +282,7 @@ class url { bool href(const StrT& str); /// Equivalent to @link href(const StrT& str) @endlink template = 0> - bool set_href(const StrT& str) { return href(str); } + inline bool set_href(const StrT& str) { return href(str); } /// @brief The protocol setter /// @@ -290,7 +295,7 @@ class url { bool protocol(const StrT& str); /// Equivalent to @link protocol(const StrT& str) @endlink template = 0> - bool set_protocol(const StrT& str) { return protocol(str); } + inline bool set_protocol(const StrT& str) { return protocol(str); } /// @brief The username setter /// @@ -303,7 +308,7 @@ class url { bool username(const StrT& str); /// Equivalent to @link username(const StrT& str) @endlink template = 0> - bool set_username(const StrT& str) { return username(str); } + inline bool set_username(const StrT& str) { return username(str); } /// @brief The password setter /// @@ -316,7 +321,7 @@ class url { bool password(const StrT& str); /// Equivalent to @link password(const StrT& str) @endlink template = 0> - bool set_password(const StrT& str) { return password(str); } + inline bool set_password(const StrT& str) { return password(str); } /// @brief The host setter /// @@ -329,7 +334,7 @@ class url { bool host(const StrT& str); /// Equivalent to @link host(const StrT& str) @endlink template = 0> - bool set_host(const StrT& str) { return host(str); } + inline bool set_host(const StrT& str) { return host(str); } /// @brief The hostname setter /// @@ -342,7 +347,7 @@ class url { bool hostname(const StrT& str); /// Equivalent to @link hostname(const StrT& str) @endlink template = 0> - bool set_hostname(const StrT& str) { return hostname(str); } + inline bool set_hostname(const StrT& str) { return hostname(str); } /// @brief The port setter /// @@ -355,7 +360,7 @@ class url { bool port(const StrT& str); /// Equivalent to @link port(const StrT& str) @endlink template = 0> - bool set_port(const StrT& str) { return port(str); } + inline bool set_port(const StrT& str) { return port(str); } /// @brief The pathname setter /// @@ -368,7 +373,7 @@ class url { bool pathname(const StrT& str); /// Equivalent to @link pathname(const StrT& str) @endlink template = 0> - bool set_pathname(const StrT& str) { return pathname(str); } + inline bool set_pathname(const StrT& str) { return pathname(str); } /// @brief The search setter /// @@ -381,7 +386,7 @@ class url { bool search(const StrT& str); /// Equivalent to @link search(const StrT& str) @endlink template = 0> - bool set_search(const StrT& str) { return search(str); } + inline bool set_search(const StrT& str) { return search(str); } /// @brief The hash setter /// @@ -394,7 +399,7 @@ class url { bool hash(const StrT& str); /// Equivalent to @link hash(const StrT& str) @endlink template = 0> - bool set_hash(const StrT& str) { return hash(str); } + inline bool set_hash(const StrT& str) { return hash(str); } // Getters @@ -405,7 +410,7 @@ class url { /// @return serialized URL [[nodiscard]] std::string_view href() const UPA_LIFETIMEBOUND; /// Equivalent to @link href() const @endlink - [[nodiscard]] std::string_view get_href() const UPA_LIFETIMEBOUND { return href(); } + [[nodiscard]] inline std::string_view get_href() const UPA_LIFETIMEBOUND { return href(); } /// @brief The origin getter /// @@ -424,7 +429,7 @@ class url { /// @return URL's scheme, followed by U+003A (:) [[nodiscard]] std::string_view protocol() const UPA_LIFETIMEBOUND; /// Equivalent to @link protocol() const @endlink - [[nodiscard]] std::string_view get_protocol() const UPA_LIFETIMEBOUND { return protocol(); } + [[nodiscard]] inline std::string_view get_protocol() const UPA_LIFETIMEBOUND { return protocol(); } /// @brief The username getter /// @@ -433,7 +438,7 @@ class url { /// @return URL’s username [[nodiscard]] std::string_view username() const UPA_LIFETIMEBOUND; /// Equivalent to @link username() const @endlink - [[nodiscard]] std::string_view get_username() const UPA_LIFETIMEBOUND { return username(); } + [[nodiscard]] inline std::string_view get_username() const UPA_LIFETIMEBOUND { return username(); } /// @brief The password getter /// @@ -442,7 +447,7 @@ class url { /// @return URL’s password [[nodiscard]] std::string_view password() const UPA_LIFETIMEBOUND; /// Equivalent to @link password() const @endlink - [[nodiscard]] std::string_view get_password() const UPA_LIFETIMEBOUND { return password(); } + [[nodiscard]] inline std::string_view get_password() const UPA_LIFETIMEBOUND { return password(); } /// @brief The host getter /// @@ -451,7 +456,7 @@ class url { /// @return URL’s host, serialized, followed by U+003A (:) and URL’s port, serialized [[nodiscard]] std::string_view host() const UPA_LIFETIMEBOUND; /// Equivalent to @link host() const @endlink - [[nodiscard]] std::string_view get_host() const UPA_LIFETIMEBOUND { return host(); } + [[nodiscard]] inline std::string_view get_host() const UPA_LIFETIMEBOUND { return host(); } /// @brief The hostname getter /// @@ -460,7 +465,7 @@ class url { /// @return URL’s host, serialized [[nodiscard]] std::string_view hostname() const UPA_LIFETIMEBOUND; /// Equivalent to @link hostname() const @endlink - [[nodiscard]] std::string_view get_hostname() const UPA_LIFETIMEBOUND { return hostname(); } + [[nodiscard]] inline std::string_view get_hostname() const UPA_LIFETIMEBOUND { return hostname(); } /// @brief The host_type getter /// @@ -474,7 +479,7 @@ class url { /// @return URL’s port, serialized, if URL’s port is not null, otherwise empty string [[nodiscard]] std::string_view port() const UPA_LIFETIMEBOUND; /// Equivalent to @link port() const @endlink - [[nodiscard]] std::string_view get_port() const UPA_LIFETIMEBOUND { return port(); } + [[nodiscard]] inline std::string_view get_port() const UPA_LIFETIMEBOUND { return port(); } /// @return URL’s port, converted to `int` value, if URL’s port is not null, /// otherwise `-1` @@ -490,7 +495,7 @@ class url { /// @return URL's path, serialized, followed by U+003F (?) and URL’s query [[nodiscard]] std::string_view path() const UPA_LIFETIMEBOUND; /// Equivalent to @link path() const @endlink - [[nodiscard]] std::string_view get_path() const UPA_LIFETIMEBOUND { return path(); } + [[nodiscard]] inline std::string_view get_path() const UPA_LIFETIMEBOUND { return path(); } /// @brief The pathname getter /// @@ -499,7 +504,7 @@ class url { /// @return URL’s path, serialized [[nodiscard]] std::string_view pathname() const UPA_LIFETIMEBOUND; /// Equivalent to @link pathname() const @endlink - [[nodiscard]] std::string_view get_pathname() const UPA_LIFETIMEBOUND { return pathname(); } + [[nodiscard]] inline std::string_view get_pathname() const UPA_LIFETIMEBOUND { return pathname(); } /// @brief The search getter /// @@ -508,7 +513,7 @@ class url { /// @return empty string or U+003F (?), followed by URL’s query [[nodiscard]] std::string_view search() const UPA_LIFETIMEBOUND; /// Equivalent to @link search() const @endlink - [[nodiscard]] std::string_view get_search() const UPA_LIFETIMEBOUND { return search(); } + [[nodiscard]] inline std::string_view get_search() const UPA_LIFETIMEBOUND { return search(); } /// @brief The hash getter /// @@ -517,7 +522,7 @@ class url { /// @return empty string or U+0023 (#), followed by URL’s fragment [[nodiscard]] std::string_view hash() const UPA_LIFETIMEBOUND; /// Equivalent to @link hash() const @endlink - [[nodiscard]] std::string_view get_hash() const UPA_LIFETIMEBOUND { return hash(); } + [[nodiscard]] inline std::string_view get_hash() const UPA_LIFETIMEBOUND { return hash(); } /// @brief The searchParams getter /// @@ -670,21 +675,6 @@ class url { INITIAL_FLAGS = SCHEME_FLAG | USERNAME_FLAG | PASSWORD_FLAG | PATH_FLAG, }; - // part flag masks - static constexpr unsigned kPartFlagMask[url::PART_COUNT] = { - SCHEME_FLAG, - 0, // SCHEME_SEP - USERNAME_FLAG, - PASSWORD_FLAG, - 0, // HOST_START - HOST_FLAG | HOST_TYPE_MASK, - PORT_FLAG, - 0, // PATH_PREFIX - PATH_FLAG | OPAQUE_PATH_FLAG, - QUERY_FLAG, - FRAGMENT_FLAG - }; - // parsing constructor template = 0> explicit url(const T& str_url, const url* base, const char* what_arg); @@ -745,16 +735,17 @@ class url { friend class url_search_params; }; +UPA_EXPORT_END namespace detail { -class url_serializer : public host_output { +class UPA_SO_VISIBLE url_serializer : public host_output { public: url_serializer() = delete; url_serializer(const url_serializer&) = delete; url_serializer& operator=(const url_serializer&) = delete; - explicit url_serializer(url& dest_url, bool need_save = true) + inline explicit url_serializer(url& dest_url, bool need_save = true) : host_output(need_save) , url_(dest_url) , last_pt_(url::SCHEME) @@ -762,16 +753,18 @@ class url_serializer : public host_output { ~url_serializer() override = default; - void new_url() { + inline void new_url() { if (!url_.empty()) url_.clear(); } - virtual void reserve(std::size_t new_cap) { util::reserve(url_.norm_url_, new_cap); } + inline virtual void reserve(std::size_t new_cap) { + util::reserve(url_.norm_url_, new_cap); + } // set data - void set_scheme(const url& src) { url_.set_scheme(src); } - void set_scheme(std::string_view str) { url_.set_scheme(str); } - void set_scheme(std::size_t scheme_length) { url_.set_scheme(scheme_length); } + inline void set_scheme(const url& src) { url_.set_scheme(src); } + inline void set_scheme(std::string_view str) { url_.set_scheme(str); } + inline void set_scheme(std::size_t scheme_length) { url_.set_scheme(scheme_length); } // set scheme virtual std::string& start_scheme(); @@ -782,7 +775,7 @@ class url_serializer : public host_output { virtual std::string& start_part(url::PartType new_pt); virtual void save_part(); - virtual void clear_part(url::PartType /*pt*/) {} + inline virtual void clear_part(url::PartType /*pt*/) {} // set empty host void set_empty_host(); @@ -814,29 +807,29 @@ class url_serializer : public host_output { void append_parts(const url& src, url::PartType t1, url::PartType t2, PathOpFn pathOpFn = nullptr); // flags - void set_flag(const url::UrlFlag flag) { url_.set_flag(flag); } - void set_host_type(const HostType ht) { url_.set_host_type(ht); } + inline void set_flag(const url::UrlFlag flag) { url_.set_flag(flag); } + inline void set_host_type(const HostType ht) { url_.set_host_type(ht); } // IMPORTANT: has-an-opaque-path flag must be set before or just after // SCHEME set; because other part's serialization depends on this flag - void set_has_opaque_path() { + inline void set_has_opaque_path() { assert(last_pt_ == url::SCHEME); url_.set_has_opaque_path(); } // get info - std::string_view get_part_view(url::PartType t) const { return url_.get_part_view(t); } - bool is_empty(const url::PartType t) const { return url_.is_empty(t); } - virtual bool is_empty_path() const { + inline std::string_view get_part_view(url::PartType t) const { return url_.get_part_view(t); } + inline bool is_empty(const url::PartType t) const { return url_.is_empty(t); } + inline virtual bool is_empty_path() const { assert(!url_.has_opaque_path()); // path_segment_count_ has meaning only if path is a list (path isn't opaque) return url_.path_segment_count_ == 0; } - bool is_null(const url::PartType t) const noexcept { return url_.is_null(t); } - bool is_special_scheme() const noexcept { return url_.is_special_scheme(); } - bool is_file_scheme() const noexcept { return url_.is_file_scheme(); } - bool has_credentials() const { return url_.has_credentials(); } - const detail::scheme_info* scheme_inf() const noexcept { return url_.scheme_inf_; } - int port_int() const { return url_.port_int(); } + inline bool is_null(const url::PartType t) const noexcept { return url_.is_null(t); } + inline bool is_special_scheme() const noexcept { return url_.is_special_scheme(); } + inline bool is_file_scheme() const noexcept { return url_.is_file_scheme(); } + inline bool has_credentials() const { return url_.has_credentials(); } + inline const detail::scheme_info* scheme_inf() const noexcept { return url_.scheme_inf_; } + inline int port_int() const { return url_.port_int(); } protected: void adjust_path_prefix(); @@ -854,13 +847,13 @@ class url_serializer : public host_output { }; -class url_setter : public url_serializer { +class UPA_SO_VISIBLE url_setter : public url_serializer { public: url_setter() = delete; url_setter(const url_setter&) = delete; url_setter& operator=(const url_setter&) = delete; - explicit url_setter(url& dest_url) + inline explicit url_setter(url& dest_url) : url_serializer(dest_url) , use_strp_(true) , curr_pt_(url::SCHEME) @@ -936,7 +929,7 @@ class url_parser { }; template - static validation_errc url_parse(url_serializer& urls, const CharT* first, const CharT* last, const url* base, State state_override = not_set_state); + static validation_errc url_parse(url_serializer& urls, const CharT* first, const CharT* last, const url* base, State state_override); template static validation_errc parse_host(url_serializer& urls, const CharT* first, const CharT* last); @@ -1460,7 +1453,7 @@ inline validation_errc url::do_parse(const CharT* first, const CharT* last, cons detail::do_trim(first, last); //TODO-WARN: validation error if trimmed - return detail::url_parser::url_parse(urls, first, last, base); + return detail::url_parser::url_parse(urls, first, last, base, detail::url_parser::not_set_state); }(); if (res == validation_errc::ok) { set_flag(VALID_FLAG); @@ -1488,7 +1481,7 @@ validation_errc url::for_can_parse(const T& str_url, const url* base) { detail::do_trim(first, last); //TODO-WARN: validation error if trimmed - return detail::url_parser::url_parse(urls, first, last, base); + return detail::url_parser::url_parse(urls, first, last, base, detail::url_parser::not_set_state); }(); if (res == validation_errc::ok) set_flag(VALID_FLAG); @@ -2759,10 +2752,25 @@ inline void url_serializer::append_parts(const url& src, url::PartType t1, url:: return t1; }(); + // part flag masks + static constexpr unsigned kPartFlagMask[url::PART_COUNT] = { + url::SCHEME_FLAG, + 0, // SCHEME_SEP + url::USERNAME_FLAG, + url::PASSWORD_FLAG, + 0, // HOST_START + url::HOST_FLAG | url::HOST_TYPE_MASK, + url::PORT_FLAG, + 0, // PATH_PREFIX + url::PATH_FLAG | url::OPAQUE_PATH_FLAG, + url::QUERY_FLAG, + url::FRAGMENT_FLAG + }; + // copy flags; they can be used when copying / serializing url parts below unsigned mask = 0; for (int ind = t1; ind <= t2; ++ind) { - mask |= url::kPartFlagMask[ind]; + mask |= kPartFlagMask[ind]; } url_.flags_ = (url_.flags_ & ~mask) | (src.flags_ & mask); @@ -3111,6 +3119,7 @@ constexpr bool has_dot_dot_segment(const CharT* first, const CharT* last, IsSlas } // namespace detail +UPA_EXPORT_BEGIN // URL utilities (non-member functions) @@ -3399,6 +3408,8 @@ inline bool check_version() { (static_cast(UPA_URL_VERSION_NUM) & sover_mask); } +UPA_EXPORT_END + } // namespace upa @@ -3407,7 +3418,7 @@ namespace std { /// @brief std::hash specialization for upa::url class template<> struct hash { - [[nodiscard]] std::size_t operator()(const upa::url& url) const noexcept { + [[nodiscard]] inline std::size_t operator()(const upa::url& url) const noexcept { return std::hash{}(url.norm_url_); } }; diff --git a/include/upa/url_for_atl.h b/include/upa/url_for_atl.h index 4df7032f..8ff6e06c 100644 --- a/include/upa/url_for_atl.h +++ b/include/upa/url_for_atl.h @@ -1,4 +1,4 @@ -// Copyright 2024 Rimas Misevičius +// Copyright 2024-2026 Rimas Misevičius // Distributed under the BSD-style license that can be // found in the LICENSE file. @@ -23,7 +23,7 @@ template struct str_arg_char_for_atl { using type = typename StrT::XCHAR; - static str_arg to_str_arg(const StrT& str) { + static inline str_arg to_str_arg(const StrT& str) { return { str.GetString(), str.GetLength() }; } }; diff --git a/include/upa/url_for_qt.h b/include/upa/url_for_qt.h index 0dd967e8..673d5ccd 100644 --- a/include/upa/url_for_qt.h +++ b/include/upa/url_for_qt.h @@ -1,4 +1,4 @@ -// Copyright 2024-2025 Rimas Misevičius +// Copyright 2024-2026 Rimas Misevičius // Distributed under the BSD-style license that can be // found in the LICENSE file. @@ -36,7 +36,7 @@ struct str_arg_char_for_qt { "StrT::value_type and CharT must be the same size"); using type = CharT; - static str_arg to_str_arg(const StrT& str) { + static inline str_arg to_str_arg(const StrT& str) { return { str.data(), str.size() }; } }; diff --git a/include/upa/url_host.h b/include/upa/url_host.h index b64c382d..4ae19674 100644 --- a/include/upa/url_host.h +++ b/include/upa/url_host.h @@ -15,19 +15,22 @@ #include "url_result.h" #include "url_utf.h" #include "util.h" -#include // any_of -#include -#include // uint16_t, uint32_t -#include -#include -#include + +#ifndef UPA_MODULE +# include // any_of +# include +# include // uint16_t, uint32_t +# include +# include +# include +#endif // UPA_MODULE namespace upa { /// @brief Host representation /// /// See: https://url.spec.whatwg.org/#host-representation -enum class HostType { +UPA_EXPORT enum class HostType { Empty = 0, ///< **empty host** is the empty string Opaque, ///< **opaque host** is a non-empty ASCII string used in a not special URL Domain, ///< **domain** is a non-empty ASCII string that identifies a realm within a network @@ -37,10 +40,10 @@ enum class HostType { }; -class host_output { +class UPA_SO_VISIBLE host_output { protected: host_output() = default; - host_output(bool need_save) + inline host_output(bool need_save) : need_save_{ need_save } {} public: host_output(const host_output&) = delete; @@ -49,7 +52,7 @@ class host_output { virtual std::string& hostStart() = 0; virtual void hostDone(HostType /*ht*/) = 0; - bool need_save() const noexcept { return need_save_; } + inline bool need_save() const noexcept { return need_save_; } private: bool need_save_ = true; }; @@ -74,7 +77,7 @@ class host_parser { // https://github.com/whatwg/url/pull/288 // https://whatpr.org/url/288.html#urlhost-class -class url_host { +UPA_EXPORT class url_host { public: url_host() = delete; url_host(const url_host&) = default; @@ -88,7 +91,7 @@ class url_host { /// /// @param[in] str Host string to parse template = 0> - explicit url_host(const StrT& str) { + inline explicit url_host(const StrT& str) { host_out out(*this); const auto inp = make_str_arg(str); @@ -103,34 +106,34 @@ class url_host { /// Host type getter /// /// @return host type, the one of: Domain, IPv4, IPv6 - [[nodiscard]] HostType type() const { + [[nodiscard]] inline HostType type() const { return type_; } /// Hostname view /// /// @return serialized host as std::string_view - [[nodiscard]] std::string_view name() const UPA_LIFETIMEBOUND { + [[nodiscard]] inline std::string_view name() const UPA_LIFETIMEBOUND { return host_str_; } /// Hostname stringifier /// /// @return host serialized to string - [[nodiscard]] std::string to_string() const { + [[nodiscard]] inline std::string to_string() const { return host_str_; } private: - class host_out : public host_output { + class UPA_SO_VISIBLE host_out : public host_output { public: - explicit host_out(url_host& host) + inline explicit host_out(url_host& host) : host_(host) {} - std::string& hostStart() override { + inline std::string& hostStart() override { return host_.host_str_; } - void hostDone(HostType ht) override { + inline void hostDone(HostType ht) override { host_.type_ = ht; } private: @@ -174,7 +177,7 @@ UPA_CONSTEXPR_20 bool contains_forbidden_host_char(const CharT* first, const Cha /// @param[in] be_strict /// @param[in] is_input_ascii /// @return `true` on success, or `false` on errors -template = 0> +UPA_EXPORT template = 0> inline bool domain_to_unicode(std::basic_string& output, const StrT& input, bool be_strict = false, bool is_input_ascii = false) { diff --git a/include/upa/url_ip.h b/include/upa/url_ip.h index 7c8639d8..3ee42c03 100644 --- a/include/upa/url_ip.h +++ b/include/upa/url_ip.h @@ -9,12 +9,15 @@ #include "config.h" // IWYU pragma: export #include "url_percent_encode.h" #include "url_result.h" -#include -#include -#include // uint16_t, uint32_t, uint64_t -#include -#include -#include + +#ifndef UPA_MODULE +# include +# include +# include // uint16_t, uint32_t, uint64_t +# include +# include +# include +#endif // UPA_MODULE namespace upa { diff --git a/include/upa/url_percent_encode.h b/include/upa/url_percent_encode.h index 70e88153..f79b4bf2 100644 --- a/include/upa/url_percent_encode.h +++ b/include/upa/url_percent_encode.h @@ -14,16 +14,20 @@ #include "str_arg.h" #include "url_utf.h" #include "util.h" -#include -#include // uint8_t -#include -#include -#include -#include + +#ifndef UPA_MODULE +# include +# include // uint8_t +# include +# include +# include +# include +#endif // UPA_MODULE namespace upa { +UPA_EXPORT_BEGIN /// @brief Represents code point set /// @@ -163,6 +167,7 @@ inline constexpr code_point_set component_no_encode_set{ [](code_point_set& self self.exclude({ 0x24, 0x25, 0x26, 0x2B, 0x2C }); } }; +UPA_EXPORT_END namespace detail { @@ -506,6 +511,7 @@ inline void append_percent_decoded(StrT&& str, std::string& output) { } // namespace detail +UPA_EXPORT_BEGIN /// @brief Percent decode input string. /// @@ -558,6 +564,7 @@ template = 0> return percent_encode(std::forward(str), component_no_encode_set); } +UPA_EXPORT_END } // namespace upa diff --git a/include/upa/url_result.h b/include/upa/url_result.h index 4502b252..c14be0f8 100644 --- a/include/upa/url_result.h +++ b/include/upa/url_result.h @@ -1,4 +1,4 @@ -// Copyright 2016-2023 Rimas Misevičius +// Copyright 2016-2026 Rimas Misevičius // Distributed under the BSD-style license that can be // found in the LICENSE file. // @@ -6,10 +6,16 @@ #ifndef UPA_URL_RESULT_H #define UPA_URL_RESULT_H -#include +#include "config.h" + +#ifndef UPA_MODULE +# include +#endif // UPA_MODULE namespace upa { +UPA_EXPORT_BEGIN + /// @brief URL validation and other error codes /// /// See: https://url.spec.whatwg.org/#validation-error @@ -94,26 +100,27 @@ enum class validation_errc { } /// @brief URL exception class - -class url_error : public std::runtime_error { +class UPA_SO_VISIBLE url_error : public std::runtime_error { public: /// constructs a new url_error object with the given result code and error message /// /// @param[in] res validation error code /// @param[in] what_arg error message - explicit url_error(validation_errc res, const char* what_arg) + inline explicit url_error(validation_errc res, const char* what_arg) : std::runtime_error(what_arg) , res_(res) {} /// @return validation error code - [[nodiscard]] validation_errc result() const noexcept { + [[nodiscard]] inline validation_errc result() const noexcept { return res_; } private: validation_errc res_; }; +UPA_EXPORT_END + namespace detail { /// @brief Result/value pair diff --git a/include/upa/url_search_params-inl.h b/include/upa/url_search_params-inl.h index 3a335729..0d96bbb5 100644 --- a/include/upa/url_search_params-inl.h +++ b/include/upa/url_search_params-inl.h @@ -1,4 +1,4 @@ -// Copyright 2016-2025 Rimas Misevičius +// Copyright 2016-2026 Rimas Misevičius // Distributed under the BSD-style license that can be // found in the LICENSE file. // @@ -10,7 +10,9 @@ #ifndef UPA_URL_SEARCH_PARAMS_INL_H #define UPA_URL_SEARCH_PARAMS_INL_H -#include // std::addressof +#ifndef UPA_MODULE +# include // std::addressof +#endif // UPA_MODULE namespace upa { diff --git a/include/upa/url_search_params.h b/include/upa/url_search_params.h index 5c666732..3e8fe2ce 100644 --- a/include/upa/url_search_params.h +++ b/include/upa/url_search_params.h @@ -10,14 +10,17 @@ #include "str_arg.h" #include "url_percent_encode.h" #include "url_utf.h" -#include -#include -#include -#include -#include -#include -#include -#include + +#ifndef UPA_MODULE +# include +# include +# include +# include +# include +# include +# include +# include +#endif // UPA_MODULE namespace upa { @@ -45,19 +48,21 @@ constexpr bool is_iterable_pairs_v using enable_if_not_base_of_t = std::enable_if_t< - !std::is_base_of_v>, int ->; + !std::is_base_of_v>, int>; } // namespace detail // forward declarations -class url; namespace detail { class url_search_params_ptr; } // namespace detail +UPA_EXPORT_BEGIN + +class url; + /// @brief URLSearchParams class /// /// Follows specification in @@ -103,7 +108,7 @@ class url_search_params /// /// @param[in] query string to parse template = 0> - explicit url_search_params(StrT&& query) + inline explicit url_search_params(StrT&& query) : params_(do_parse(true, std::forward(query))) {} @@ -115,7 +120,7 @@ class url_search_params detail::enable_if_not_base_of_t = 0, std::enable_if_t, int> = 0 > - explicit url_search_params(ConT&& cont) { + inline explicit url_search_params(ConT&& cont) { for (const auto& p : cont) { params_.emplace_back(make_string(p.first), make_string(p.second)); } @@ -303,38 +308,38 @@ class url_search_params // Iterators /// @return an iterator to the beginning of name-value list - [[nodiscard]] const_iterator begin() const noexcept { return params_.begin(); } + [[nodiscard]] inline const_iterator begin() const noexcept { return params_.begin(); } /// @return an iterator to the beginning of name-value list - [[nodiscard]] const_iterator cbegin() const noexcept { return params_.cbegin(); } + [[nodiscard]] inline const_iterator cbegin() const noexcept { return params_.cbegin(); } /// @return an iterator to the end of name-value list - [[nodiscard]] const_iterator end() const noexcept { return params_.end(); } + [[nodiscard]] inline const_iterator end() const noexcept { return params_.end(); } /// @return an iterator to the end of name-value list - [[nodiscard]] const_iterator cend() const noexcept { return params_.cend(); } + [[nodiscard]] inline const_iterator cend() const noexcept { return params_.cend(); } /// @return a reverse iterator to the beginning of name-value list - [[nodiscard]] const_reverse_iterator rbegin() const noexcept { return params_.rbegin(); } + [[nodiscard]] inline const_reverse_iterator rbegin() const noexcept { return params_.rbegin(); } /// @return a reverse iterator to the beginning of name-value list - [[nodiscard]] const_reverse_iterator crbegin() const noexcept { return params_.crbegin(); } + [[nodiscard]] inline const_reverse_iterator crbegin() const noexcept { return params_.crbegin(); } /// @return a reverse iterator to the end of name-value list - [[nodiscard]] const_reverse_iterator rend() const noexcept { return params_.rend(); } + [[nodiscard]] inline const_reverse_iterator rend() const noexcept { return params_.rend(); } /// @return a reverse iterator to the end of name-value list - [[nodiscard]] const_reverse_iterator crend() const noexcept { return params_.crend(); } + [[nodiscard]] inline const_reverse_iterator crend() const noexcept { return params_.crend(); } // Capacity /// Checks whether the name-value list is empty /// /// @return `true` if the container is empty, `false` otherwise - [[nodiscard]] bool empty() const noexcept { return params_.empty(); } + [[nodiscard]] inline bool empty() const noexcept { return params_.empty(); } /// @return the number of elements in the name-value list - [[nodiscard]] size_type size() const noexcept { return params_.size(); } + [[nodiscard]] inline size_type size() const noexcept { return params_.size(); } // Utils @@ -376,6 +381,7 @@ class url_search_params url* url_ptr_ = nullptr; }; +UPA_EXPORT_END namespace detail { @@ -395,20 +401,20 @@ class url_search_params_ptr // destructor UPA_CONSTEXPR_23 ~url_search_params_ptr() = default; - void init(url* url_ptr) { + inline void init(url* url_ptr) { ptr_.reset(new url_search_params(url_ptr)); // NOLINT(cppcoreguidelines-owning-memory) } - void set_url_ptr(url* url_ptr) noexcept { + inline void set_url_ptr(url* url_ptr) noexcept { if (ptr_) ptr_->url_ptr_ = url_ptr; } - void clear_params() noexcept { + inline void clear_params() noexcept { assert(ptr_); ptr_->clear_params(); } - void parse_params(std::string_view query) { + inline void parse_params(std::string_view query) { assert(ptr_); ptr_->parse_params(query); } @@ -750,6 +756,8 @@ inline std::string url_search_params::to_string() const { return query; } +UPA_EXPORT_BEGIN + // Non-member functions /// @brief Performs stream output on URL search parameters @@ -777,6 +785,7 @@ inline void swap(url_search_params& lhs, url_search_params& rhs) noexcept { lhs.swap(rhs); } +UPA_EXPORT_END } // namespace upa diff --git a/include/upa/url_utf.h b/include/upa/url_utf.h index 37a5bd21..a5e2fe4c 100644 --- a/include/upa/url_utf.h +++ b/include/upa/url_utf.h @@ -11,10 +11,12 @@ #include "config.h" // IWYU pragma: export #include "url_result.h" -#include // uint8_t, uint32_t -#include -#include +#ifndef UPA_MODULE +# include // uint8_t, uint32_t +# include +# include +#endif // UPA_MODULE namespace upa { @@ -49,7 +51,9 @@ class url_utf { private: // Replacement character (U+FFFD) static constexpr std::string_view kReplacementCharUtf8{ "\xEF\xBF\xBD" }; +}; +namespace detail { // Following two arrays have values from corresponding macros in ICU 74.1 library's // include\unicode\utf8.h file. @@ -57,17 +61,23 @@ class url_utf { // Each bit indicates whether one lead byte + first trail byte pair starts a valid sequence. // Lead byte E0..EF bits 3..0 are used as byte index, // first trail byte bits 7..5 are used as bit index into that byte. - static constexpr std::uint8_t k_U8_LEAD3_T1_BITS[16] = { + inline constexpr std::uint8_t k_U8_LEAD3_T1_BITS[16] = { 0x20, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x10, 0x30, 0x30 }; // Internal bit vector for 4-byte UTF-8 validity check, for use in U8_IS_VALID_LEAD4_AND_T1. // Each bit indicates whether one lead byte + first trail byte pair starts a valid sequence. // First trail byte bits 7..4 are used as byte index, // lead byte F0..F4 bits 2..0 are used as bit index into that byte. - static constexpr std::uint8_t k_U8_LEAD4_T1_BITS[16] = { + inline constexpr std::uint8_t k_U8_LEAD4_T1_BITS[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x0F, 0x0F, 0x0F, 0x00, 0x00, 0x00, 0x00 }; -}; + + // For use with append_utf8 function. It appends a byte to output string. + template + UPA_CONSTEXPR_20 void append_to_string(std::uint8_t c, std::basic_string& str) { + str.push_back(static_cast(c)); + }; +} // namespace detail // The URL class (https://url.spec.whatwg.org/#url-class) in URL Standard uses @@ -97,13 +107,6 @@ constexpr detail::result_value url_utf::read_utf_char(const CharT return { false, 0xFFFD }; // REPLACEMENT CHARACTER } -namespace detail { - template - UPA_CONSTEXPR_20 void append_to_string(std::uint8_t c, std::basic_string& str) { - str.push_back(static_cast(c)); - }; -} // namespace detail - template UPA_CONSTEXPR_20 void url_utf::read_char_append_utf8(const CharT*& it, const CharT* last, std::string& output) { const std::uint32_t code_point = read_utf_char(it, last).value; @@ -140,11 +143,11 @@ constexpr bool url_utf::read_code_point(const char*& first, const char* last, st // fetch/validate/assemble all but last trail byte (c >= 0xE0 ? (c < 0xF0 ? // U+0800..U+FFFF except surrogates - k_U8_LEAD3_T1_BITS[c &= 0xF] & (1 << ((tmp = static_cast(*first)) >> 5)) && + detail::k_U8_LEAD3_T1_BITS[c &= 0xF] & (1 << ((tmp = static_cast(*first)) >> 5)) && (tmp &= 0x3F, 1) : // U+10000..U+10FFFF (c -= 0xF0) <= 4 && - k_U8_LEAD4_T1_BITS[(tmp = static_cast(*first)) >> 4] & (1 << c) && + detail::k_U8_LEAD4_T1_BITS[(tmp = static_cast(*first)) >> 4] & (1 << c) && (c = (c << 6) | (tmp & 0x3F), ++first != last) && (tmp = static_cast(static_cast(*first) - 0x80)) <= 0x3F) && // valid second-to-last trail byte diff --git a/include/upa/urlpattern.h b/include/upa/urlpattern.h index 1735371c..d480c63c 100644 --- a/include/upa/urlpattern.h +++ b/include/upa/urlpattern.h @@ -8,19 +8,21 @@ #include "url.h" // NOLINT(llvm-include-order) #include "unicode_id.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#ifndef UPA_MODULE +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#endif // UPA_MODULE namespace upa { namespace pattern { @@ -132,6 +134,8 @@ struct has_regex_engine_members constexpr bool is_regex_engine_v = std::is_default_constructible_v @@ -177,7 +181,7 @@ struct urlpattern_init { /// @brief Get value of member by name /// @param name Name of the member to get /// @return Value of the member if found, `std::nullopt` otherwise - [[nodiscard]] std::optional get(std::string_view name) const { + [[nodiscard]] inline std::optional get(std::string_view name) const { if (auto ptr = get_member(name)) return this->*ptr; return std::nullopt; @@ -187,7 +191,7 @@ struct urlpattern_init { /// @param name Name of the member to set /// @param value Value to set, must be assignable to std::string template , int> = 0> - void set(std::string_view name, T&& value) { + inline void set(std::string_view name, T&& value) { if (auto ptr = get_member(name)) this->*ptr = std::forward(value); } @@ -198,6 +202,8 @@ struct urlpattern_init { get_member(std::string_view name); }; +UPA_EXPORT_END + namespace pattern { // 1.6. Constructor string parsing @@ -256,7 +262,6 @@ struct token { type type_; std::size_t index_; std::string_view value_; - }; // https://urlpattern.spec.whatwg.org/#token-list @@ -447,6 +452,8 @@ constexpr bool hostname_pattern_is_ipv6_address(std::string_view input) noexcept } // namespace pattern +UPA_EXPORT_BEGIN + /////////////////////////////////////////////////////////////////////// // 1.2. The URLPattern class // https://urlpattern.spec.whatwg.org/#urlpattern-class @@ -651,7 +658,7 @@ class urlpattern { /// @param[in] opt optional `upa::urlpattern_options` struct template = 0, upa::enable_if_optional_str_arg_t = 0> - urlpattern(const T& input, TB&& base_url, urlpattern_options opt = {}) + inline urlpattern(const T& input, TB&& base_url, urlpattern_options opt = {}) : urlpattern{ make_urlpattern_init(input, std::forward(base_url)), opt } {} /// @brief Constructs urlpattern object from URL pattern string @@ -667,7 +674,7 @@ class urlpattern { /// @param[in] input URL pattern string /// @param[in] opt optional `upa::urlpattern_options` struct template = 0> - urlpattern(const T& input, urlpattern_options opt = {}) + inline urlpattern(const T& input, urlpattern_options opt = {}) : urlpattern{ make_urlpattern_init(input, std::nullopt), opt } {} /// @brief Test whether URL pattern matches the input @@ -691,7 +698,7 @@ class urlpattern { /// `false` otherwise template = 0, upa::enable_if_optional_str_arg_t = 0> - [[nodiscard]] bool test(const T& input, const TB& base_url_str = std::nullopt) const; + [[nodiscard]] bool test(const T& input, const TB& base_url_str = upa::nullopt) const; /// @brief Test whether URL pattern matches the URL. /// @param[in] url URL to test @@ -754,7 +761,7 @@ class urlpattern { std::enable_if_t, int> = 0, upa::enable_if_str_arg_t = 0, upa::enable_if_optional_str_arg_t = 0> [[nodiscard]] std::optional exec(const T& input, - const TB& base_url_str = std::nullopt) const; + const TB& base_url_str = upa::nullopt) const; /// @brief Executes the URL pattern against the URL /// @@ -842,16 +849,18 @@ class urlpattern { /// @brief urlpattern exception class /// /// This exception can be thrown by the `upa::urlpattern` constructor in the case of an error. -class urlpattern_error : public std::runtime_error { +class UPA_SO_VISIBLE urlpattern_error : public std::runtime_error { public: /// constructs a new urlpattern_error object with the given error message /// /// @param[in] what_arg error message - explicit urlpattern_error(const char* what_arg) + inline explicit urlpattern_error(const char* what_arg) : std::runtime_error(what_arg) {} }; +UPA_EXPORT_END + /////////////////////////////////////////////////////////////////////// // 1.2. The URLPattern class // https://urlpattern.spec.whatwg.org/#urlpattern-class diff --git a/include/upa/util.h b/include/upa/util.h index b049975a..3d5e15e6 100644 --- a/include/upa/util.h +++ b/include/upa/util.h @@ -7,17 +7,20 @@ #define UPA_UTIL_H #include "config.h" -#include -#include -#include -#include -#ifdef __cpp_lib_start_lifetime_as -# include -#endif -#include -#include -#include -#include + +#ifndef UPA_MODULE +# include +# include +# include +# include +# ifdef __cpp_lib_start_lifetime_as +# include +# endif +# include +# include +# include +# include +#endif // UPA_MODULE namespace upa::util { diff --git a/src/module/upa.url.cpp b/src/module/upa.url.cpp new file mode 100644 index 00000000..bd7e8524 --- /dev/null +++ b/src/module/upa.url.cpp @@ -0,0 +1,29 @@ +// Copyright 2026 Rimas Misevičius +// Distributed under the BSD-style license that can be +// found in the LICENSE file. +// +module; + +#include "upa.url.h" + +// Macros +#include "upa/config.h" +#include "upa/url_version.h" + +#define UPA_IDNA_CPP_20 +#define UPA_IDNA_CONSTEXPR_20 constexpr + +module upa.url; + +#if (UPA_IMPORT_STD) +import std; +#endif + +#include "../idna.cpp" +#include "../url.cpp" +#include "../url_ip.cpp" +#include "../url_search_params.cpp" +#include "../url_utf.cpp" +#include "../urlpattern.cpp" +#include "../unicode_id.cpp" +#include "../public_suffix_list.cpp" diff --git a/src/module/upa.url.cppm b/src/module/upa.url.cppm new file mode 100644 index 00000000..b6de712d --- /dev/null +++ b/src/module/upa.url.cppm @@ -0,0 +1,18 @@ +// Copyright 2026 Rimas Misevičius +// Distributed under the BSD-style license that can be +// found in the LICENSE file. +// +module; + +#include "upa.url.h" + +export module upa.url; + +#if (UPA_IMPORT_STD) +import std; +#endif + +#include "upa/idna.h" +#include "upa/url.h" +#include "upa/urlpattern.h" +#include "upa/public_suffix_list.h" diff --git a/src/module/upa.url.h b/src/module/upa.url.h new file mode 100644 index 00000000..81de02f7 --- /dev/null +++ b/src/module/upa.url.h @@ -0,0 +1,43 @@ +// Copyright 2026 Rimas Misevičius +// Distributed under the BSD-style license that can be +// found in the LICENSE file. +// +#define UPA_MODULE +#define UPA_EXPORT export +#define UPA_EXPORT_BEGIN export { +#define UPA_EXPORT_END } + +#include +#include + +#ifndef UPA_IMPORT_STD +# define UPA_IMPORT_STD 0 +#endif + +#if !(UPA_IMPORT_STD) +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#endif diff --git a/src/public_suffix_list.cpp b/src/public_suffix_list.cpp index 433e9187..dc0743f0 100644 --- a/src/public_suffix_list.cpp +++ b/src/public_suffix_list.cpp @@ -1,11 +1,13 @@ -// Copyright 2024-2025 Rimas Misevičius +// Copyright 2024-2026 Rimas Misevičius // Distributed under the BSD-style license that can be // found in the LICENSE file. // // Formal algorithm: // https://github.com/publicsuffix/list/wiki/Format#formal-algorithm // -#include "upa/public_suffix_list.h" +#ifndef UPA_MODULE +# include "upa/public_suffix_list.h" +#endif // UPA_MODULE namespace upa { namespace { diff --git a/src/unicode_id.cpp b/src/unicode_id.cpp index 536d84a9..e36f3ed4 100644 --- a/src/unicode_id.cpp +++ b/src/unicode_id.cpp @@ -1,10 +1,12 @@ -// Copyright 2023-2025 Rimas Misevičius +// Copyright 2023-2026 Rimas Misevičius // Distributed under the BSD-style license that can be // found in the LICENSE file. // -#include "upa/unicode_id.h" -#include -#include +#ifndef UPA_MODULE +# include "upa/unicode_id.h" +# include +# include +#endif // UPA_MODULE namespace upa::pattern::table { namespace { diff --git a/src/url.cpp b/src/url.cpp index 143cfd04..87d74e06 100644 --- a/src/url.cpp +++ b/src/url.cpp @@ -2,8 +2,9 @@ // Distributed under the BSD-style license that can be // found in the LICENSE file. // - -#include "upa/url.h" +#ifndef UPA_MODULE +# include "upa/url.h" +#endif // UPA_MODULE namespace upa { namespace detail { diff --git a/src/url_ip.cpp b/src/url_ip.cpp index 37660c15..70cd1ca0 100644 --- a/src/url_ip.cpp +++ b/src/url_ip.cpp @@ -2,9 +2,10 @@ // Distributed under the BSD-style license that can be // found in the LICENSE file. // - -#include "upa/url_ip.h" -#include "upa/util.h" +#ifndef UPA_MODULE +# include "upa/url_ip.h" +# include "upa/util.h" +#endif // UPA_MODULE namespace upa { diff --git a/src/url_search_params.cpp b/src/url_search_params.cpp index b4f2a653..655040c6 100644 --- a/src/url_search_params.cpp +++ b/src/url_search_params.cpp @@ -2,10 +2,10 @@ // Distributed under the BSD-style license that can be // found in the LICENSE file. // - -#include "upa/url_percent_encode.h" -#include "upa/url_search_params.h" - +#ifndef UPA_MODULE +# include "upa/url_percent_encode.h" +# include "upa/url_search_params.h" +#endif // UPA_MODULE namespace upa { namespace { diff --git a/src/url_utf.cpp b/src/url_utf.cpp index ff39160a..2c47e813 100644 --- a/src/url_utf.cpp +++ b/src/url_utf.cpp @@ -2,9 +2,10 @@ // Distributed under the BSD-style license that can be // found in the LICENSE file. // - -#include "upa/url_utf.h" -#include +#ifndef UPA_MODULE +# include "upa/url_utf.h" +# include +#endif // UPA_MODULE namespace upa { diff --git a/src/urlpattern.cpp b/src/urlpattern.cpp index 136a1e42..a16faf28 100644 --- a/src/urlpattern.cpp +++ b/src/urlpattern.cpp @@ -1,16 +1,17 @@ -// Copyright 2025 Rimas Misevičius +// Copyright 2025-2026 Rimas Misevičius // Distributed under the BSD-style license that can be // found in the LICENSE file. // -#include "upa/urlpattern.h" - -#include -#include -#include -#include -#include -#include -#include +#ifndef UPA_MODULE +# include "upa/urlpattern.h" +# include +# include +# include +# include +# include +# include +# include +#endif // UPA_MODULE namespace upa { namespace { diff --git a/test/test-module.cpp b/test/test-module.cpp new file mode 100644 index 00000000..50831066 --- /dev/null +++ b/test/test-module.cpp @@ -0,0 +1,142 @@ +// Copyright 2026 Rimas Misevičius +// Distributed under the BSD-style license that can be +// found in the LICENSE file. +// +#include "doctest-main.h" + +#include "upa/regex_engine_srell.h" + +#include + +import upa.url; + +// Custom string class and it's specialization + +template +class CustomString { +public: + CustomString(const CharT* data, std::size_t length) + : data_(data), length_(length) {} + const CharT* GetData() const { return data_; } + std::size_t GetLength() const { return length_; } +private: + const CharT* data_ = nullptr; + std::size_t length_ = 0; +}; + +namespace upa { + +template +struct str_arg_char> { + using type = CharT; + + static str_arg to_str_arg(const CustomString& str) { + return { str.GetData(), str.GetLength() }; + } +}; + +} // namespace upa + +TEST_CASE("upa::url") { + upa::url url("https://user:psw@example.com:1234/p/a?q=Q#f"); + + CHECK(url.protocol() == "https:"); + CHECK(url.username() == "user"); + CHECK(url.password() == "psw"); + CHECK(url.host() == "example.com:1234"); + CHECK(url.hostname() == "example.com"); + CHECK(url.port_int() == 1234); + CHECK(url.pathname() == "/p/a"); + CHECK(url.search() == "?q=Q"); + CHECK(url.hash() == "#f"); + + CHECK(url.pathname("../path")); + CHECK(url.pathname() == "/path"); + + upa::url url2{ url }; + CHECK(url2.href() == url.href()); + + CHECK(upa::url::can_parse("about:blank")); + + // Custom string + CHECK(upa::success(url.parse(CustomString("about:blank", 11)))); + CHECK(url.href() == "about:blank"); +} + +TEST_CASE("std::unordered_set") { + std::unordered_set aibe; + aibe.emplace(upa::url{ "about:blank" }); + aibe.emplace(upa::url{ "file:///path" }); + aibe.emplace(upa::url{ "https://example.org/" }); + + CHECK_FALSE(aibe.contains(upa::url{ "about:about" })); + CHECK(aibe.contains(upa::url{ "about:blank" })); +} + +TEST_CASE("upa::url_search_params") { + constexpr auto get_eq = + [](upa::url_search_params& usp, const char* name, const char* value) { + return usp.get(name) && *usp.get(name) == value; + }; + + upa::url_search_params usp{ "a=2&c=1" }; + + CHECK(usp.size() == 2); + CHECK(get_eq(usp, "a", "2")); + CHECK(get_eq(usp, "c", "1")); + CHECK_FALSE(usp.has("b")); + usp.append("b", "3"); + CHECK(usp.size() == 3); + CHECK(get_eq(usp, "b", "3")); + usp.set("a", "4"); + CHECK(get_eq(usp, "a", "4")); + usp.remove("c"); + CHECK_FALSE(usp.has("c")); +} + +TEST_CASE("upa::url_host") { + upa::url_host host("www.example.com"); + CHECK(host.to_string() == "www.example.com"); + CHECK(host.type() == upa::HostType::Domain); + + upa::url_host host2{ host }; + CHECK(host2.to_string() == "www.example.com"); +} + +TEST_CASE("upa::public_suffix_list") { + upa::public_suffix_list psl; + upa::public_suffix_list::push_context ctx; + psl.push_line(ctx, "co.uk"); + psl.finalize(ctx); + + const auto registrable_domain = upa::public_suffix_list::option::registrable_domain; + CHECK(psl.get_suffix_view("www.example.co.uk", registrable_domain) == "example.co.uk"); + + upa::public_suffix_list psl2{ std::move(psl) }; + CHECK(psl2.get_suffix_view("www.miestas.co.uk", registrable_domain) == "miestas.co.uk"); + + // Invalid hostname + CHECK(psl2.get_suffix_view("a^b.co.uk") == ""); +} + +TEST_CASE("upa::urlpattern") { + upa::urlpattern urlp{ "https://*.example.com/*" }; + + CHECK(urlp.test("https://www.example.com/path")); + CHECK_FALSE(urlp.test("https://example.com/#1")); +} + +TEST_CASE("upa::urlpattern_init") { + upa::urlpattern_init urlp_init; + + urlp_init.protocol = upa::make_string(std::string("http:")); + urlp_init.hostname = upa::make_string("*.lt"); + urlp_init.port = upa::make_string(L"80"); + urlp_init.pathname = upa::make_string(u"/*"); + urlp_init.set("search", upa::make_string(U"q=*")); + urlp_init.set("hash", "h"); + + upa::urlpattern urlp{ urlp_init }; + CHECK(urlp.test("http://www.example.lt/path?q=1#h")); + CHECK_FALSE(urlp.test("http://www.example.lt/path")); +}