diff --git a/libmamba/CMakeLists.txt b/libmamba/CMakeLists.txt index 584d1e4f7..a70eed554 100644 --- a/libmamba/CMakeLists.txt +++ b/libmamba/CMakeLists.txt @@ -142,6 +142,7 @@ set(LIBMAMBA_SOURCES ${LIBMAMBA_SOURCE_DIR}/specs/conda_url.cpp ${LIBMAMBA_SOURCE_DIR}/specs/version.cpp ${LIBMAMBA_SOURCE_DIR}/specs/version_spec.cpp + ${LIBMAMBA_SOURCE_DIR}/specs/channel_spec.cpp ${LIBMAMBA_SOURCE_DIR}/specs/repo_data.cpp # Core API (low-level) ${LIBMAMBA_SOURCE_DIR}/core/singletons.cpp @@ -236,6 +237,7 @@ set(LIBMAMBA_PUBLIC_HEADERS ${LIBMAMBA_INCLUDE_DIR}/mamba/specs/conda_url.hpp ${LIBMAMBA_INCLUDE_DIR}/mamba/specs/version.hpp ${LIBMAMBA_INCLUDE_DIR}/mamba/specs/version_spec.hpp + ${LIBMAMBA_INCLUDE_DIR}/mamba/specs/channel_spec.hpp ${LIBMAMBA_INCLUDE_DIR}/mamba/specs/repo_data.hpp # Core API (low-level) ${LIBMAMBA_INCLUDE_DIR}/mamba/core/activation.hpp diff --git a/libmamba/include/mamba/core/channel.hpp b/libmamba/include/mamba/core/channel.hpp index 1816e8679..bea6f975a 100644 --- a/libmamba/include/mamba/core/channel.hpp +++ b/libmamba/include/mamba/core/channel.hpp @@ -13,9 +13,9 @@ #include #include #include -#include #include "mamba/specs/conda_url.hpp" +#include "mamba/util/flat_set.hpp" namespace mamba { @@ -25,6 +25,10 @@ namespace mamba { class RepoChecker; } + namespace specs + { + class ChannelSpec; + } std::vector get_known_platforms(); @@ -40,11 +44,11 @@ namespace mamba ~Channel(); - const std::string& scheme() const; + std::string_view scheme() const; const std::string& location() const; const std::string& name() const; const std::string& canonical_name() const; - const std::vector& platforms() const; + const util::flat_set& platforms() const; std::optional auth() const; std::optional user() const; std::optional password() const; @@ -56,9 +60,9 @@ namespace mamba std::string base_url() const; std::string platform_url(std::string platform, bool with_credential = true) const; // The pairs consist of (platform,url) - std::vector> + util::flat_set> platform_urls(bool with_credential = true) const; - std::vector urls(bool with_credential = true) const; + util::flat_set urls(bool with_credential = true) const; private: @@ -70,14 +74,25 @@ namespace mamba std::string_view user = {}, std::string_view password = {}, std::string_view token = {}, - std::string_view package_filename = {} + std::string_view package_filename = {}, + util::flat_set platforms = {} ); + Channel( + specs::CondaURL url, + std::string location, + std::string name, + std::string canonical_name, + util::flat_set platforms = {} + ); + + const specs::CondaURL& url() const; + specs::CondaURL m_url; std::string m_location; std::string m_name; std::string m_canonical_name; - std::vector m_platforms; + util::flat_set m_platforms; // This is used to make sure that there is a unique repo for every channel mutable std::unique_ptr p_repo_checker; @@ -116,7 +131,7 @@ namespace mamba const Channel& make_channel(const std::string& value); std::vector get_channels(const std::vector& channel_names); - const Channel& get_channel_alias() const; + const specs::CondaURL& get_channel_alias() const; const channel_map& get_custom_channels() const; Context& context() const @@ -128,7 +143,7 @@ namespace mamba Context& m_context; ChannelCache m_channel_cache; - Channel m_channel_alias; + specs::CondaURL m_channel_alias; channel_map m_custom_channels; multichannel_map m_custom_multichannels; @@ -137,16 +152,18 @@ namespace mamba const multichannel_map& get_custom_multichannels() const; Channel make_simple_channel( - const Channel& channel_alias, + const specs::CondaURL& channel_alias, const std::string& channel_url, const std::string& channel_name, const std::string& channel_canonical_name ); - Channel from_url(std::string_view url); + Channel from_any_path(specs::ChannelSpec&& spec); + Channel from_package_path(specs::ChannelSpec&& spec); + Channel from_path(specs::ChannelSpec&& spec); + Channel from_url(specs::ChannelSpec&& spec); Channel from_name(const std::string& name); Channel from_value(const std::string& value); - Channel from_alias(std::string_view alias); }; } // namespace mamba diff --git a/libmamba/include/mamba/fs/filesystem.hpp b/libmamba/include/mamba/fs/filesystem.hpp index a36e6f59d..a23bc316b 100644 --- a/libmamba/include/mamba/fs/filesystem.hpp +++ b/libmamba/include/mamba/fs/filesystem.hpp @@ -377,11 +377,21 @@ namespace mamba::fs return m_path.extension(); } + u8path lexically_normal() const + { + return m_path.lexically_normal(); + } + u8path lexically_relative(const u8path& base) const { return m_path.lexically_relative(base); } + u8path lexically_proximate(const u8path& base) const + { + return m_path.lexically_proximate(base); + } + //---- Modifiers ---- void clear() noexcept diff --git a/libmamba/include/mamba/specs/channel_spec.hpp b/libmamba/include/mamba/specs/channel_spec.hpp new file mode 100644 index 000000000..a9ab747d0 --- /dev/null +++ b/libmamba/include/mamba/specs/channel_spec.hpp @@ -0,0 +1,95 @@ +// Copyright (c) 2023, QuantStack and Mamba Contributors +// +// Distributed under the terms of the BSD 3-Clause License. +// +// The full license is in the file LICENSE, distributed with this software. + +#ifndef MAMBA_SPECS_CHANNEL_SPEC_HPP +#define MAMBA_SPECS_CHANNEL_SPEC_HPP + +#include +#include + +#include "mamba/util/flat_set.hpp" + +namespace mamba::specs +{ + /** + * Channel specification. + * + * This represent the string that is passed by the user to select a channel. + * It needs to be resolved in order to get a final URL/path. + * This is even true when a full URL or path is given, as some authentification information + * may come from login database. + * + * Note that for a string to be considered a URL, it must have an explicit scheme. + * So "repo.anaconda.com" is considered a name, similarily to "conda-forge" and not a URL. + * This is because otherwise it is not possible to tell names and URL appart. + */ + class ChannelSpec + { + public: + + enum class Type + { + /** + * A URL to a full repo strucuture. + * + * Example "https://repo.anaconda.com/conda-forge". + */ + URL, + /** + * A URL to a single package. + * + * Example "https://repo.anaconda.com/conda-forge/linux-64/pkg-0.0-bld.conda". + */ + PackageURL, + /** + * An (possibly implicit) path to a full repo strucuture. + * + * Example "/Users/name/conda-bld", "./conda-bld", "~/.conda-bld". + */ + Path, + /** + * An (possibly implicit) path to a single-package. + * + * Example "/tmp/pkg-0.0-bld.conda", "./pkg-0.0-bld.conda", "~/pkg-0.0-bld.tar.bz2". + */ + PackagePath, + /** + * A relative name. + * + * It needs to be resolved using a channel alias, or a custom channel. + * Example "conda-forge", "locals", "my-channel/my-label". + */ + Name, + }; + + static constexpr std::string_view default_name = "defaults"; + static constexpr std::string_view platform_separators = "|,;"; + + using dynamic_platform_set = util::flat_set; + + [[nodiscard]] static auto parse(std::string_view str) -> ChannelSpec; + + ChannelSpec() = default; + ChannelSpec(std::string location, dynamic_platform_set filters, Type type); + + [[nodiscard]] auto type() const -> Type; + + [[nodiscard]] auto location() const& -> const std::string&; + [[nodiscard]] auto location() && -> std::string; + [[nodiscard]] auto clear_location() -> std::string; + + [[nodiscard]] auto platform_filters() const& -> const dynamic_platform_set&; + [[nodiscard]] auto platform_filters() && -> dynamic_platform_set; + [[nodiscard]] auto clear_platform_filters() -> dynamic_platform_set; + + private: + + std::string m_location = std::string(default_name); + dynamic_platform_set m_platform_filters = {}; + Type m_type = {}; + }; +} +#endif diff --git a/libmamba/include/mamba/specs/conda_url.hpp b/libmamba/include/mamba/specs/conda_url.hpp index 8385542ae..938d8ef4f 100644 --- a/libmamba/include/mamba/specs/conda_url.hpp +++ b/libmamba/include/mamba/specs/conda_url.hpp @@ -21,7 +21,7 @@ namespace mamba::specs public: using StripScheme = util::detail::StripScheme; - using HideConfidential = util::detail::HideConfidential; + using Credentials = util::detail::Credentials; using Encode = util::detail::Encode; using Decode = util::detail::Decode; @@ -34,8 +34,10 @@ namespace mamba::specs explicit CondaURL(util::URL&& url); explicit CondaURL(const util::URL& url); + using Base::scheme_is_defaulted; using Base::scheme; using Base::set_scheme; + using Base::clear_scheme; using Base::user; using Base::set_user; using Base::clear_user; @@ -43,6 +45,7 @@ namespace mamba::specs using Base::set_password; using Base::clear_password; using Base::authentication; + using Base::host_is_defaulted; using Base::host; using Base::set_host; using Base::clear_host; @@ -52,9 +55,7 @@ namespace mamba::specs using Base::authority; using Base::path; using Base::pretty_path; - using Base::set_path; using Base::clear_path; - using Base::append_path; using Base::query; using Base::set_query; using Base::clear_query; @@ -62,6 +63,42 @@ namespace mamba::specs using Base::set_fragment; using Base::clear_fragment; + /** + * Set the path from a not encoded value. + * + * All '/' are not encoded but interpreted as separators. + * On windows with a file scheme, the colon after the drive letter is not encoded. + * A leading '/' is added if abscent. + * If the path contains only a token, a trailing '/' is added afterwards. + */ + void set_path(std::string_view path, Encode::yes_type = Encode::yes); + + /** Set the path from an already encoded value. + * + * A leading '/' is added if abscent. + * If the path contains only a token, a trailing '/' is added afterwards. + */ + void set_path(std::string path, Encode::no_type); + + /** + * Append a not encoded sub path to the current path. + * + * Contrary to `std::filesystem::path::append`, this always append and never replace + * the current path, even if @p subpath starts with a '/'. + * All '/' are not encoded but interpreted as separators. + * If the final path contains only a token, a trailing '/' is added afterwards. + */ + void append_path(std::string_view path, Encode::yes_type = Encode::yes); + + /** + * Append a already encoded sub path to the current path. + * + * Contrary to `std::filesystem::path::append`, this always append and never replace + * the current path, even if @p subpath starts with a '/'. + * If the final path contains only a token, a trailing '/' is added afterwards. + */ + void append_path(std::string_view path, Encode::no_type); + /** Return the Conda token, as delimited with "/t/", or empty if there isn't any. */ [[nodiscard]] auto token() const -> std::string_view; @@ -76,6 +113,29 @@ namespace mamba::specs /** Clear the token and return ``true`` if it exists, otherwise return ``false``. */ auto clear_token() -> bool; + /** Return the encoded part of the path without any Conda token, always start with '/'. */ + [[nodiscard]] auto path_without_token(Decode::no_type) const -> std::string_view; + + /** Return the decoded part of the path without any Conda token, always start with '/'. */ + [[nodiscard]] auto path_without_token(Decode::yes_type = Decode::yes) const -> std::string; + + /** + * Set the path from an already encoded value, without changing the Conda token. + * + * A leading '/' is added if abscent. + */ + void set_path_without_token(std::string_view path, Encode::no_type); + + /** + * Set the path from an not yet encoded value, without changing the Conda token. + * + * A leading '/' is added if abscent. + */ + void set_path_without_token(std::string_view path, Encode::yes_type = Encode::yes); + + /** Clear the path without changing the Conda token and return ``true`` if it exists. */ + auto clear_path_without_token() -> bool; + /** Return the platform if part of the URL path. */ [[nodiscard]] auto platform() const -> std::optional; @@ -138,7 +198,8 @@ namespace mamba::specs /** Clear the package and return true if it exists, otherwise return ``false``. */ auto clear_package() -> bool; - using Base::str; + /** Return the full, exact, encoded URL. */ + [[nodiscard]] auto str(Credentials credentials = Credentials::Show) const -> std::string; /** * Return the full decoded url. @@ -147,18 +208,20 @@ namespace mamba::specs * asset. * @param strip_scheme If true, remove the scheme and "localhost" on file URI. * @param rstrip_path If non-null, remove the given charaters at the end of the path. - * @param hide_confidential If true, hide password and tokens in the decoded string. + * @param credentials If true, hide password and tokens in the decoded string. + * @param credentials Decide to keep, remove, or hide passwrd, users, and token. */ [[nodiscard]] auto pretty_str( StripScheme strip_scheme = StripScheme::no, char rstrip_path = 0, - HideConfidential hide_confidential = HideConfidential::no + Credentials credentials = Credentials::Show ) const -> std::string; private: void set_platform_no_check_input(std::string_view platform); + void ensure_path_without_token_leading_slash(); friend auto operator==(const CondaURL&, const CondaURL&) -> bool; }; diff --git a/libmamba/include/mamba/util/path_manip.hpp b/libmamba/include/mamba/util/path_manip.hpp index 1d04ecf43..6dfc6bbc2 100644 --- a/libmamba/include/mamba/util/path_manip.hpp +++ b/libmamba/include/mamba/util/path_manip.hpp @@ -45,5 +45,11 @@ namespace mamba::util * Convert the Windows path separators to Posix ones on Windows only. */ [[nodiscard]] auto path_to_posix(std::string path) -> std::string; + + /** + * Check that a path is a prefix of another path. + */ + [[nodiscard]] auto + path_is_prefix(std::string_view parent, std::string_view child, char sep = '/') -> bool; } #endif diff --git a/libmamba/include/mamba/util/string.hpp b/libmamba/include/mamba/util/string.hpp index 842b85b0a..7ebf41950 100644 --- a/libmamba/include/mamba/util/string.hpp +++ b/libmamba/include/mamba/util/string.hpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -107,12 +106,26 @@ namespace mamba::util std::string_view remove_prefix(std::string_view str, std::string_view prefix); std::string_view remove_prefix(std::string_view str, std::string_view::value_type c); + /** + * Return a view to prefix if present, and a view to the rest of the input. + */ + std::array split_prefix(std::string_view str, std::string_view prefix); + std::array + split_prefix(std::string_view str, std::string_view::value_type c); + /** * Return a view to the input without the suffix if present. */ std::string_view remove_suffix(std::string_view str, std::string_view suffix); std::string_view remove_suffix(std::string_view str, std::string_view::value_type c); + /** + * Return a view to the head of the input, and a view to suffix if present. + */ + std::array split_suffix(std::string_view str, std::string_view suffix); + std::array + split_suffix(std::string_view str, std::string_view::value_type c); + std::string_view lstrip(std::string_view input, char c); std::wstring_view lstrip(std::wstring_view input, wchar_t c); std::string_view lstrip(std::string_view input, std::string_view chars); diff --git a/libmamba/include/mamba/util/url.hpp b/libmamba/include/mamba/util/url.hpp index 4cbb314f4..b9cc9080d 100644 --- a/libmamba/include/mamba/util/url.hpp +++ b/libmamba/include/mamba/util/url.hpp @@ -7,6 +7,7 @@ #ifndef MAMBA_UTIL_URL_HPP #define MAMBA_UTIL_URL_HPP +#include #include #include @@ -22,10 +23,11 @@ namespace mamba::util yes }; - enum class HideConfidential : bool + enum class Credentials { - no, - yes + Show, + Hide, + Remove, }; struct Encode @@ -59,7 +61,7 @@ namespace mamba::util public: using StripScheme = detail::StripScheme; - using HideConfidential = detail::HideConfidential; + using Credentials = detail::Credentials; using Encode = detail::Encode; using Decode = detail::Decode; @@ -84,12 +86,18 @@ namespace mamba::util /** Create a local URL. */ URL() = default; + /** Return whether the scheme is defaulted, i.e. not explicitly set. */ + [[nodiscard]] auto scheme_is_defaulted() const -> bool; + /** Return the scheme, always non-empty. */ - [[nodiscard]] auto scheme() const -> const std::string&; + [[nodiscard]] auto scheme() const -> std::string_view; /** Set a non-empty scheme. */ void set_scheme(std::string_view scheme); + /** Clear the scheme back to a defaulted value and return the old value. */ + auto clear_scheme() -> std::string; + /** Return the encoded user, or empty if none. */ [[nodiscard]] auto user(Decode::no_type) const -> const std::string&; @@ -123,6 +131,9 @@ namespace mamba::util /** Return the encoded basic authentication string. */ [[nodiscard]] auto authentication() const -> std::string; + /** Return whether the host was defaulted, i.e. not explicitly set. */ + [[nodiscard]] auto host_is_defaulted() const -> bool; + /** Return the encoded host, always non-empty except for file scheme. */ [[nodiscard]] auto host(Decode::no_type) const -> std::string_view; @@ -148,7 +159,7 @@ namespace mamba::util auto clear_port() -> std::string; /** Return the encoded autority part of the URL. */ - [[nodiscard]] auto authority() const -> std::string; + [[nodiscard]] auto authority(Credentials = Credentials::Show) const -> std::string; /** Return the encoded path, always starts with a '/'. */ [[nodiscard]] auto path(Decode::no_type) const -> const std::string&; @@ -215,32 +226,41 @@ namespace mamba::util auto clear_fragment() -> std::string; /** Return the full, exact, encoded URL. */ - [[nodiscard]] auto str() const -> std::string; + [[nodiscard]] auto str(Credentials credentials = Credentials::Show) const -> std::string; /** * Return the full decoded url. * - * Due to decoding, the outcome may not be understood by parser and usable to reach an - * asset. + * Due to decoding, the outcome may not be understood by parser and usable to fetch the URL. * @param strip_scheme If true, remove the scheme and "localhost" on file URI. * @param rstrip_path If non-null, remove the given charaters at the end of the path. - * @param hide_confidential If true, hide password in the decoded string. + * @param credentials Decide to keep, remove, or hide credentials. */ [[nodiscard]] auto pretty_str( StripScheme strip_scheme = StripScheme::no, char rstrip_path = 0, - HideConfidential hide_confidential = HideConfidential::no + Credentials credentials = Credentials::Show ) const -> std::string; protected: + [[nodiscard]] auto authentication_elems(Credentials, Decode::no_type) const + -> std::array; + [[nodiscard]] auto authentication_elems(Credentials, Decode::yes_type) const + -> std::array; + + [[nodiscard]] auto authority_elems(Credentials, Decode::no_type) const + -> std::array; + [[nodiscard]] auto authority_elems(Credentials, Decode::yes_type) const + -> std::array; + [[nodiscard]] auto pretty_str_path(StripScheme strip_scheme = StripScheme::no, char rstrip_path = 0) const -> std::string; private: - std::string m_scheme = std::string(https); + std::string m_scheme = {}; std::string m_user = {}; std::string m_password = {}; std::string m_host = {}; diff --git a/libmamba/include/mamba/util/url_manip.hpp b/libmamba/include/mamba/util/url_manip.hpp index dd0f114cb..11fa1d43a 100644 --- a/libmamba/include/mamba/util/url_manip.hpp +++ b/libmamba/include/mamba/util/url_manip.hpp @@ -73,6 +73,13 @@ namespace mamba::util */ [[nodiscard]] auto abs_path_to_url(std::string_view path) -> std::string; + /** + * Transform an absolute path to a %-encoded "file://" URL. + * + * Does nothing if the input is already has a URL scheme. + */ + [[nodiscard]] auto abs_path_or_url_to_url(std::string_view path) -> std::string; + /** * Transform an absolute or relative path to a %-encoded "file://" URL. */ diff --git a/libmamba/src/core/channel.cpp b/libmamba/src/core/channel.cpp index 73410cc6b..d0faf5e3d 100644 --- a/libmamba/src/core/channel.cpp +++ b/libmamba/src/core/channel.cpp @@ -5,7 +5,6 @@ // The full license is in the file LICENSE, distributed with this software. #include -#include #include #include @@ -13,9 +12,8 @@ #include "mamba/core/context.hpp" #include "mamba/core/environment.hpp" #include "mamba/core/package_cache.hpp" -#include "mamba/core/util_os.hpp" #include "mamba/core/validate.hpp" -#include "mamba/specs/archive.hpp" +#include "mamba/specs/channel_spec.hpp" #include "mamba/specs/conda_url.hpp" #include "mamba/util/path_manip.hpp" #include "mamba/util/string.hpp" @@ -25,7 +23,6 @@ namespace mamba { - // Constants used by Channel and ChannelContext namespace { const std::map DEFAULT_CUSTOM_CHANNELS = { @@ -41,177 +38,20 @@ namespace mamba const char LOCAL_CHANNELS_NAME[] = "local"; const char DEFAULT_CHANNELS_NAME[] = "defaults"; - } // namespace - // Specific functions, used only in this file - namespace - { std::optional nonempty_str(std::string&& s) { return s.empty() ? std::optional() : std::make_optional(s); } - // Channel configuration - struct channel_configuration + auto channel_alias_location(specs::CondaURL url) -> std::string { - std::string location; - std::string name; - std::string scheme; - std::string user; - std::string password; - std::string token; - }; - - channel_configuration read_channel_configuration( - ChannelContext& channel_context, - const std::string& scheme, - const std::string& host, - const std::string& port, - const std::string& path - ) - { - auto spath = std::string(util::rstrip(path, '/')); - std::string url = [&]() - { - auto parsed_url = util::URL(); - parsed_url.set_scheme(scheme); - parsed_url.set_host(host); - parsed_url.set_port(port); - parsed_url.set_path(spath); - return parsed_url.pretty_str(util::URL::StripScheme::yes); - }(); - - // Case 1: No path given, channel name is "" - if (spath.empty()) - { - auto l_url = util::URL(); - l_url.set_host(host); - l_url.set_port(port); - return channel_configuration{ - /* .location= */ l_url - .pretty_str(util::URL::StripScheme::yes, /* rstrip_path= */ '/'), - /* .name= */ "", - /* .scheme= */ scheme, - /* .user= */ "", - /* .password= */ "", - /* .token= */ "", - }; - } - - // Case 2: migrated_custom_channels not implemented yet - // Case 3: migrated_channel_aliases not implemented yet - - // Case 4: custom_channels matches - const auto& custom_channels = channel_context.get_custom_channels(); - for (const auto& ca : custom_channels) - { - const Channel& channel = ca.second; - std::string test_url = util::join_url(channel.location(), channel.name()); - if (vector_is_prefix(util::split(test_url, "/"), util::split(url, "/"))) - { - auto subname = std::string(util::strip(url.replace(0u, test_url.size(), ""), '/')); - - return channel_configuration{ - /* .location= */ channel.location(), - /* .name= */ util::join_url(channel.name(), subname), - /* .scheme= */ scheme, - /* .user= */ channel.user().value_or(""), - /* .password= */ channel.password().value_or(""), - /* .token= */ channel.token().value_or(""), - }; - } - } - - // Case 5: channel_alias match - const Channel& ca = channel_context.get_channel_alias(); - if (!ca.location().empty() && util::starts_with(url, ca.location())) - { - auto name = std::string(util::strip(url.replace(0u, ca.location().size(), ""), '/')); - return channel_configuration{ - /* .location= */ ca.location(), - /* .name= */ name, - /* .scheme= */ scheme, - /* .user= */ ca.user().value_or(""), - /* .password= */ ca.password().value_or(""), - /* .token= */ ca.token().value_or(""), - }; - } - - // Case 6: not-otherwise-specified file://-type urls - if (host.empty() || ((host == util::URL::localhost) && port.empty())) - { - auto sp = util::rsplit(url, "/", 1); - return channel_configuration{ - /* .location= */ sp[0].size() ? sp[0] : "/", - /* .name= */ sp[1], - /* .scheme= */ "file", - /* .user= */ "", - /* .password= */ "", - /* .token= */ "", - }; - } - - // Case 7: fallback, channel_location = host:port and channel_name = path - spath = util::lstrip(spath, '/'); - auto location = util::URL(); - location.set_host(host); - location.set_port(port); - return channel_configuration{ - /* .location= */ location.pretty_str(util::URL::StripScheme::yes, /* rstrip_path= */ '/'), - /* .name= */ spath, - /* .scheme= */ scheme, - /* .user= */ "", - /* .password= */ "", - /* .token= */ "", - }; + url.clear_user(); + url.clear_password(); + url.clear_token(); + return url.pretty_str(specs::CondaURL::StripScheme::yes, '/'); } - - std::vector take_platforms(const Context& context, std::string& value) - { - std::vector platforms; - if (!value.empty()) - { - if (value[value.size() - 1] == ']') - { - const auto end_value = value.find_last_of('['); - if (end_value != std::string::npos) - { - auto ind = end_value + 1; - while (ind < value.size() - 1) - { - auto end = value.find_first_of(", ]", ind); - assert(end != std::string::npos); - platforms.emplace_back(value.substr(ind, end - ind)); - ind = end; - while (value[ind] == ',' || value[ind] == ' ') - { - ind++; - } - } - - value.resize(end_value); - } - } - // This is required because a channel can be instantiated from an URL - // that already contains the platform - else - { - std::string platform = ""; - util::split_platform(get_known_platforms(), value, context.platform, value, platform); - if (!platform.empty()) - { - platforms.push_back(std::move(platform)); - } - } - } - - if (platforms.empty()) - { - platforms = context.platforms(); - } - return platforms; - } - } // namespace + } std::vector get_known_platforms() { @@ -231,13 +71,14 @@ namespace mamba std::string_view user, std::string_view password, std::string_view token, - std::string_view package_filename + std::string_view package_filename, + util::flat_set platforms ) : m_url() , m_location(std::move(location)) , m_name(std::move(name)) , m_canonical_name(std::move(canonical_name)) - , m_platforms() + , m_platforms(std::move(platforms)) { if (m_name != UNKNOWN_CHANNEL) { @@ -264,9 +105,29 @@ namespace mamba } } + Channel::Channel( + specs::CondaURL url, + std::string location, + std::string name, + std::string canonical_name, + util::flat_set platforms + ) + : m_url(std::move(url)) + , m_location(std::move(location)) + , m_name(std::move(name)) + , m_canonical_name(std::move(canonical_name)) + , m_platforms(std::move(platforms)) + { + } + Channel::~Channel() = default; - const std::string& Channel::scheme() const + const specs::CondaURL& Channel::url() const + { + return m_url; + } + + std::string_view Channel::scheme() const { return m_url.scheme(); } @@ -281,7 +142,7 @@ namespace mamba return m_name; } - const std::vector& Channel::platforms() const + const util::flat_set& Channel::platforms() const { return m_platforms; } @@ -344,40 +205,42 @@ namespace mamba } else { - return util::concat_scheme_url(scheme(), util::join_url(location(), name())); + return util::concat_scheme_url(std::string(scheme()), util::join_url(location(), name())); } } - std::vector Channel::urls(bool with_credential) const + util::flat_set Channel::urls(bool with_credential) const { - if (package_filename()) + if (auto fn = package_filename()) { - std::string base = location(); + std::string base = {}; if (with_credential && token()) { - base = util::join_url(base, "t", *token()); + base = util::join_url(location(), "t", *token()); + } + else + { + base = location(); } - std::string platform = m_platforms[0]; return { { util::build_url( auth(), - scheme(), - util::join_url(base, name(), platform, *package_filename()), + std::string(scheme()), + util::join_url(base, name(), std::move(fn).value()), with_credential ) } }; } - else + + auto out = util::flat_set{}; + for (auto& [_, v] : platform_urls(with_credential)) { - std::vector ret; - for (auto& [_, v] : platform_urls(with_credential)) - { - ret.emplace_back(v); - } - return ret; + out.insert(v); } + return out; } - std::vector> Channel::platform_urls(bool with_credential) const + util::flat_set> + Channel::platform_urls(bool with_credential) const { std::string base = location(); if (with_credential && token()) @@ -385,15 +248,20 @@ namespace mamba base = util::join_url(base, "t", *token()); } - std::vector> ret; + auto out = util::flat_set>{}; for (const auto& platform : platforms()) { - ret.emplace_back( + out.insert({ platform, - util::build_url(auth(), scheme(), util::join_url(base, name(), platform), with_credential) - ); + util::build_url( + auth(), + std::string(scheme()), + util::join_url(base, name(), platform), + with_credential + ), + }); } - return ret; + return out; } std::string Channel::platform_url(std::string platform, bool with_credential) const @@ -403,7 +271,12 @@ namespace mamba { base = util::join_url(base, "t", *token()); } - return util::build_url(auth(), scheme(), util::join_url(base, name(), platform), with_credential); + return util::build_url( + auth(), + std::string(scheme()), + util::join_url(base, name(), platform), + with_credential + ); } bool operator==(const Channel& lhs, const Channel& rhs) @@ -421,7 +294,7 @@ namespace mamba *********************************/ Channel ChannelContext::make_simple_channel( - const Channel& channel_alias, + const specs::CondaURL& channel_alias, const std::string& channel_url, const std::string& channel_name, const std::string& channel_canonical_name @@ -429,14 +302,15 @@ namespace mamba { if (!util::url_has_scheme(channel_url)) { + auto ca_location = channel_alias_location(channel_alias); return Channel( /* scheme= */ channel_alias.scheme(), - /* location= */ channel_alias.location(), + /* location= */ std::move(ca_location), /* name= */ std::string(util::strip(channel_name.empty() ? channel_url : channel_name, '/')), /* canonical_name= */ channel_canonical_name, - /* user= */ channel_alias.user().value_or(""), - /* password= */ channel_alias.password().value_or(""), - /* token= */ channel_alias.token().value_or(""), + /* user= */ channel_alias.user(), + /* password= */ channel_alias.password(), + /* token= */ channel_alias.token(), /* package_filename= */ {} ); } @@ -454,12 +328,11 @@ namespace mamba std::string name(channel_name); if (name.empty()) { - if (!channel_alias.location().empty() - && util::starts_with(location, channel_alias.location())) + if (auto ca_location = channel_alias_location(channel_alias); + util::starts_with(location, ca_location)) { - name = location; - name.replace(0u, channel_alias.location().size(), ""); - location = channel_alias.location(); + name = std::string(util::strip(util::remove_prefix(location, ca_location), '/')); + location = std::move(ca_location); } else if (url.scheme() == "file") { @@ -486,44 +359,233 @@ namespace mamba ); } - Channel ChannelContext::from_url(std::string_view url_str) + namespace { - auto url = specs::CondaURL::parse(url_str); - std::string package_name = url.package(); - url.clear_package(); - std::string token = std::string(url.token()); - url.clear_token(); - - auto config = read_channel_configuration(*this, url.scheme(), url.host(), url.port(), url.path()); - - auto res_scheme = !config.scheme.empty() ? config.scheme : "https"; - std::string canonical_name; - - const auto& custom_channels = get_custom_channels(); - if ((custom_channels.find(config.name) != custom_channels.end()) - || (config.location == get_channel_alias().location())) + auto url_match(const specs::CondaURL& registered, const specs::CondaURL& candidate) -> bool { - canonical_name = config.name; + using Decode = typename specs::CondaURL::Decode; + + // Not checking users, passwords, and tokens + return /**/ + // Defaulted scheme matches all, otherwise schemes must be the same + (registered.scheme_is_defaulted() || (registered.scheme() == candidate.scheme())) + // Hosts must always be the same + && (registered.host(Decode::no) == candidate.host(Decode::no)) + // Different ports are considered different channels + && (registered.port() == candidate.port()) + // Registered path must be a prefix + && util::path_is_prefix( + registered.path_without_token(Decode::no), + candidate.path_without_token(Decode::no) + ); } - else + + auto rsplit_once(std::string_view str, char sep) { - canonical_name = util::concat_scheme_url( - res_scheme, - util::join_url(config.location, config.name) + auto [head, tail] = util::rstrip_if_parts(str, [sep](char c) { return c != sep; }); + if (head.empty()) + { + return std::array{ head, tail }; + } + return std::array{ head.substr(0, head.size() - 1), tail }; + } + + auto + make_platforms(util::flat_set filters, const std::vector& defaults) + { + if (filters.empty()) + { + for (const auto& plat : defaults) + { + filters.insert(plat); + } + } + return filters; + }; + } + + Channel ChannelContext::from_any_path(specs::ChannelSpec&& spec) + { + auto uri = specs::CondaURL::parse(util::path_or_url_to_url(spec.location())); + + auto path = uri.pretty_path(); + auto [parent, current] = rsplit_once(path, '/'); + for (const auto& [canonical_name, chan] : get_custom_channels()) + { + if (url_match(chan.url(), uri)) + { + return Channel( + /* url= */ std::move(uri), + /* location= */ chan.url().pretty_str(specs::CondaURL::StripScheme::yes), + /* name= */ std::string(util::rstrip(parent, '/')), + /* canonical_name= */ std::string(canonical_name) + ); + } + } + + if (const auto& ca = get_channel_alias(); url_match(ca, uri)) + { + auto name = util::strip(util::remove_prefix(uri.path(), ca.path()), '/'); + return Channel( + /* url= */ std::move(uri), + /* location= */ ca.pretty_str(specs::CondaURL::StripScheme::yes), + /* name= */ std::string(name), + /* canonical_name= */ std::string(name) ); } - std::string user = url.user(); // % encoded - std::string password = url.password(); // % encoded + auto canonical_name = uri.pretty_str(); return Channel( - /* scheme= */ res_scheme, - /* location= */ config.location, - /* name= */ config.name, - /* canonical_name= */ canonical_name, - /* user= */ user.empty() ? config.user : user, - /* password= */ password.empty() ? config.password : password, - /* token= */ token.empty() ? config.token : token, - /* package_filename= */ package_name + /* url= */ std::move(uri), + /* location= */ std::string(util::rstrip(parent, '/')), + /* name= */ std::string(util::rstrip(current, '/')), + /* canonical_name= */ std::move(canonical_name) + ); + } + + Channel ChannelContext::from_package_path(specs::ChannelSpec&& spec) + { + assert(spec.type() == specs::ChannelSpec::Type::PackagePath); + return from_any_path(std::move(spec)); + } + + Channel ChannelContext::from_path(specs::ChannelSpec&& spec) + { + assert(spec.type() == specs::ChannelSpec::Type::Path); + auto platforms = make_platforms(spec.clear_platform_filters(), m_context.platforms()); + auto chan = from_any_path(std::move(spec)); + chan.m_platforms = std::move(platforms); + return chan; + } + + namespace + { + // Channel configuration + struct channel_configuration + { + specs::CondaURL url; + std::string location; + std::string name; + std::string canonical_name; + }; + + channel_configuration + read_channel_configuration(ChannelContext& channel_context, specs::CondaURL url) + { + assert(url.scheme() != "file"); + + url.clear_package(); + url.clear_token(); + url.clear_password(); + url.clear_user(); + + std::string default_location = url.pretty_str( + specs::CondaURL::StripScheme::yes, + '/', + specs::CondaURL::Credentials::Remove + ); + + // Case 4: custom_channels matches + for (const auto& [canonical_name, chan] : channel_context.get_custom_channels()) + { + std::string test_url = util::join_url(chan.location(), chan.name()); + if (vector_is_prefix(util::split(test_url, "/"), util::split(default_location, "/"))) + { + auto subname = std::string( + util::strip(default_location.replace(0u, test_url.size(), ""), '/') + ); + + auto location = chan.location(); + auto name = util::join_url(chan.name(), subname); + auto l_url = specs::CondaURL::parse(chan.base_url()); + l_url.append_path(subname); + l_url.set_scheme(url.scheme()); + l_url.set_user(chan.user().value_or("")); + l_url.set_password(chan.password().value_or("")); + if (auto token = chan.token().value_or(""); !token.empty()) + { + l_url.set_token(std::move(token)); + } + return channel_configuration{ + /* .url= */ std::move(l_url), + /* .location= */ std::move(location), + /* .name= */ std::move(name), + /* .canonical_name= */ std::move(canonical_name), + }; + } + } + + // Case 5: channel_alias match + const auto& ca = channel_context.get_channel_alias(); + if (auto ca_location = channel_alias_location(ca); + util::starts_with(default_location, ca_location)) + { + auto name = std::string( + util::strip(util::remove_prefix(default_location, ca_location), '/') + ); + auto l_url = specs::CondaURL::parse(util::join_url(ca_location, name)); + l_url.set_scheme(url.scheme()); + l_url.set_user(ca.user()); + l_url.set_password(ca.password()); + if (auto token = ca.token(); !token.empty()) + { + l_url.set_token(std::move(token)); + } + return channel_configuration{ + /*. .url= */ std::move(l_url), + /* .location= */ std::move(ca_location), + /* .name= */ name, + /* .canonical_name= */ name, + }; + } + + // Case 2: migrated_custom_channels not implemented yet + // Case 3: migrated_channel_aliases not implemented yet + + // Case 1: No path given, channel name is "" + // Case 7: fallback, channel_location = host:port and channel_name = path + auto name = std::string(util::strip(url.path_without_token(), '/')); + auto location = url.authority(specs::CondaURL::Credentials::Remove); + auto canonical_name = url.pretty_str( + util::URL::StripScheme::no, + /* rstrip_path/ */ '/', + specs::CondaURL::Credentials::Remove + ); + return channel_configuration{ + /* .url= */ std::move(url), + /* .location= */ std::move(location), + /* .name= */ std::move(name), + /* .canonical_name= */ std::move(canonical_name), + }; + } + } + + Channel ChannelContext::from_url(specs::ChannelSpec&& spec) + { + assert(util::url_has_scheme(spec.location())); + auto url = specs::CondaURL::parse(spec.location()); + + auto config = read_channel_configuration(*this, url); + + using Decode = typename specs::CondaURL::Decode; + using Encode = typename specs::CondaURL::Encode; + if (url.user(Decode::no).empty() && !config.url.user(Decode::no).empty()) + { + url.set_user(config.url.clear_user(), Encode::no); + } + if (url.password(Decode::no).empty() && !config.url.password(Decode::no).empty()) + { + url.set_password(config.url.clear_password(), Encode::no); + } + if (url.token().empty() && !config.url.token().empty()) + { + url.set_token(config.url.token()); + } + return Channel( + /* url= */ std::move(url), + /* location= */ std::move(config.location), + /* name= */ std::move(config.name), + /* canonical_name= */ std::move(config.canonical_name) ); } @@ -586,15 +648,15 @@ namespace mamba } else { - const Channel& alias = get_channel_alias(); + const auto& alias = get_channel_alias(); return Channel( /* scheme= */ alias.scheme(), - /* location= */ alias.location(), + /* location= */ channel_alias_location(alias), /* name= */ name, /* canonical_name= */ name, - /* user= */ alias.user().value_or(""), - /* password= */ alias.password().value_or(""), - /* token= */ alias.token().value_or("") + /* user= */ alias.user(), + /* password= */ alias.password(), + /* token= */ alias.token() ); } } @@ -611,42 +673,56 @@ namespace mamba ); } - std::string value = in_value; - auto platforms = take_platforms(m_context, value); + auto spec = specs::ChannelSpec::parse(in_value); - auto chan = util::url_has_scheme(value) ? from_url(fix_win_path(value)) - : util::is_explicit_path(value) ? from_url(util::path_or_url_to_url(value)) - : specs::has_archive_extension(value) ? from_url(fix_win_path(value)) - : from_name(value); + auto get_platforms = [&]() + { + auto out = spec.platform_filters(); - chan.m_platforms = std::move(platforms); + if (out.empty()) + { + for (const auto& plat : m_context.platforms()) + { + out.insert(plat); + } + } + return out; + }; - return chan; + return [&](specs::ChannelSpec&& l_spec) -> Channel + { + switch (l_spec.type()) + { + case specs::ChannelSpec::Type::PackagePath: + { + return from_package_path(std::move(l_spec)); + } + case specs::ChannelSpec::Type::Path: + { + return from_path(std::move(l_spec)); + } + case specs::ChannelSpec::Type::PackageURL: + { + return from_url(std::move(l_spec)); + } + case specs::ChannelSpec::Type::URL: + { + auto plats = get_platforms(); + auto chan = from_url(std::move(l_spec)); + chan.m_platforms = std::move(plats); + return chan; + } + case specs::ChannelSpec::Type::Name: + { + auto chan = from_name(l_spec.location()); + chan.m_platforms = get_platforms(); + return chan; + } + } + throw std::invalid_argument("Invalid ChannelSpec::Type"); + }(std::move(spec)); } - Channel ChannelContext::from_alias(std::string_view alias) - { - auto url = specs::CondaURL::parse(alias); - - std::string token = std::string(url.token()); - std::string user = url.user(); // % encoded - url.clear_user(); - std::string password = url.password(); // % encoded - url.clear_password(); - url.clear_token(); - - return Channel( - /* scheme= */ url.scheme(), - /* location= */ url.pretty_str(specs::CondaURL::StripScheme::yes, '/'), - /* name= */ "", - /* canonical_name= */ "", - /* user= */ user, - /* password= */ password, - /* token= */ token - ); - } - - const Channel& ChannelContext::make_channel(const std::string& value) { auto res = m_channel_cache.find(value); @@ -723,7 +799,7 @@ namespace mamba return result; } - const Channel& ChannelContext::get_channel_alias() const + const specs::CondaURL& ChannelContext::get_channel_alias() const { return m_channel_alias; } @@ -740,7 +816,7 @@ namespace mamba ChannelContext::ChannelContext(Context& context) : m_context(context) - , m_channel_alias(from_alias(m_context.channel_alias)) + , m_channel_alias(specs::CondaURL::parse(util::path_or_url_to_url(m_context.channel_alias))) { init_custom_channels(); } diff --git a/libmamba/src/core/match_spec.cpp b/libmamba/src/core/match_spec.cpp index a18630e26..747237e90 100644 --- a/libmamba/src/core/match_spec.cpp +++ b/libmamba/src/core/match_spec.cpp @@ -102,7 +102,10 @@ namespace mamba channel = parsed_channel.canonical_name(); // TODO how to handle this with multiple platforms? - subdir = parsed_channel.platforms()[0]; + if (const auto& plats = parsed_channel.platforms(); !plats.empty()) + { + subdir = plats.front(); + } fn = *parsed_channel.package_filename(); url = spec_str; is_file = true; diff --git a/libmamba/src/core/query.cpp b/libmamba/src/core/query.cpp index 6a713f358..fb2df9f13 100644 --- a/libmamba/src/core/query.cpp +++ b/libmamba/src/core/query.cpp @@ -186,7 +186,7 @@ namespace mamba out, " {:<15} {}\n", "URL", - url.pretty_str(CondaURL::StripScheme::no, '/', CondaURL::HideConfidential::yes) + url.pretty_str(CondaURL::StripScheme::no, '/', CondaURL::Credentials::Hide) ); fmt::print(out, fmtstring, "MD5", pkg.md5.empty() ? "Not available" : pkg.md5); diff --git a/libmamba/src/core/util.cpp b/libmamba/src/core/util.cpp index 974a32693..797b4c0b1 100644 --- a/libmamba/src/core/util.cpp +++ b/libmamba/src/core/util.cpp @@ -1621,14 +1621,16 @@ namespace mamba if (host.empty()) { - options = { - scheme, - "all", - }; + options = { std::string(scheme), "all" }; } else { - options = { scheme + "://" + host, scheme, "all://" + host, "all" }; + options = { + util::concat(scheme, "://", host), + std::string(scheme), + util::concat("all://", host), + "all", + }; } for (auto& option : options) diff --git a/libmamba/src/specs/channel_spec.cpp b/libmamba/src/specs/channel_spec.cpp new file mode 100644 index 000000000..c9539dfc2 --- /dev/null +++ b/libmamba/src/specs/channel_spec.cpp @@ -0,0 +1,191 @@ +// Copyright (c) 2023, QuantStack and Mamba Contributors +// +// Distributed under the terms of the BSD 3-Clause License. +// +// The full license is in the file LICENSE, distributed with this software. + +#include +#include +#include +#include +#include + +#include + +#include "mamba/fs/filesystem.hpp" +#include "mamba/specs/archive.hpp" +#include "mamba/specs/channel_spec.hpp" +#include "mamba/specs/platform.hpp" +#include "mamba/util/path_manip.hpp" +#include "mamba/util/string.hpp" +#include "mamba/util/url_manip.hpp" + +namespace mamba::specs +{ + // Defined in conda_url.cpp + [[nodiscard]] auto find_slash_and_platform(std::string_view path) + -> std::tuple>; + + namespace + { + using dynamic_platform_set = ChannelSpec::dynamic_platform_set; + + auto parse_platform_list(std::string_view plats) -> dynamic_platform_set + { + static constexpr auto is_not_sep = [](char c) -> bool + { return !util::contains(ChannelSpec::platform_separators, c); }; + + auto out = dynamic_platform_set{}; + auto head_rest = util::lstrip_if_parts(plats, is_not_sep); + while (!head_rest.front().empty()) + { + // Accepting all strings, so that user can dynamically register new platforms + out.insert(util::to_lower(util::strip(head_rest.front()))); + head_rest = util::lstrip_if_parts( + util::lstrip(head_rest.back(), ChannelSpec::platform_separators), + is_not_sep + ); + } + return out; + } + + auto parse_platform_path(std::string_view str) -> std::pair + { + static constexpr auto npos = std::string_view::npos; + + auto [start, len, plat] = find_slash_and_platform(str); + if (plat.has_value()) + { + const auto end = (len == npos) ? str.size() : start + len; + return { + util::concat(str.substr(0, start), str.substr(end)), + std::string(platform_name(plat.value())), + }; + } + return { {}, {} }; + } + + auto split_location_platform(std::string_view str) + -> std::pair + { + if (util::ends_with(str, ']')) + { + // Parsing platforms in "something[linux-64,noarch]" + const auto start_pos = str.find_last_of('['); + if ((start_pos != std::string_view::npos) && (start_pos != 0)) + { + return { + std::string(util::rstrip(str.substr(0, start_pos))), + parse_platform_list(str.substr(start_pos + 1, str.size() - start_pos - 2)), + }; + } + } + + if (!has_archive_extension(str)) + { + // Paring a platform inside a path name. + // This is required because a channel can be instantiated from a value that already + // contains the platform. + auto [rest, plat] = parse_platform_path(str); + if (!plat.empty()) + { + rest = util::rstrip(rest, '/'); + return { + std::move(rest), + { std::move(plat) }, + }; + } + } + + // For single archive channel specs, we don't need to compute platform filters + // since they are not needed to compute URLs. + return { std::string(util::rstrip(str)), {} }; + } + + auto parse_path(std::string_view str) -> std::string + { + auto out = util::path_to_posix(fs::u8path(str).lexically_normal().string()); + out = util::rstrip(out, '/'); + return out; + } + } + + auto ChannelSpec::parse(std::string_view str) -> ChannelSpec + { + str = util::strip(str); + if (str.empty()) + { + return {}; + } + + auto [location, filters] = split_location_platform(str); + + const std::string_view scheme = util::url_get_scheme(location); + Type type = {}; + if (scheme == "file") + { + type = has_archive_extension(location) ? Type::PackagePath : Type::Path; + } + else if (!scheme.empty()) + { + type = has_archive_extension(location) ? Type::PackageURL : Type::URL; + } + else if (util::is_explicit_path(location)) + { + location = parse_path(location); + type = has_archive_extension(location) ? Type::PackagePath : Type::Path; + } + else + { + type = Type::Name; + } + + return { std::move(location), std::move(filters), type }; + } + + ChannelSpec::ChannelSpec(std::string location, dynamic_platform_set filters, Type type) + : m_location(std::move(location)) + , m_platform_filters(std::move(filters)) + , m_type(type) + { + if (m_location.empty()) + { + m_location = std::string(default_name); + } + } + + auto ChannelSpec::type() const -> Type + { + return m_type; + } + + auto ChannelSpec::location() const& -> const std::string& + { + return m_location; + } + + auto ChannelSpec::location() && -> std::string + { + return std::move(m_location); + } + + auto ChannelSpec::clear_location() -> std::string + { + return std::exchange(m_location, ""); + } + + auto ChannelSpec::platform_filters() const& -> const dynamic_platform_set& + { + return m_platform_filters; + } + + auto ChannelSpec::platform_filters() && -> dynamic_platform_set + { + return std::move(m_platform_filters); + } + + auto ChannelSpec::clear_platform_filters() -> dynamic_platform_set + { + return std::exchange(m_platform_filters, {}); + } +} diff --git a/libmamba/src/specs/conda_url.cpp b/libmamba/src/specs/conda_url.cpp index 524f8eef2..99c7b9ad7 100644 --- a/libmamba/src/specs/conda_url.cpp +++ b/libmamba/src/specs/conda_url.cpp @@ -18,6 +18,33 @@ namespace mamba::specs { + /** + * Find the location of "/os-arch"-like subsring. + * + * Not a static function, it is needed in "channel_spec.cpp". + */ + auto find_slash_and_platform(std::string_view path) + -> std::tuple> + { + static constexpr auto npos = std::string_view::npos; + + auto start = std::size_t(0); + auto end = path.find('/', start + 1); + while (start != npos) + { + assert(start < end); + const auto count = (end == npos) ? npos : end - start; + const auto count_minus_1 = (end == npos) ? npos : end - start - 1; + if (auto plat = platform_parse(path.substr(start + 1, count_minus_1))) + { + return { start, count, plat }; + } + start = end; + end = path.find('/', start + 1); + } + return { npos, 0, std::nullopt }; + } + namespace { [[nodiscard]] auto is_token_char(char c) -> bool @@ -45,37 +72,40 @@ namespace mamba::specs && std::all_of(token_rest.cbegin(), token_rest.cend(), &is_token_char); } - [[nodiscard]] auto find_token_and_prefix(std::string_view path) - -> std::pair + [[nodiscard]] auto token_and_prefix_len(std::string_view path) -> std::size_t { static constexpr auto npos = std::string_view::npos; + static constexpr auto prefix = CondaURL::token_prefix; - const auto prefix_pos = path.find(CondaURL::token_prefix); - if (prefix_pos == npos) + if ((path.size() <= prefix.size()) || !util::starts_with(path, prefix)) { - return std::pair{ std::string_view::npos, 0ul }; + return 0; } - const auto token_pos = prefix_pos + CondaURL::token_prefix.size(); - const auto token_end_pos = path.find('/', token_pos); - assert(token_pos < token_end_pos); - const auto token_len = (token_end_pos == npos) ? npos : token_end_pos - token_pos; - if (is_token(path.substr(token_pos, token_len))) + const auto token_end_pos = path.find('/', prefix.size()); + assert(prefix.size() < token_end_pos); + const auto token_len = (token_end_pos == npos) ? npos : token_end_pos - prefix.size(); + if (is_token(path.substr(prefix.size(), token_len))) { - const auto token_and_prefix_len = (token_end_pos == npos) - ? npos - : token_end_pos - token_pos - + CondaURL::token_prefix.size(); - return std::pair{ prefix_pos, token_and_prefix_len }; + return token_end_pos; } - return std::pair{ std::string_view::npos, 0ul }; + return 0; + } + } + + void CondaURL::ensure_path_without_token_leading_slash() + { + if (path_without_token().empty()) + { + set_path_without_token("/", Encode::no); } } CondaURL::CondaURL(URL&& url) : Base(std::move(url)) { + ensure_path_without_token_leading_slash(); } CondaURL::CondaURL(const util::URL& url) @@ -88,19 +118,43 @@ namespace mamba::specs return CondaURL(URL::parse(url)); } + void CondaURL::set_path(std::string_view path, Encode::yes_type) + { + Base::set_path(path, Encode::yes); + ensure_path_without_token_leading_slash(); + } + + void CondaURL::set_path(std::string path, Encode::no_type) + { + Base::set_path(path, Encode::no); + ensure_path_without_token_leading_slash(); + } + + void CondaURL::append_path(std::string_view path, Encode::yes_type) + { + Base::append_path(path, Encode::yes); + ensure_path_without_token_leading_slash(); + } + + void CondaURL::append_path(std::string_view path, Encode::no_type) + { + Base::append_path(path, Encode::no); + ensure_path_without_token_leading_slash(); + } + auto CondaURL::token() const -> std::string_view { static constexpr auto npos = std::string_view::npos; const auto& l_path = path(Decode::no); - const auto [pos, len] = find_token_and_prefix(l_path); - if ((pos == npos) || (len == 0)) + const auto len = token_and_prefix_len(l_path); + if (len == 0) { return ""; } assert(token_prefix.size() < len); const auto token_len = (len != npos) ? len - token_prefix.size() : npos; - return std::string_view(l_path).substr(pos + token_prefix.size(), token_len); + return std::string_view(l_path).substr(token_prefix.size(), token_len); } namespace @@ -109,27 +163,25 @@ namespace mamba::specs std::string& path, std::size_t pos, std::size_t len, - std::string_view token + std::string_view new_token ) { static constexpr auto npos = std::string_view::npos; assert(CondaURL::token_prefix.size() < len); const auto token_len = (len != npos) ? len - CondaURL::token_prefix.size() : npos; - path.replace(pos + CondaURL::token_prefix.size(), token_len, token); + path.replace(pos + CondaURL::token_prefix.size(), token_len, new_token); } } void CondaURL::set_token(std::string_view token) { - static constexpr auto npos = std::string_view::npos; - if (!is_token(token)) { throw std::invalid_argument(fmt::format(R"(Invalid CondaURL token "{}")", token)); } - const auto [pos, len] = find_token_and_prefix(path(Decode::no)); - if ((pos == npos) || (len == 0)) + const auto len = token_and_prefix_len(path(Decode::no)); + if (len == 0) { std::string l_path = clear_path(); // percent encoded assert(util::starts_with(l_path, '/')); @@ -138,54 +190,72 @@ namespace mamba::specs else { std::string l_path = clear_path(); // percent encoded - set_token_no_check_input_impl(l_path, pos, len, token); + set_token_no_check_input_impl(l_path, 0, len, token); set_path(std::move(l_path), Encode::no); } } auto CondaURL::clear_token() -> bool { - const auto [pos, len] = find_token_and_prefix(path(Decode::no)); - if ((pos == std::string::npos) || (len == 0)) + const auto len = token_and_prefix_len(path(Decode::no)); + if (len == 0) { return false; } assert(token_prefix.size() < len); std::string l_path = clear_path(); // percent encoded - l_path.erase(pos, len); - set_path(std::move(l_path), Encode::no); + l_path.erase(0, len); + Base::set_path(std::move(l_path), Encode::no); return true; } - namespace + auto CondaURL::path_without_token(Decode::no_type) const -> std::string_view { - [[nodiscard]] auto find_slash_and_platform(std::string_view path) - -> std::tuple> + const auto& full_path = path(Decode::no); + if (const auto len = token_and_prefix_len(full_path); len > 0) { - static constexpr auto npos = std::string_view::npos; - - assert(!path.empty() && (path.front() == '/')); - auto start = std::size_t(0); - auto end = path.find('/', start + 1); - while (start != npos) - { - assert(start < end); - const auto count = (end == npos) ? npos : end - start; - const auto count_minus_1 = (end == npos) ? npos : end - start - 1; - if (auto plat = platform_parse(path.substr(start + 1, count_minus_1))) - { - return { start, count, plat }; - } - start = end; - end = path.find('/', start + 1); - } - return { npos, 0, std::nullopt }; + return std::string_view(full_path).substr(std::min(len, full_path.size())); } + return full_path; + } + + auto CondaURL::path_without_token(Decode::yes_type) const -> std::string + { + return util::url_decode(path_without_token(Decode::no)); + } + + void CondaURL::set_path_without_token(std::string_view new_path, Encode::no_type) + { + if (const auto len = token_and_prefix_len(path(Decode::no)); len > 0) + { + auto old_path = clear_path(); + old_path.erase(std::min(len, old_path.size())); + Base::set_path(std::move(old_path), Encode::no); + Base::append_path(new_path.empty() ? "/" : new_path); + } + else + { + Base::set_path(std::string(new_path), Encode::no); + } + } + + void CondaURL::set_path_without_token(std::string_view new_path, Encode::yes_type) + { + clear_path_without_token(); + Base::append_path(new_path.empty() ? "/" : new_path, Encode::yes); + } + + auto CondaURL::clear_path_without_token() -> bool + { + const std::size_t old_len = path(Decode::no).size(); + set_path_without_token("", Encode::no); + return path(Decode::no).size() != old_len; } auto CondaURL::platform() const -> std::optional { const auto& l_path = path(Decode::no); + assert(!l_path.empty() && (l_path.front() == '/')); const auto [pos, count, plat] = find_slash_and_platform(l_path); return plat; } @@ -195,6 +265,7 @@ namespace mamba::specs static constexpr auto npos = std::string_view::npos; const auto& l_path = path(Decode::no); + assert(!l_path.empty() && (l_path.front() == '/')); const auto [pos, len, plat] = find_slash_and_platform(l_path); if (!plat.has_value()) { @@ -209,6 +280,7 @@ namespace mamba::specs { static constexpr auto npos = std::string_view::npos; + assert(!path(Decode::no).empty() && (path(Decode::no).front() == '/')); const auto [pos, len, plat] = find_slash_and_platform(path(Decode::no)); if (!plat.has_value()) { @@ -220,7 +292,7 @@ namespace mamba::specs std::string l_path = clear_path(); // percent encoded const auto plat_len = (len != npos) ? len - 1 : npos; l_path.replace(pos + 1, plat_len, platform); - set_path(std::move(l_path), Encode::no); + Base::set_path(std::move(l_path), Encode::no); } void CondaURL::set_platform(std::string_view platform) @@ -239,6 +311,7 @@ namespace mamba::specs auto CondaURL::clear_platform() -> bool { + assert(!path(Decode::no).empty() && (path(Decode::no).front() == '/')); const auto [pos, count, plat] = find_slash_and_platform(path(Decode::no)); if (!plat.has_value()) { @@ -247,7 +320,7 @@ namespace mamba::specs assert(1 < count); std::string l_path = clear_path(); // percent encoded l_path.erase(pos, count); - set_path(std::move(l_path), Encode::no); + Base::set_path(std::move(l_path), Encode::no); return true; } @@ -287,11 +360,11 @@ namespace mamba::specs auto l_path = clear_path(); const auto pos = std::min(std::min(l_path.rfind('/'), l_path.size()) + 1ul, l_path.size()); l_path.replace(pos, std::string::npos, pkg); - set_path(std::move(l_path), Encode::no); + Base::set_path(std::move(l_path), Encode::no); } else { - append_path(pkg, Encode::no); + Base::append_path(pkg, Encode::no); } } @@ -302,40 +375,113 @@ namespace mamba::specs { auto l_path = clear_path(); l_path.erase(l_path.rfind('/')); - set_path(std::move(l_path), Encode::no); + Base::set_path(std::move(l_path), Encode::no); return true; } return false; } + auto CondaURL::str(Credentials credentials) const -> std::string + { + std::string_view l_path = ""; + std::string_view l_token = ""; + switch (credentials) + { + case (Credentials::Show): + { + l_path = path(Decode::no); + break; + } + case (Credentials::Hide): + { + if (token().empty()) + { + l_path = path(Decode::no); + } + else + { + l_path = path_without_token(Decode::no); + l_token = "*****"; + } + break; + } + case (Credentials::Remove): + { + l_path = path_without_token(Decode::no); + break; + } + } + std::array authority = authority_elems(credentials, Decode::no); + return util::concat( + scheme(), + "://", + authority[0], + authority[1], + authority[2], + authority[3], + authority[4], + authority[5], + authority[6], + l_token.empty() ? "" : token_prefix, + l_token, + l_path, + query().empty() ? "" : "?", + query(), + fragment().empty() ? "" : "#", + fragment() + ); + } + auto - CondaURL::pretty_str(StripScheme strip_scheme, char rstrip_path, HideConfidential hide_confifential) const + CondaURL::pretty_str(StripScheme strip_scheme, char rstrip_path, Credentials credentials) const -> std::string { - std::string computed_path = pretty_str_path(strip_scheme, rstrip_path); - - if (hide_confifential == HideConfidential::yes) + std::string l_path = {}; + switch (credentials) { - const auto [pos, len] = find_token_and_prefix(computed_path); - if ((pos < std::string::npos) && (len > 0)) + case (Credentials::Show): { - set_token_no_check_input_impl(computed_path, pos, len, "*****"); + l_path = pretty_str_path(strip_scheme, rstrip_path); + break; + } + case (Credentials::Hide): + { + if (token().empty()) + { + l_path = pretty_str_path(strip_scheme, rstrip_path); + } + else + { + l_path = util::concat("/t/*****", path_without_token(Decode::yes)); + } + break; + } + case (Credentials::Remove): + { + if (token().empty()) + { + l_path = pretty_str_path(strip_scheme, rstrip_path); + } + else + { + l_path = path_without_token(Decode::yes); + } + break; } } + std::array authority = authority_elems(credentials, Decode::yes); return util::concat( (strip_scheme == StripScheme::no) ? scheme() : "", (strip_scheme == StripScheme::no) ? "://" : "", - user(Decode::yes), - password(Decode::no).empty() ? "" : ":", - password(Decode::no).empty() - ? "" - : ((hide_confifential == HideConfidential::no) ? password(Decode::yes) : "*****"), - user(Decode::no).empty() ? "" : "@", - host(Decode::yes), - port().empty() ? "" : ":", - port(), - computed_path, + authority[0], + authority[1], + authority[2], + authority[3], + authority[4], + authority[5], + authority[6], + l_path, query().empty() ? "" : "?", query(), fragment().empty() ? "" : "#", diff --git a/libmamba/src/util/path_manip.cpp b/libmamba/src/util/path_manip.cpp index 91ac9a567..bc82892d2 100644 --- a/libmamba/src/util/path_manip.cpp +++ b/libmamba/src/util/path_manip.cpp @@ -5,7 +5,6 @@ // The full license is in the file LICENSE, distributed with this software. #include -#include #include "mamba/util/build.hpp" #include "mamba/util/path_manip.hpp" @@ -72,4 +71,30 @@ namespace mamba::util } return path; } + + // TODO(C++20): Use std::ranges::split_view + auto path_is_prefix(std::string_view parent, std::string_view child, char sep) -> bool + { + static constexpr auto npos = std::string_view::npos; + + std::size_t parent_start = 0; + std::size_t parent_end = parent.find(sep); + std::size_t child_start = 0; + std::size_t child_end = child.find(sep); + auto parent_item = [&]() { return parent.substr(parent_start, parent_end); }; + auto child_item = [&]() { return child.substr(child_start, child_end); }; + while ((parent_end != npos) && (child_end != npos)) + { + if (parent_item() != child_item()) + { + return false; + } + parent_start = parent_end + 1; + parent_end = parent.find(sep, parent_start); + child_start = child_end + 1; + child_end = child.find(sep, child_start); + } + // Last item comparison + return parent_item().empty() || (parent_item() == child_item()); + } } diff --git a/libmamba/src/util/string.cpp b/libmamba/src/util/string.cpp index 86708b191..661de95ee 100644 --- a/libmamba/src/util/string.cpp +++ b/libmamba/src/util/string.cpp @@ -285,40 +285,62 @@ namespace mamba::util * Implementation of remove prefix/suffix functions * ******************************************************/ - std::string_view remove_prefix(std::string_view str, std::string_view prefix) + std::array split_prefix(std::string_view str, std::string_view prefix) { if (starts_with(str, prefix)) { - return str.substr(prefix.size()); + return { str.substr(0, prefix.size()), str.substr(prefix.size()) }; } - return str; + return { std::string_view(), str }; + } + + std::array split_prefix(std::string_view str, std::string_view::value_type c) + { + if (starts_with(str, c)) + { + return { str.substr(0, 1), str.substr(1) }; + } + return { std::string_view(), str }; + } + + std::string_view remove_prefix(std::string_view str, std::string_view prefix) + { + return std::get<1>(split_prefix(str, prefix)); } std::string_view remove_prefix(std::string_view str, std::string_view::value_type c) { - if (starts_with(str, c)) + return std::get<1>(split_prefix(str, c)); + } + + std::array split_suffix(std::string_view str, std::string_view suffix) + { + if (ends_with(str, suffix)) { - return str.substr(1); + auto suffix_pos = str.size() - suffix.size(); + return { str.substr(0, suffix_pos), str.substr(suffix_pos) }; } - return str; + return { str, std::string_view() }; + } + + std::array split_suffix(std::string_view str, std::string_view::value_type c) + { + if (ends_with(str, c)) + { + auto suffix_pos = str.size() - 1; + return { str.substr(0, suffix_pos), str.substr(suffix_pos) }; + } + return { str, std::string_view() }; } std::string_view remove_suffix(std::string_view str, std::string_view suffix) { - if (ends_with(str, suffix)) - { - return str.substr(0, str.size() - suffix.size()); - } - return str; + return std::get<0>(split_suffix(str, suffix)); } std::string_view remove_suffix(std::string_view str, std::string_view::value_type c) { - if (ends_with(str, c)) - { - return str.substr(0, str.size() - 1); - } - return str; + return std::get<0>(split_suffix(str, c)); } /*************************************** @@ -432,7 +454,7 @@ namespace mamba::util std::array, 2> lstrip_parts_impl(std::basic_string_view input, CharOrStrView chars) { - std::size_t const start = input.find_first_not_of(chars); + const std::size_t start = input.find_first_not_of(chars); if (start == std::basic_string_view::npos) { return { input, std::basic_string_view{} }; @@ -469,7 +491,7 @@ namespace mamba::util std::array, 2> rstrip_parts_impl(std::basic_string_view input, CharOrStrView chars) { - std::size_t const end = input.find_last_not_of(chars); + const std::size_t end = input.find_last_not_of(chars); if (end == std::basic_string_view::npos) { return { std::basic_string_view{}, input }; @@ -506,13 +528,13 @@ namespace mamba::util std::array, 3> strip_parts_impl(std::basic_string_view input, CharOrStrView chars) { - std::size_t const start = input.find_first_not_of(chars); + const std::size_t start = input.find_first_not_of(chars); if (start == std::basic_string_view::npos) { return { input, {}, {} }; } - std::size_t const end = input.find_last_not_of(chars) + 1; - std::size_t const length = end - start; + const std::size_t end = input.find_last_not_of(chars) + 1; + const std::size_t length = end - start; return { input.substr(0, start), input.substr(start, length), input.substr(end) }; } } @@ -554,8 +576,8 @@ namespace mamba::util std::vector> result; - std::size_t const len = input.size(); - std::size_t const n = sep.size(); + const std::size_t len = input.size(); + const std::size_t n = sep.size(); std::size_t i = 0; std::size_t j = 0; @@ -597,8 +619,8 @@ namespace mamba::util std::vector> result; - std::size_t const len = input.size(); - std::size_t const n = sep.size(); + const std::size_t len = input.size(); + const std::size_t n = sep.size(); std::size_t i = len; std::size_t j = len; diff --git a/libmamba/src/util/url.cpp b/libmamba/src/util/url.cpp index 4461f87a9..a33d040c1 100644 --- a/libmamba/src/util/url.cpp +++ b/libmamba/src/util/url.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -180,7 +182,7 @@ namespace mamba::util file_uri_unc2_to_unc4(url), CURLU_NON_SUPPORT_SCHEME | CURLU_DEFAULT_SCHEME, }; - out.set_scheme(handle.get_part(CURLUPART_SCHEME).value_or(std::string(URL::https))); + out.set_scheme(handle.get_part(CURLUPART_SCHEME).value_or("")); out.set_user(handle.get_part(CURLUPART_USER).value_or(""), Encode::no); out.set_password(handle.get_part(CURLUPART_PASSWORD).value_or(""), Encode::no); out.set_host(handle.get_part(CURLUPART_HOST).value_or("")); @@ -192,20 +194,34 @@ namespace mamba::util return out; } - auto URL::scheme() const -> const std::string& + auto URL::scheme_is_defaulted() const -> bool { + return m_scheme.empty(); + } + + auto URL::scheme() const -> std::string_view + { + if (scheme_is_defaulted()) + { + return https; + } return m_scheme; } void URL::set_scheme(std::string_view scheme) { - if (scheme.empty()) - { - throw std::invalid_argument("Cannot set empty scheme"); - } m_scheme = util::to_lower(util::rstrip(scheme)); } + auto URL::clear_scheme() -> std::string + { + if (scheme_is_defaulted()) + { + return std::string(https); + } + return std::exchange(m_scheme, ""); + } + auto URL::user(Decode::no_type) const -> const std::string& { return m_user; @@ -256,16 +272,74 @@ namespace mamba::util return std::exchange(m_password, ""); } + namespace + { + template + auto + authentication_elems_impl(URL::Credentials credentials, UGetter&& get_user, PGetter&& get_password) + { + switch (credentials) + { + case (URL::Credentials::Show): + { + Str user = get_user(); + Str pass = user.empty() ? "" : get_password(); + Str sep = pass.empty() ? "" : ":"; + return std::array{ std::move(user), std::move(sep), std::move(pass) }; + } + case (URL::Credentials::Hide): + { + Str user = get_user(); + Str pass = user.empty() ? "" : "*****"; + Str sep = user.empty() ? "" : ":"; + return std::array{ std::move(user), std::move(sep), std::move(pass) }; + } + case (URL::Credentials::Remove): + { + return std::array{ "", "", "" }; + } + } + assert(false); + throw std::invalid_argument("Invalid enum number"); + } + } + + auto URL::authentication_elems(Credentials credentials, Decode::no_type) const + -> std::array + { + return authentication_elems_impl( + credentials, + [&]() -> std::string_view { return user(Decode::no); }, + [&]() -> std::string_view { return password(Decode::no); } + ); + } + + auto URL::authentication_elems(Credentials credentials, Decode::yes_type) const + -> std::array + { + return authentication_elems_impl( + credentials, + [&]() -> std::string { return user(Decode::yes); }, + [&]() -> std::string { return password(Decode::yes); } + ); + } + auto URL::authentication() const -> std::string { - const auto& u = user(Decode::no); - const auto& p = password(Decode::no); - return p.empty() ? u : util::concat(u, ':', p); + return std::apply( + [](auto&&... elem) { return util::concat(std::forward(elem)...); }, + authentication_elems(Credentials::Show, Decode::no) + ); + } + + auto URL::host_is_defaulted() const -> bool + { + return m_host.empty(); } auto URL::host(Decode::no_type) const -> std::string_view { - if ((m_scheme != "file") && m_host.empty()) + if ((scheme() != "file") && host_is_defaulted()) { return localhost; } @@ -295,12 +369,9 @@ namespace mamba::util auto URL::clear_host() -> std::string { - // Cheap == comparison that works because of class invariant - if (auto l_host = host(Decode::no); l_host.data() != m_host.data()) + if (host_is_defaulted()) { - auto out = std::string(l_host); - set_host("", Encode::no); - return out; + return std::string(host(Decode::no)); } return std::exchange(m_host, ""); } @@ -324,19 +395,50 @@ namespace mamba::util return std::exchange(m_port, ""); } - auto URL::authority() const -> std::string + namespace { - const auto& l_user = user(Decode::no); - const auto& l_pass = password(Decode::no); - const auto& l_host = host(Decode::no); - return util::concat( - l_user, - l_pass.empty() ? "" : ":", - l_pass, - l_user.empty() ? "" : "@", - l_host, - m_port.empty() ? "" : ":", - m_port + template + auto authority_elems_impl(std::array user_sep_pass, Str host, Str port) + { + const bool has_auth = !user_sep_pass[0].empty(); + const bool has_port = !port.empty(); + return std::array{ + std::move(user_sep_pass[0]), + std::move(user_sep_pass[1]), + std::move(user_sep_pass[2]), + Str(has_auth ? "@" : ""), + std::move(host), + Str(has_port ? ":" : ""), + std::move(port), + }; + } + } + + auto URL::authority_elems(Credentials credentials, Decode::no_type) const + -> std::array + { + return authority_elems_impl( + authentication_elems(credentials, Decode::no), + host(Decode::no), + port() + ); + } + + auto URL::authority_elems(Credentials credentials, Decode::yes_type) const + -> std::array + { + return authority_elems_impl( + authentication_elems(credentials, Decode::yes), + host(Decode::yes), + port() + ); + } + + auto URL::authority(Credentials credentials) const -> std::string + { + return std::apply( + [](auto&&... elem) { return util::concat(std::forward(elem)...); }, + authority_elems(credentials, Decode::no) ); } @@ -396,7 +498,7 @@ namespace mamba::util auto URL::pretty_path() const -> std::string { // All paths start with a '/' except those like "file:///C:/folder/file.txt" - if (m_scheme == "file") + if (on_win && scheme() == "file") { assert(util::starts_with(m_path, '/')); auto path_no_slash = url_decode(std::string_view(m_path).substr(1)); @@ -464,18 +566,19 @@ namespace mamba::util return std::exchange(m_fragment, ""); } - auto URL::str() const -> std::string + auto URL::str(Credentials credentials) const -> std::string { + std::array authority = authority_elems(credentials, Decode::no); return util::concat( scheme(), "://", - user(Decode::no), - m_password.empty() ? "" : ":", - password(Decode::no), - m_user.empty() ? "" : "@", - host(Decode::no), - m_port.empty() ? "" : ":", - port(), + authority[0], + authority[1], + authority[2], + authority[3], + authority[4], + authority[5], + authority[6], path(Decode::no), m_query.empty() ? "" : "?", m_query, @@ -488,7 +591,7 @@ namespace mamba::util { std::string computed_path = {}; // When stripping file scheme, not showing leading '/' for Windows path with drive - if ((m_scheme == "file") && (strip_scheme == StripScheme::yes) && host(Decode::no).empty()) + if ((scheme() == "file") && (strip_scheme == StripScheme::yes) && host(Decode::no).empty()) { computed_path = pretty_path(); } @@ -500,22 +603,20 @@ namespace mamba::util return computed_path; } - auto - URL::pretty_str(StripScheme strip_scheme, char rstrip_path, HideConfidential hide_confidential) const + auto URL::pretty_str(StripScheme strip_scheme, char rstrip_path, Credentials credentials) const -> std::string { + std::array authority = authority_elems(credentials, Decode::yes); return util::concat( - (strip_scheme == StripScheme::no) ? m_scheme : "", + (strip_scheme == StripScheme::no) ? scheme() : "", (strip_scheme == StripScheme::no) ? "://" : "", - user(Decode::yes), - m_password.empty() ? "" : ":", - password(Decode::no).empty() - ? "" - : ((hide_confidential == HideConfidential::no) ? password(Decode::yes) : "*****"), - m_user.empty() ? "" : "@", - host(Decode::yes), - m_port.empty() ? "" : ":", - m_port, + authority[0], + authority[1], + authority[2], + authority[3], + authority[4], + authority[5], + authority[6], pretty_str_path(strip_scheme, rstrip_path), m_query.empty() ? "" : "?", m_query, diff --git a/libmamba/src/util/url_manip.cpp b/libmamba/src/util/url_manip.cpp index a1b256828..b1f201a7f 100644 --- a/libmamba/src/util/url_manip.cpp +++ b/libmamba/src/util/url_manip.cpp @@ -15,6 +15,7 @@ #include #include +#include "mamba/core/environment.hpp" #include "mamba/fs/filesystem.hpp" #include "mamba/util/build.hpp" #include "mamba/util/path_manip.hpp" @@ -256,9 +257,18 @@ namespace mamba::util return util::concat(file_scheme, url_encode(path, '/')); } + auto abs_path_or_url_to_url(std::string_view path) -> std::string + { + if (url_has_scheme(path)) + { + return std::string(path); + } + return abs_path_to_url(path); + } + auto path_to_url(std::string_view path) -> std::string { - return abs_path_to_url(fs::absolute(path).string()); + return abs_path_to_url(fs::absolute(env::expand_user(path)).lexically_normal().string()); } auto path_or_url_to_url(std::string_view path) -> std::string diff --git a/libmamba/tests/CMakeLists.txt b/libmamba/tests/CMakeLists.txt index 3a2a96486..0514cc90b 100644 --- a/libmamba/tests/CMakeLists.txt +++ b/libmamba/tests/CMakeLists.txt @@ -45,6 +45,7 @@ set(LIBMAMBA_TEST_SRCS src/specs/test_conda_url.cpp src/specs/test_version.cpp src/specs/test_version_spec.cpp + src/specs/test_channel_spec.cpp src/specs/test_repo_data.cpp ../longpath.manifest diff --git a/libmamba/tests/src/core/test_channel.cpp b/libmamba/tests/src/core/test_channel.cpp index 0770688d6..124803e88 100644 --- a/libmamba/tests/src/core/test_channel.cpp +++ b/libmamba/tests/src/core/test_channel.cpp @@ -1,3 +1,9 @@ +// Copyright (c) 2023, QuantStack and Mamba Contributors +// +// Distributed under the terms of the BSD 3-Clause License. +// +// The full license is in the file LICENSE, distributed with this software. + #include #include @@ -5,7 +11,7 @@ #include "mamba/core/channel.hpp" #include "mamba/core/context.hpp" #include "mamba/core/environment.hpp" -#include "mamba/specs/platform.hpp" +#include "mamba/util/flat_set.hpp" #include "mambatests.hpp" @@ -13,6 +19,8 @@ namespace mamba { static const std::string platform = std::string(specs::build_platform_name()); + using PlatformSet = typename util::flat_set; + using UrlSet = typename util::flat_set; static_assert(std::is_move_constructible_v); static_assert(std::is_move_assignable_v); @@ -25,10 +33,7 @@ namespace mamba // make_simple_channel ChannelContext channel_context{ mambatests::context() }; const auto& ch = channel_context.get_channel_alias(); - CHECK_EQ(ch.scheme(), "https"); - CHECK_EQ(ch.location(), "conda.anaconda.org"); - CHECK_EQ(ch.name(), ""); - CHECK_EQ(ch.canonical_name(), ""); + CHECK_EQ(ch.str(), "https://conda.anaconda.org/"); const auto& custom = channel_context.get_custom_channels(); @@ -61,10 +66,7 @@ namespace mamba ChannelContext channel_context{ mambatests::context() }; const auto& ch = channel_context.get_channel_alias(); - CHECK_EQ(ch.scheme(), "https"); - CHECK_EQ(ch.location(), "mydomain.com/channels"); - CHECK_EQ(ch.name(), ""); - CHECK_EQ(ch.canonical_name(), ""); + CHECK_EQ(ch.str(), "https://mydomain.com/channels/"); const auto& custom = channel_context.get_custom_channels(); @@ -81,7 +83,7 @@ namespace mamba CHECK_EQ(c.name(), "conda-forge"); CHECK_EQ(c.canonical_name(), "conda-forge"); // CHECK_EQ(c.url(), "conda-forge"); - CHECK_EQ(c.platforms(), std::vector({ platform, "noarch" })); + CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); ctx.channel_alias = "https://conda.anaconda.org"; } @@ -98,8 +100,7 @@ namespace mamba ChannelContext channel_context{ ctx }; auto base = std::string("https://ali.as/prefix-and-more/"); auto& chan = channel_context.make_channel(base); - std::vector expected_urls = { base + platform, base + "noarch" }; - CHECK_EQ(chan.urls(), expected_urls); + CHECK_EQ(chan.urls(), UrlSet{ base + platform, base + "noarch" }); ctx.channel_alias = "https://conda.anaconda.org"; ctx.custom_channels.clear(); @@ -119,10 +120,7 @@ namespace mamba ChannelContext channel_context{ ctx }; const auto& ch = channel_context.get_channel_alias(); - CHECK_EQ(ch.scheme(), "https"); - CHECK_EQ(ch.location(), "mydomain.com/channels"); - CHECK_EQ(ch.name(), ""); - CHECK_EQ(ch.canonical_name(), ""); + CHECK_EQ(ch.str(), "https://mydomain.com/channels/"); { std::string value = "test_channel"; @@ -131,9 +129,11 @@ namespace mamba CHECK_EQ(c.location(), "/tmp"); CHECK_EQ(c.name(), "test_channel"); CHECK_EQ(c.canonical_name(), "test_channel"); - CHECK_EQ(c.platforms(), std::vector({ platform, "noarch" })); - std::vector exp_urls({ std::string("file:///tmp/test_channel/") + platform, - std::string("file:///tmp/test_channel/noarch") }); + CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); + const UrlSet exp_urls({ + std::string("file:///tmp/test_channel/") + platform, + "file:///tmp/test_channel/noarch", + }); CHECK_EQ(c.urls(), exp_urls); } @@ -144,11 +144,11 @@ namespace mamba CHECK_EQ(c.location(), "conda.mydomain.xyz"); CHECK_EQ(c.name(), "some_channel"); CHECK_EQ(c.canonical_name(), "some_channel"); - CHECK_EQ(c.platforms(), std::vector({ platform, "noarch" })); - std::vector exp_urls( - { std::string("https://conda.mydomain.xyz/some_channel/") + platform, - std::string("https://conda.mydomain.xyz/some_channel/noarch") } - ); + CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); + const UrlSet exp_urls({ + std::string("https://conda.mydomain.xyz/some_channel/") + platform, + "https://conda.mydomain.xyz/some_channel/noarch", + }); CHECK_EQ(c.urls(), exp_urls); } @@ -164,12 +164,12 @@ namespace mamba ctx.custom_multichannels["xtest"] = std::vector{ "https://mydomain.com/conda-forge", "https://mydomain.com/bioconda", - "https://mydomain.com/snakepit" + "https://mydomain.com/snakepit", }; ctx.custom_multichannels["ytest"] = std::vector{ "https://otherdomain.com/conda-forge", "https://otherdomain.com/bioconda", - "https://otherdomain.com/snakepit" + "https://otherdomain.com/snakepit", }; ChannelContext channel_context{ ctx }; @@ -179,17 +179,17 @@ namespace mamba CHECK_EQ(x.size(), 3); auto* c1 = x[0]; - std::vector exp_urls( - { std::string("https://mydomain.com/conda-forge/") + platform, - std::string("https://mydomain.com/conda-forge/noarch") } - ); + const UrlSet exp_urls({ + std::string("https://mydomain.com/conda-forge/") + platform, + "https://mydomain.com/conda-forge/noarch", + }); CHECK_EQ(c1->urls(), exp_urls); - std::vector exp_urlsy3( - { std::string("https://otherdomain.com/snakepit/") + platform, - std::string("https://otherdomain.com/snakepit/noarch") } - ); + const UrlSet exp_urlsy3({ + std::string("https://otherdomain.com/snakepit/") + platform, + "https://otherdomain.com/snakepit/noarch", + }); auto y = channel_context.get_channels({ "ytest" }); auto* y3 = y[2]; @@ -225,24 +225,24 @@ namespace mamba auto* c2 = x[1]; auto* c3 = x[2]; - std::vector exp_urls( - { std::string("https://condaforge.org/channels/conda-forge/") + platform, - std::string("https://condaforge.org/channels/conda-forge/noarch") } - ); + const UrlSet exp_urls({ + std::string("https://condaforge.org/channels/conda-forge/") + platform, + "https://condaforge.org/channels/conda-forge/noarch", + }); CHECK_EQ(c1->urls(), exp_urls); - std::vector exp_urls2( - { std::string("https://mydomain.com/bioconda/") + platform, - std::string("https://mydomain.com/bioconda/noarch") } - ); + const UrlSet exp_urls2({ + std::string("https://mydomain.com/bioconda/") + platform, + "https://mydomain.com/bioconda/noarch", + }); CHECK_EQ(c2->urls(), exp_urls2); - std::vector exp_urls3( - { std::string("https://mydomain.xyz/xyzchannel/xyz/") + platform, - std::string("https://mydomain.xyz/xyzchannel/xyz/noarch") } - ); + const UrlSet exp_urls3({ + std::string("https://mydomain.xyz/xyzchannel/xyz/") + platform, + "https://mydomain.xyz/xyzchannel/xyz/noarch", + }); CHECK_EQ(c3->urls(), exp_urls3); @@ -262,17 +262,17 @@ namespace mamba const Channel* c2 = x[1]; CHECK_EQ(c1->name(), "pkgs/main"); - std::vector exp_urls( - { std::string("https://repo.anaconda.com/pkgs/main/") + platform, - std::string("https://repo.anaconda.com/pkgs/main/noarch") } - ); + const UrlSet exp_urls({ + std::string("https://repo.anaconda.com/pkgs/main/") + platform, + "https://repo.anaconda.com/pkgs/main/noarch", + }); CHECK_EQ(c1->urls(), exp_urls); CHECK_EQ(c2->name(), "pkgs/r"); - std::vector exp_urls2( - { std::string("https://repo.anaconda.com/pkgs/r/") + platform, - std::string("https://repo.anaconda.com/pkgs/r/noarch") } - ); + const UrlSet exp_urls2({ + std::string("https://repo.anaconda.com/pkgs/r/") + platform, + "https://repo.anaconda.com/pkgs/r/noarch", + }); CHECK_EQ(c2->urls(), exp_urls2); CHECK_EQ(c1->location(), "repo.anaconda.com"); @@ -285,8 +285,10 @@ namespace mamba TEST_CASE("custom_default_channels") { auto& ctx = mambatests::context(); - ctx.default_channels = { "https://mamba.com/test/channel", - "https://mamba.com/stable/channel" }; + ctx.default_channels = { + "https://mamba.com/test/channel", + "https://mamba.com/stable/channel", + }; ChannelContext channel_context{ ctx }; auto x = channel_context.get_channels({ "defaults" }); @@ -294,15 +296,15 @@ namespace mamba const Channel* c2 = x[1]; CHECK_EQ(c1->name(), "test/channel"); - std::vector exp_urls( - { std::string("https://mamba.com/test/channel/") + platform, - std::string("https://mamba.com/test/channel/noarch") } - ); + const UrlSet exp_urls({ + std::string("https://mamba.com/test/channel/") + platform, + "https://mamba.com/test/channel/noarch", + }); CHECK_EQ(c1->urls(), exp_urls); - std::vector exp_urls2( - { std::string("https://mamba.com/stable/channel/") + platform, - std::string("https://mamba.com/stable/channel/noarch") } - ); + const UrlSet exp_urls2({ + std::string("https://mamba.com/stable/channel/") + platform, + "https://mamba.com/stable/channel/noarch", + }); CHECK_EQ(c2->urls(), exp_urls2); CHECK_EQ(c2->name(), "stable/channel"); @@ -360,11 +362,11 @@ namespace mamba CHECK_EQ(c.location(), "server.com/private/channels"); CHECK_EQ(c.name(), "test_channel"); CHECK_EQ(c.canonical_name(), "test_channel"); - CHECK_EQ(c.platforms(), std::vector({ platform, "noarch" })); - std::vector exp_urls( - { std::string("https://server.com/private/channels/test_channel/") + platform, - std::string("https://server.com/private/channels/test_channel/noarch") } - ); + CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); + const UrlSet exp_urls({ + std::string("https://server.com/private/channels/test_channel/") + platform, + "https://server.com/private/channels/test_channel/noarch", + }); CHECK_EQ(c.urls(), exp_urls); } @@ -375,13 +377,12 @@ namespace mamba CHECK_EQ(c.location(), "server.com/private/channels"); CHECK_EQ(c.name(), "test_channel/mylabel/xyz"); CHECK_EQ(c.canonical_name(), "test_channel/mylabel/xyz"); - CHECK_EQ(c.platforms(), std::vector({ platform, "noarch" })); - std::vector exp_urls( - { std::string("https://server.com/private/channels/test_channel/mylabel/xyz/") - + platform, - std::string("https://server.com/private/channels/test_channel/mylabel/xyz/noarch" - ) } - ); + CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); + const UrlSet exp_urls({ + std::string("https://server.com/private/channels/test_channel/mylabel/xyz/") + + platform, + "https://server.com/private/channels/test_channel/mylabel/xyz/noarch", + }); CHECK_EQ(c.urls(), exp_urls); } @@ -392,12 +393,12 @@ namespace mamba CHECK_EQ(c.location(), "server.com/random/channels"); CHECK_EQ(c.name(), "random/test_channel/pkg"); CHECK_EQ(c.canonical_name(), "random/test_channel/pkg"); - CHECK_EQ(c.platforms(), std::vector({ platform, "noarch" })); - std::vector exp_urls( - { std::string("https://server.com/random/channels/random/test_channel/pkg/") - + platform, - std::string("https://server.com/random/channels/random/test_channel/pkg/noarch") } - ); + CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); + const UrlSet exp_urls({ + std::string("https://server.com/random/channels/random/test_channel/pkg/") + + platform, + "https://server.com/random/channels/random/test_channel/pkg/noarch", + }); CHECK_EQ(c.urls(), exp_urls); } @@ -417,7 +418,7 @@ namespace mamba CHECK_EQ(c.location(), "repo.mamba.pm"); CHECK_EQ(c.name(), "conda-forge"); CHECK_EQ(c.canonical_name(), "https://repo.mamba.pm/conda-forge"); - CHECK_EQ(c.platforms(), std::vector({ platform, "noarch" })); + CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); } TEST_CASE("make_channel") @@ -429,7 +430,7 @@ namespace mamba CHECK_EQ(c.location(), "conda.anaconda.org"); CHECK_EQ(c.name(), "conda-forge"); CHECK_EQ(c.canonical_name(), "conda-forge"); - CHECK_EQ(c.platforms(), std::vector({ platform, "noarch" })); + CHECK_EQ(c.platforms(), PlatformSet({ platform, "noarch" })); std::string value2 = "https://repo.anaconda.com/pkgs/main[" + platform + "]"; const Channel& c2 = channel_context.make_channel(value2); @@ -437,7 +438,7 @@ namespace mamba CHECK_EQ(c2.location(), "repo.anaconda.com"); CHECK_EQ(c2.name(), "pkgs/main"); CHECK_EQ(c2.canonical_name(), "https://repo.anaconda.com/pkgs/main"); - CHECK_EQ(c2.platforms(), std::vector({ platform })); + CHECK_EQ(c2.platforms(), PlatformSet({ platform })); std::string value3 = "https://conda.anaconda.org/conda-forge[" + platform + "]"; const Channel& c3 = channel_context.make_channel(value3); @@ -445,7 +446,7 @@ namespace mamba CHECK_EQ(c3.location(), c.location()); CHECK_EQ(c3.name(), c.name()); CHECK_EQ(c3.canonical_name(), c.canonical_name()); - CHECK_EQ(c3.platforms(), std::vector({ platform })); + CHECK_EQ(c3.platforms(), PlatformSet({ platform })); std::string value4 = "/home/mamba/test/channel_b"; const Channel& c4 = channel_context.make_channel(value4); @@ -459,7 +460,7 @@ namespace mamba CHECK_EQ(c4.canonical_name(), "file:///home/mamba/test/channel_b"); #endif CHECK_EQ(c4.name(), "channel_b"); - CHECK_EQ(c4.platforms(), std::vector({ platform, "noarch" })); + CHECK_EQ(c4.platforms(), PlatformSet({ platform, "noarch" })); std::string value5 = "/home/mamba/test/channel_b[" + platform + "]"; const Channel& c5 = channel_context.make_channel(value5); @@ -472,25 +473,22 @@ namespace mamba CHECK_EQ(c5.canonical_name(), "file:///home/mamba/test/channel_b"); #endif CHECK_EQ(c5.name(), "channel_b"); - CHECK_EQ(c5.platforms(), std::vector({ platform })); + CHECK_EQ(c5.platforms(), PlatformSet({ platform })); std::string value6a = "http://localhost:8000/conda-forge[noarch]"; const Channel& c6a = channel_context.make_channel(value6a); - CHECK_EQ( - c6a.urls(false), - std::vector({ "http://localhost:8000/conda-forge/noarch" }) - ); + CHECK_EQ(c6a.urls(false), UrlSet({ "http://localhost:8000/conda-forge/noarch" })); std::string value6b = "http://localhost:8000/conda_mirror/conda-forge[noarch]"; const Channel& c6b = channel_context.make_channel(value6b); CHECK_EQ( c6b.urls(false), - std::vector({ "http://localhost:8000/conda_mirror/conda-forge/noarch" }) + UrlSet({ "http://localhost:8000/conda_mirror/conda-forge/noarch" }) ); std::string value7 = "conda-forge[noarch,arbitrary]"; const Channel& c7 = channel_context.make_channel(value7); - CHECK_EQ(c7.platforms(), std::vector({ "noarch", "arbitrary" })); + CHECK_EQ(c7.platforms(), PlatformSet({ "noarch", "arbitrary" })); } TEST_CASE("urls") @@ -500,16 +498,20 @@ namespace mamba const Channel& c = channel_context.make_channel(value); CHECK_EQ( c.urls(), - std::vector({ "https://conda.anaconda.org/conda-forge/noarch", - "https://conda.anaconda.org/conda-forge/win-64", - "https://conda.anaconda.org/conda-forge/arbitrary" }) + UrlSet({ + "https://conda.anaconda.org/conda-forge/arbitrary", + "https://conda.anaconda.org/conda-forge/noarch", + "https://conda.anaconda.org/conda-forge/win-64", + }) ); const Channel& c1 = channel_context.make_channel("https://conda.anaconda.org/conda-forge"); CHECK_EQ( c1.urls(), - std::vector({ "https://conda.anaconda.org/conda-forge/" + platform, - "https://conda.anaconda.org/conda-forge/noarch" }) + UrlSet({ + "https://conda.anaconda.org/conda-forge/" + platform, + "https://conda.anaconda.org/conda-forge/noarch", + }) ); } @@ -524,13 +526,9 @@ namespace mamba CHECK_EQ(chan.token(), "my-12345-token"); CHECK_EQ( chan.urls(true), - std::vector{ - { "https://conda.anaconda.org/t/my-12345-token/conda-forge/noarch" } } - ); - CHECK_EQ( - chan.urls(false), - std::vector{ { "https://conda.anaconda.org/conda-forge/noarch" } } + UrlSet({ "https://conda.anaconda.org/t/my-12345-token/conda-forge/noarch" }) ); + CHECK_EQ(chan.urls(false), UrlSet({ "https://conda.anaconda.org/conda-forge/noarch" })); } TEST_CASE("add_multiple_tokens") @@ -555,8 +553,7 @@ namespace mamba const Channel& c = channel_context.make_channel("C:\\test\\channel"); CHECK_EQ( c.urls(false), - std::vector({ "file:///C:/test/channel/win-64", - "file:///C:/test/channel/noarch" }) + UrlSet({ "file:///C:/test/channel/win-64", "file:///C:/test/channel/noarch" }) ); } else @@ -564,8 +561,8 @@ namespace mamba const Channel& c = channel_context.make_channel("/test/channel"); CHECK_EQ( c.urls(false), - std::vector({ std::string("file:///test/channel/") + platform, - "file:///test/channel/noarch" }) + UrlSet({ std::string("file:///test/channel/") + platform, + "file:///test/channel/noarch" }) ); } } @@ -576,8 +573,8 @@ namespace mamba const Channel& c = channel_context.make_channel("http://localhost:8000/"); CHECK_EQ(c.platform_url("win-64", false), "http://localhost:8000/win-64"); CHECK_EQ(c.base_url(), "http://localhost:8000"); - std::vector expected_urls({ std::string("http://localhost:8000/") + platform, - "http://localhost:8000/noarch" }); + const UrlSet expected_urls({ std::string("http://localhost:8000/") + platform, + "http://localhost:8000/noarch" }); CHECK_EQ(c.urls(true), expected_urls); const Channel& c4 = channel_context.make_channel("http://localhost:8000"); CHECK_EQ(c4.platform_url("linux-64", false), "http://localhost:8000/linux-64"); @@ -593,11 +590,11 @@ namespace mamba "https://localhost:8000/t/xy-12345678-1234-1234-1234-123456789012/win-64" ); - std::vector expected_urls2( - { std::string("https://localhost:8000/t/xy-12345678-1234-1234-1234-123456789012/") - + platform, - "https://localhost:8000/t/xy-12345678-1234-1234-1234-123456789012/noarch" } - ); + const UrlSet expected_urls2({ + std::string("https://localhost:8000/t/xy-12345678-1234-1234-1234-123456789012/") + + platform, + "https://localhost:8000/t/xy-12345678-1234-1234-1234-123456789012/noarch", + }); CHECK_EQ(c3.urls(true), expected_urls2); } diff --git a/libmamba/tests/src/doctest-printer/conda_url.hpp b/libmamba/tests/src/doctest-printer/conda_url.hpp new file mode 100644 index 000000000..fec64683b --- /dev/null +++ b/libmamba/tests/src/doctest-printer/conda_url.hpp @@ -0,0 +1,21 @@ +// Copyright (c) 2023, QuantStack and Mamba Contributors +// +// Distributed under the terms of the BSD 3-Clause License. +// +// The full license is in the file LICENSE, distributed with this software. + +#include + +#include "mamba/specs/conda_url.hpp" + +namespace doctest +{ + template <> + struct StringMaker + { + static auto convert(const mamba::specs::CondaURL& value) -> String + { + return { value.str().c_str() }; + } + }; +} diff --git a/libmamba/tests/src/doctest-printer/flat_set.hpp b/libmamba/tests/src/doctest-printer/flat_set.hpp new file mode 100644 index 000000000..696c37f67 --- /dev/null +++ b/libmamba/tests/src/doctest-printer/flat_set.hpp @@ -0,0 +1,22 @@ +// Copyright (c) 2023, QuantStack and Mamba Contributors +// +// Distributed under the terms of the BSD 3-Clause License. +// +// The full license is in the file LICENSE, distributed with this software. + +#include +#include + +#include "mamba/util/flat_set.hpp" + +namespace doctest +{ + template + struct StringMaker> + { + static auto convert(const mamba::util::flat_set& value) -> String + { + return { fmt::format("std::flat_set{{{}}}", fmt::join(value, ", ")).c_str() }; + } + }; +} diff --git a/libmamba/tests/src/doctest-printer/vector.hpp b/libmamba/tests/src/doctest-printer/vector.hpp index 0eb5cf042..cb06beb52 100644 --- a/libmamba/tests/src/doctest-printer/vector.hpp +++ b/libmamba/tests/src/doctest-printer/vector.hpp @@ -11,10 +11,10 @@ namespace doctest { - template - struct StringMaker> + template + struct StringMaker> { - static auto convert(const std::vector& value) -> String + static auto convert(const std::vector& value) -> String { return { fmt::format("std::vector{{{}}}", fmt::join(value, ", ")).c_str() }; } diff --git a/libmamba/tests/src/specs/test_channel_spec.cpp b/libmamba/tests/src/specs/test_channel_spec.cpp new file mode 100644 index 000000000..144fb2327 --- /dev/null +++ b/libmamba/tests/src/specs/test_channel_spec.cpp @@ -0,0 +1,195 @@ +// Copyright (c) 2023, QuantStack and Mamba Contributors +// +// Distributed under the terms of the BSD 3-Clause License. +// +// The full license is in the file LICENSE, distributed with this software. + +#include + +#include "mamba/fs/filesystem.hpp" +#include "mamba/specs/channel_spec.hpp" +#include "mamba/util/build.hpp" +#include "mamba/util/path_manip.hpp" +#include "mamba/util/string.hpp" + +using namespace mamba; +using namespace mamba::specs; + +TEST_SUITE("specs::channel_spec") +{ + TEST_CASE("Parsing") + { + using Type = typename ChannelSpec::Type; + using PlatformSet = typename util::flat_set; + + SUBCASE("https://repo.anaconda.com/conda-forge") + { + const auto spec = ChannelSpec::parse("https://repo.anaconda.com/conda-forge"); + CHECK_EQ(spec.type(), Type::URL); + CHECK_EQ(spec.location(), "https://repo.anaconda.com/conda-forge"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + + SUBCASE("https://repo.anaconda.com/conda-forge/osx-64") + { + const auto spec = ChannelSpec::parse("https://repo.anaconda.com/conda-forge/osx-64"); + CHECK_EQ(spec.type(), Type::URL); + CHECK_EQ(spec.location(), "https://repo.anaconda.com/conda-forge"); + CHECK_EQ(spec.platform_filters(), PlatformSet{ "osx-64" }); + } + + SUBCASE("https://repo.anaconda.com/conda-forge[win-64|noarch]") + { + const auto spec = ChannelSpec::parse("https://repo.anaconda.com/conda-forge[win-64|noarch]" + ); + CHECK_EQ(spec.type(), Type::URL); + CHECK_EQ(spec.location(), "https://repo.anaconda.com/conda-forge"); + CHECK_EQ(spec.platform_filters(), PlatformSet{ "win-64", "noarch" }); + } + + SUBCASE("https://repo.anaconda.com/conda-forge/linux-64/pkg-0.0-bld.conda") + { + const auto spec = ChannelSpec::parse( + "https://repo.anaconda.com/conda-forge/linux-64/pkg-0.0-bld.conda" + ); + CHECK_EQ(spec.type(), Type::PackageURL); + CHECK_EQ(spec.location(), "https://repo.anaconda.com/conda-forge/linux-64/pkg-0.0-bld.conda"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + + SUBCASE("file:///Users/name/conda") + { + const auto spec = ChannelSpec::parse("file:///Users/name/conda"); + CHECK_EQ(spec.type(), Type::Path); + CHECK_EQ(spec.location(), "file:///Users/name/conda"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + + SUBCASE("file:///Users/name/conda[linux-64]") + { + const auto spec = ChannelSpec::parse("file:///Users/name/conda[linux-64]"); + CHECK_EQ(spec.type(), Type::Path); + CHECK_EQ(spec.location(), "file:///Users/name/conda"); + CHECK_EQ(spec.platform_filters(), PlatformSet{ "linux-64" }); + } + + SUBCASE("file://C:/Users/name/conda") + { + if (util::on_win) + { + const auto spec = ChannelSpec::parse("file://C:/Users/name/conda"); + CHECK_EQ(spec.type(), Type::Path); + CHECK_EQ(spec.location(), "file://C:/Users/name/conda"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + } + + SUBCASE("/Users/name/conda") + { + const auto spec = ChannelSpec::parse("/Users/name/conda"); + CHECK_EQ(spec.type(), Type::Path); + CHECK_EQ(spec.location(), "/Users/name/conda"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + + SUBCASE("./folder/../folder/.") + { + const auto spec = ChannelSpec::parse("./folder/../folder/."); + CHECK_EQ(spec.type(), Type::Path); + CHECK_EQ(spec.location(), "folder"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + + SUBCASE("~/folder/") + { + const auto spec = ChannelSpec::parse("~/folder/"); + CHECK_EQ(spec.type(), Type::Path); + CHECK_EQ(spec.location(), "~/folder"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + + SUBCASE("/tmp/pkg-0.0-bld.tar.bz2") + { + const auto spec = ChannelSpec::parse("/tmp/pkg-0.0-bld.tar.bz2"); + CHECK_EQ(spec.type(), Type::PackagePath); + CHECK_EQ(spec.location(), "/tmp/pkg-0.0-bld.tar.bz2"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + + SUBCASE("C:/tmp//pkg-0.0-bld.tar.bz2") + { + const auto spec = ChannelSpec::parse("C:/tmp//pkg-0.0-bld.tar.bz2"); + CHECK_EQ(spec.type(), Type::PackagePath); + CHECK_EQ(spec.location(), "C:/tmp/pkg-0.0-bld.tar.bz2"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + + SUBCASE(R"(C:\tmp\pkg-0.0-bld.tar.bz2)") + { + if (util::on_win) + { + const auto spec = ChannelSpec::parse(R"(C:\tmp\pkg-0.0-bld.tar.bz2)"); + CHECK_EQ(spec.type(), Type::PackagePath); + CHECK_EQ(spec.location(), "C:/tmp/pkg-0.0-bld.tar.bz2"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + } + + SUBCASE("conda-forge") + { + const auto spec = ChannelSpec::parse("conda-forge"); + CHECK_EQ(spec.type(), Type::Name); + CHECK_EQ(spec.location(), "conda-forge"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + + SUBCASE("repo.anaconda.com") + { + const auto spec = ChannelSpec::parse("repo.anaconda.com"); + // Unintuitive but correct type, this is not a URL. Better explicit than clever. + CHECK_EQ(spec.type(), Type::Name); + CHECK_EQ(spec.location(), "repo.anaconda.com"); + CHECK_EQ(spec.platform_filters(), PlatformSet{}); + } + + SUBCASE("conda-forge/linux-64") + { + const auto spec = ChannelSpec::parse("conda-forge/linux-64"); + CHECK_EQ(spec.type(), Type::Name); + CHECK_EQ(spec.location(), "conda-forge"); + CHECK_EQ(spec.platform_filters(), PlatformSet{ "linux-64" }); + } + + SUBCASE("conda-forge[linux-avx512]") + { + const auto spec = ChannelSpec::parse("conda-forge[linux-avx512]"); + CHECK_EQ(spec.type(), Type::Name); + CHECK_EQ(spec.location(), "conda-forge"); + CHECK_EQ(spec.platform_filters(), PlatformSet{ "linux-avx512" }); + } + + SUBCASE("conda-forge[]") + { + const auto spec = ChannelSpec::parse("conda-forge[linux-64]"); + CHECK_EQ(spec.type(), Type::Name); + CHECK_EQ(spec.location(), "conda-forge"); + CHECK_EQ(spec.platform_filters(), PlatformSet{ "linux-64" }); + } + + SUBCASE("conda-forge/linux-64/label/foo_dev") + { + const auto spec = ChannelSpec::parse("conda-forge/linux-64/label/foo_dev"); + CHECK_EQ(spec.type(), Type::Name); + CHECK_EQ(spec.location(), "conda-forge/label/foo_dev"); + CHECK_EQ(spec.platform_filters(), PlatformSet{ "linux-64" }); + } + + SUBCASE("conda-forge/label/foo_dev[linux-64]") + { + const auto spec = ChannelSpec::parse("conda-forge/label/foo_dev[linux-64]"); + CHECK_EQ(spec.type(), Type::Name); + CHECK_EQ(spec.location(), "conda-forge/label/foo_dev"); + CHECK_EQ(spec.platform_filters(), PlatformSet{ "linux-64" }); + } + } +} diff --git a/libmamba/tests/src/specs/test_conda_url.cpp b/libmamba/tests/src/specs/test_conda_url.cpp index e71cce4f6..2120d809e 100644 --- a/libmamba/tests/src/specs/test_conda_url.cpp +++ b/libmamba/tests/src/specs/test_conda_url.cpp @@ -23,12 +23,15 @@ TEST_SUITE("specs::CondaURL") { url.set_path("/folder/file.txt"); CHECK_EQ(url.token(), ""); + CHECK_EQ(url.path_without_token(), "/folder/file.txt"); url.set_token("mytoken"); CHECK_EQ(url.token(), "mytoken"); + CHECK_EQ(url.path_without_token(), "/folder/file.txt"); CHECK_EQ(url.path(), "/t/mytoken/folder/file.txt"); CHECK(url.clear_token()); + CHECK_EQ(url.path_without_token(), "/folder/file.txt"); CHECK_EQ(url.path(), "/folder/file.txt"); } @@ -36,12 +39,14 @@ TEST_SUITE("specs::CondaURL") { url.set_path("/t/xy-12345678-1234/conda-forge/linux-64"); CHECK_EQ(url.token(), "xy-12345678-1234"); + CHECK_EQ(url.path_without_token(), "/conda-forge/linux-64"); SUBCASE("Cannot set invalid token") { CHECK_THROWS_AS(url.set_token(""), std::invalid_argument); CHECK_THROWS_AS(url.set_token("?fds:g"), std::invalid_argument); CHECK_EQ(url.token(), "xy-12345678-1234"); + CHECK_EQ(url.path_without_token(), "/conda-forge/linux-64"); CHECK_EQ(url.path(), "/t/xy-12345678-1234/conda-forge/linux-64"); } @@ -49,6 +54,7 @@ TEST_SUITE("specs::CondaURL") { CHECK(url.clear_token()); CHECK_EQ(url.token(), ""); + CHECK_EQ(url.path_without_token(), "/conda-forge/linux-64"); CHECK_EQ(url.path(), "/conda-forge/linux-64"); } @@ -56,6 +62,7 @@ TEST_SUITE("specs::CondaURL") { url.set_token("abcd"); CHECK_EQ(url.token(), "abcd"); + CHECK_EQ(url.path_without_token(), "/conda-forge/linux-64"); CHECK_EQ(url.path(), "/t/abcd/conda-forge/linux-64"); } } @@ -67,23 +74,61 @@ TEST_SUITE("specs::CondaURL") url.set_token("abcd"); CHECK_EQ(url.token(), "abcd"); - CHECK_EQ(url.path(), "/t/abcd"); + CHECK_EQ(url.path_without_token(), "/"); + CHECK_EQ(url.path(), "/t/abcd/"); CHECK(url.clear_token()); + CHECK_EQ(url.token(), ""); + CHECK_EQ(url.path_without_token(), "/"); CHECK_EQ(url.path(), "/"); } SUBCASE("https://repo.mamba.pm/bar/t/xy-12345678-1234-1234-1234-123456789012/") { url.set_path("/bar/t/xy-12345678-1234-1234-1234-123456789012/"); - CHECK_EQ(url.token(), "xy-12345678-1234-1234-1234-123456789012"); + CHECK_EQ(url.token(), ""); // Not at begining of path url.set_token("abcd"); CHECK_EQ(url.token(), "abcd"); - CHECK_EQ(url.path(), "/bar/t/abcd/"); + CHECK_EQ(url.path_without_token(), "/bar/t/xy-12345678-1234-1234-1234-123456789012/"); + CHECK_EQ(url.path(), "/t/abcd/bar/t/xy-12345678-1234-1234-1234-123456789012/"); CHECK(url.clear_token()); - CHECK_EQ(url.path(), "/bar/"); + CHECK_EQ(url.path_without_token(), "/bar/t/xy-12345678-1234-1234-1234-123456789012/"); + CHECK_EQ(url.path(), "/bar/t/xy-12345678-1234-1234-1234-123456789012/"); + } + } + + TEST_CASE("Path without token") + { + CondaURL url{}; + url.set_scheme("https"); + url.set_host("repo.mamba.pm"); + + SUBCASE("Setters") + { + url.set_path_without_token("foo"); + CHECK_EQ(url.path_without_token(), "/foo"); + url.set_token("mytoken"); + CHECK_EQ(url.path_without_token(), "/foo"); + CHECK(url.clear_path_without_token()); + CHECK_EQ(url.path_without_token(), "/"); + } + + SUBCASE("Parse") + { + url = CondaURL::parse("mamba.org/t/xy-12345678-1234-1234-1234-123456789012"); + CHECK_EQ(url.token(), "xy-12345678-1234-1234-1234-123456789012"); + CHECK_EQ(url.path_without_token(), "/"); + CHECK_EQ(url.path(), "/t/xy-12345678-1234-1234-1234-123456789012/"); + } + + SUBCASE("Encoding") + { + url.set_token("mytoken"); + url.set_path_without_token("some / weird/path %"); + CHECK_EQ(url.path_without_token(), "/some / weird/path %"); + CHECK_EQ(url.path_without_token(CondaURL::Decode::no), "/some%20/%20weird/path%20%25"); } } @@ -99,6 +144,7 @@ TEST_SUITE("specs::CondaURL") CHECK_EQ(url.platform_name(), ""); CHECK_THROWS_AS(url.set_platform(Platform::linux_64), std::invalid_argument); + CHECK_EQ(url.path_without_token(), "/"); CHECK_EQ(url.path(), "/"); CHECK_FALSE(url.clear_platform()); @@ -251,6 +297,42 @@ TEST_SUITE("specs::CondaURL") } } + TEST_CASE("str options") + { + CondaURL url = {}; + + SUBCASE("without credentials") + { + CHECK_EQ(url.str(CondaURL::Credentials::Show), "https://localhost/"); + CHECK_EQ(url.str(CondaURL::Credentials::Hide), "https://localhost/"); + CHECK_EQ(url.str(CondaURL::Credentials::Remove), "https://localhost/"); + } + + SUBCASE("with some credentials") + { + url.set_user("user@mamba.org"); + url.set_password("pass"); + + CHECK_EQ(url.str(CondaURL::Credentials::Show), "https://user%40mamba.org:pass@localhost/"); + CHECK_EQ(url.str(CondaURL::Credentials::Hide), "https://user%40mamba.org:*****@localhost/"); + CHECK_EQ(url.str(CondaURL::Credentials::Remove), "https://localhost/"); + + SUBCASE("and token") + { + url.set_path("/t/abcd1234/linux-64"); + CHECK_EQ( + url.str(CondaURL::Credentials::Show), + "https://user%40mamba.org:pass@localhost/t/abcd1234/linux-64" + ); + CHECK_EQ( + url.str(CondaURL::Credentials::Hide), + "https://user%40mamba.org:*****@localhost/t/*****/linux-64" + ); + CHECK_EQ(url.str(CondaURL::Credentials::Remove), "https://localhost/linux-64"); + } + } + } + TEST_CASE("pretty_str options") { SUBCASE("scheme option") @@ -283,30 +365,60 @@ TEST_SUITE("specs::CondaURL") CHECK_EQ(url.pretty_str(CondaURL::StripScheme::no, '/'), "https://mamba.org/page"); } - SUBCASE("Hide confidential option") + SUBCASE("Credentail option") { CondaURL url = {}; - url.set_user("user"); - url.set_password("pass"); - CHECK_EQ( - url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::HideConfidential::no), - "https://user:pass@localhost/" - ); - CHECK_EQ( - url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::HideConfidential::yes), - "https://user:*****@localhost/" - ); - url.set_path("/custom/t/abcd1234/linux-64"); - CHECK_EQ( - url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::HideConfidential::no), - "https://user:pass@localhost/custom/t/abcd1234/linux-64" - ); + SUBCASE("without credentials") + { + CHECK_EQ( + url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::Credentials::Show), + "https://localhost/" + ); + CHECK_EQ( + url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::Credentials::Hide), + "https://localhost/" + ); + CHECK_EQ( + url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::Credentials::Remove), + "https://localhost/" + ); + } - CHECK_EQ( - url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::HideConfidential::yes), - "https://user:*****@localhost/custom/t/*****/linux-64" - ); + SUBCASE("with user:password") + { + url.set_user("user"); + url.set_password("pass"); + CHECK_EQ( + url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::Credentials::Show), + "https://user:pass@localhost/" + ); + CHECK_EQ( + url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::Credentials::Hide), + "https://user:*****@localhost/" + ); + CHECK_EQ( + url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::Credentials::Remove), + "https://localhost/" + ); + + SUBCASE("and token") + { + url.set_path("/t/abcd1234/linux-64"); + CHECK_EQ( + url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::Credentials::Show), + "https://user:pass@localhost/t/abcd1234/linux-64" + ); + CHECK_EQ( + url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::Credentials::Hide), + "https://user:*****@localhost/t/*****/linux-64" + ); + CHECK_EQ( + url.pretty_str(CondaURL::StripScheme::no, 0, CondaURL::Credentials::Remove), + "https://localhost/linux-64" + ); + } + } } SUBCASE("https://user:password@mamba.org:8080/folder/file.html?param=value#fragment") diff --git a/libmamba/tests/src/specs/test_version.cpp b/libmamba/tests/src/specs/test_version.cpp index d22a78b30..f42d95688 100644 --- a/libmamba/tests/src/specs/test_version.cpp +++ b/libmamba/tests/src/specs/test_version.cpp @@ -16,7 +16,7 @@ using namespace mamba::specs; -TEST_SUITE("version") +TEST_SUITE("specs::version") { TEST_CASE("atom_comparison") { diff --git a/libmamba/tests/src/specs/test_version_spec.cpp b/libmamba/tests/src/specs/test_version_spec.cpp index 21689d921..9d4443f5f 100644 --- a/libmamba/tests/src/specs/test_version_spec.cpp +++ b/libmamba/tests/src/specs/test_version_spec.cpp @@ -14,7 +14,7 @@ using namespace mamba::specs; -TEST_SUITE("version_spec") +TEST_SUITE("specs::version_spec") { TEST_CASE("VersionPredicate") { diff --git a/libmamba/tests/src/util/test_flat_set.cpp b/libmamba/tests/src/util/test_flat_set.cpp index 128e9e5f7..303301023 100644 --- a/libmamba/tests/src/util/test_flat_set.cpp +++ b/libmamba/tests/src/util/test_flat_set.cpp @@ -12,6 +12,8 @@ #include "mamba/util/flat_set.hpp" +#include "doctest-printer/flat_set.hpp" + using namespace mamba::util; TEST_SUITE("util::flat_set") @@ -35,7 +37,6 @@ TEST_SUITE("util::flat_set") static_assert(std::is_same_v); } - TEST_CASE("equality") { CHECK_EQ(flat_set(), flat_set()); diff --git a/libmamba/tests/src/util/test_path_manip.cpp b/libmamba/tests/src/util/test_path_manip.cpp index 332f2d0c3..fc116b928 100644 --- a/libmamba/tests/src/util/test_path_manip.cpp +++ b/libmamba/tests/src/util/test_path_manip.cpp @@ -69,4 +69,24 @@ TEST_SUITE("util::path_manip") CHECK_EQ(path_to_posix(R"(folder/weird\file)"), R"(folder/weird\file)"); } } + + TEST_CASE("path_is_prefix") + { + CHECK(path_is_prefix("", "")); + CHECK(path_is_prefix("", "folder")); + + CHECK(path_is_prefix("folder", "folder")); + CHECK(path_is_prefix("/", "/folder")); + CHECK(path_is_prefix("/folder", "/folder")); + CHECK(path_is_prefix("/folder/file.txt", "/folder/file.txt")); + CHECK(path_is_prefix("folder/file.txt", "folder/file.txt")); + + CHECK_FALSE(path_is_prefix("/folder", "/")); + CHECK_FALSE(path_is_prefix("/folder", "/folder-more")); + CHECK_FALSE(path_is_prefix("/folder/file.json", "/folder/file.txt")); + CHECK_FALSE(path_is_prefix("folder/file.json", "folder/file.txt")); + + // Debatable + CHECK_FALSE(path_is_prefix("/folder/", "/folder")); + } } diff --git a/libmamba/tests/src/util/test_string.cpp b/libmamba/tests/src/util/test_string.cpp index 7d052200f..59aa18d2b 100644 --- a/libmamba/tests/src/util/test_string.cpp +++ b/libmamba/tests/src/util/test_string.cpp @@ -74,6 +74,28 @@ namespace mamba::util CHECK(contains("", "")); // same as Python ``"" in ""`` } + TEST_CASE("split_prefix") + { + using PrefixTail = decltype(split_prefix("", "")); + CHECK_EQ(split_prefix("", ""), PrefixTail{ "", "" }); + CHECK_EQ(split_prefix("hello", ""), PrefixTail{ "", "hello" }); + CHECK_EQ(split_prefix("hello", "hello"), PrefixTail{ "hello", "" }); + CHECK_EQ(split_prefix("", "hello"), PrefixTail{ "", "" }); + CHECK_EQ( + split_prefix("https://localhost", "https://"), + PrefixTail{ "https://", "localhost" } + ); + CHECK_EQ( + split_prefix("https://localhost", "http://"), + PrefixTail{ "", "https://localhost" } + ); + CHECK_EQ(split_prefix("aabb", "a"), PrefixTail{ "a", "abb" }); + CHECK_EQ(split_prefix("", 'a'), PrefixTail{ "", "" }); + CHECK_EQ(split_prefix("a", 'a'), PrefixTail{ "a", "" }); + CHECK_EQ(split_prefix("aaa", 'a'), PrefixTail{ "a", "aa" }); + CHECK_EQ(split_prefix("aabb", 'b'), PrefixTail{ "", "aabb" }); + } + TEST_CASE("remove_prefix") { CHECK_EQ(remove_prefix("", ""), ""); @@ -89,6 +111,22 @@ namespace mamba::util CHECK_EQ(remove_prefix("aabb", 'b'), "aabb"); } + TEST_CASE("split_suffix") + { + using HeadSuffix = decltype(split_suffix("", "")); + CHECK_EQ(split_suffix("", ""), HeadSuffix{ "", "" }); + CHECK_EQ(split_suffix("hello", ""), HeadSuffix{ "hello", "" }); + CHECK_EQ(split_suffix("hello", "hello"), HeadSuffix{ "", "hello" }); + CHECK_EQ(split_suffix("", "hello"), HeadSuffix{ "", "" }); + CHECK_EQ(split_suffix("localhost:8080", ":8080"), HeadSuffix{ "localhost", ":8080" }); + CHECK_EQ(split_suffix("localhost:8080", ":80"), HeadSuffix{ "localhost:8080", "" }); + CHECK_EQ(split_suffix("aabb", "b"), HeadSuffix{ "aab", "b" }); + CHECK_EQ(split_suffix("", 'b'), HeadSuffix{ "", "" }); + CHECK_EQ(split_suffix("b", 'b'), HeadSuffix{ "", "b" }); + CHECK_EQ(split_suffix("bbb", 'b'), HeadSuffix{ "bb", "b" }); + CHECK_EQ(split_suffix("aabb", 'a'), HeadSuffix{ "aabb", "" }); + } + TEST_CASE("remove_suffix") { CHECK_EQ(remove_suffix("", ""), ""); diff --git a/libmamba/tests/src/util/test_url.cpp b/libmamba/tests/src/util/test_url.cpp index 7a3993d56..3eaca7eb9 100644 --- a/libmamba/tests/src/util/test_url.cpp +++ b/libmamba/tests/src/util/test_url.cpp @@ -5,7 +5,6 @@ // The full license is in the file LICENSE, distributed with this software. #include -#include #include #include @@ -109,7 +108,14 @@ TEST_SUITE("util::URL") url.set_scheme("file"); url.set_path("C:/folder/file.txt"); CHECK_EQ(url.path(), "/C:/folder/file.txt"); - CHECK_EQ(url.pretty_path(), "C:/folder/file.txt"); + if (on_win) + { + CHECK_EQ(url.pretty_path(), "C:/folder/file.txt"); + } + else + { + CHECK_EQ(url.pretty_path(), "/C:/folder/file.txt"); + } } SUBCASE("Case") @@ -121,10 +127,55 @@ TEST_SUITE("util::URL") CHECK_EQ(url.host(), "some_host.com"); } + SUBCASE("Default scheme") + { + URL url{}; + CHECK(url.scheme_is_defaulted()); + CHECK_EQ(url.scheme(), "https"); + + url.set_scheme("https"); + CHECK_FALSE(url.scheme_is_defaulted()); + CHECK_EQ(url.scheme(), "https"); + + url.set_scheme(""); + CHECK(url.scheme_is_defaulted()); + url.set_scheme("https"); + + url.set_scheme("ftp"); + CHECK_FALSE(url.scheme_is_defaulted()); + CHECK_EQ(url.scheme(), "ftp"); + + CHECK_EQ(url.clear_scheme(), "ftp"); + CHECK(url.scheme_is_defaulted()); + url.set_scheme("https"); + } + + SUBCASE("Default host") + { + URL url{}; + CHECK(url.host_is_defaulted()); + CHECK_EQ(url.host(), "localhost"); + + url.set_host("localhost"); + CHECK_FALSE(url.host_is_defaulted()); + CHECK_EQ(url.host(), "localhost"); + + url.set_host(""); + CHECK(url.host_is_defaulted()); + url.set_host("localhost"); + + url.set_host("test.org"); + CHECK_FALSE(url.host_is_defaulted()); + CHECK_EQ(url.host(), "test.org"); + + CHECK_EQ(url.clear_host(), "test.org"); + CHECK(url.host_is_defaulted()); + url.set_host("localhost"); + } + SUBCASE("Invalid") { URL url{}; - CHECK_THROWS_AS(url.set_scheme(""), std::invalid_argument); CHECK_THROWS_AS(url.set_port("not-a-number"), std::invalid_argument); } @@ -261,7 +312,14 @@ TEST_SUITE("util::URL") CHECK_EQ(url.scheme(), "file"); CHECK_EQ(url.host(), ""); CHECK_EQ(url.path(), "/C:/Users/wolfv/test/document.json"); - CHECK_EQ(url.pretty_path(), "C:/Users/wolfv/test/document.json"); + if (on_win) + { + CHECK_EQ(url.pretty_path(), "C:/Users/wolfv/test/document.json"); + } + else + { + CHECK_EQ(url.pretty_path(), "/C:/Users/wolfv/test/document.json"); + } CHECK_EQ(url.user(), ""); CHECK_EQ(url.password(), ""); CHECK_EQ(url.port(), ""); @@ -331,6 +389,28 @@ TEST_SUITE("util::URL") } } + TEST_CASE("str options") + { + URL url = {}; + + SUBCASE("without credentials") + { + CHECK_EQ(url.str(URL::Credentials::Show), "https://localhost/"); + CHECK_EQ(url.str(URL::Credentials::Hide), "https://localhost/"); + CHECK_EQ(url.str(URL::Credentials::Remove), "https://localhost/"); + } + + SUBCASE("with some credentials") + { + url.set_user("user@mamba.org"); + url.set_password("pass"); + + CHECK_EQ(url.str(URL::Credentials::Show), "https://user%40mamba.org:pass@localhost/"); + CHECK_EQ(url.str(URL::Credentials::Hide), "https://user%40mamba.org:*****@localhost/"); + CHECK_EQ(url.str(URL::Credentials::Remove), "https://localhost/"); + } + } + TEST_CASE("pretty_str options") { SUBCASE("scheme option") @@ -363,19 +443,44 @@ TEST_SUITE("util::URL") CHECK_EQ(url.pretty_str(URL::StripScheme::no, '/'), "https://mamba.org/page"); } - SUBCASE("Hide password option") + SUBCASE("Credential option") { URL url = {}; - url.set_user("user"); - url.set_password("pass"); - CHECK_EQ( - url.pretty_str(URL::StripScheme::no, 0, URL::HideConfidential::no), - "https://user:pass@localhost/" - ); - CHECK_EQ( - url.pretty_str(URL::StripScheme::no, 0, URL::HideConfidential::yes), - "https://user:*****@localhost/" - ); + + SUBCASE("without credentials") + { + CHECK_EQ( + url.pretty_str(URL::StripScheme::no, 0, URL::Credentials::Show), + "https://localhost/" + ); + CHECK_EQ( + url.pretty_str(URL::StripScheme::no, 0, URL::Credentials::Hide), + "https://localhost/" + ); + CHECK_EQ( + url.pretty_str(URL::StripScheme::no, 0, URL::Credentials::Remove), + "https://localhost/" + ); + } + + SUBCASE("with some credentials") + { + url.set_user("user"); + url.set_password("pass"); + + CHECK_EQ( + url.pretty_str(URL::StripScheme::no, 0, URL::Credentials::Show), + "https://user:pass@localhost/" + ); + CHECK_EQ( + url.pretty_str(URL::StripScheme::no, 0, URL::Credentials::Hide), + "https://user:*****@localhost/" + ); + CHECK_EQ( + url.pretty_str(URL::StripScheme::no, 0, URL::Credentials::Remove), + "https://localhost/" + ); + } } } @@ -457,7 +562,14 @@ TEST_SUITE("util::URL") CHECK_EQ(url.str(), "file:///C%3A/folder%26/file.txt"); } CHECK_EQ(url.pretty_str(), "file:///C:/folder&/file.txt"); - CHECK_EQ(url.pretty_str(URL::StripScheme::yes), "C:/folder&/file.txt"); + if (on_win) + { + CHECK_EQ(url.pretty_str(URL::StripScheme::yes), "C:/folder&/file.txt"); + } + else + { + CHECK_EQ(url.pretty_str(URL::StripScheme::yes), "/C:/folder&/file.txt"); + } } SUBCASE("https://user@email.com:pw%rd@mamba.org/some /path$/") @@ -492,12 +604,27 @@ TEST_SUITE("util::URL") url.set_query("param=value"); url.set_fragment("fragment"); CHECK_EQ(url.authority(), "mamba.org"); + CHECK_EQ(url.authority(URL::Credentials::Show), "mamba.org"); + CHECK_EQ(url.authority(URL::Credentials::Hide), "mamba.org"); + CHECK_EQ(url.authority(URL::Credentials::Remove), "mamba.org"); + url.set_port("8000"); CHECK_EQ(url.authority(), "mamba.org:8000"); + CHECK_EQ(url.authority(URL::Credentials::Show), "mamba.org:8000"); + CHECK_EQ(url.authority(URL::Credentials::Hide), "mamba.org:8000"); + CHECK_EQ(url.authority(URL::Credentials::Remove), "mamba.org:8000"); + url.set_user("user@email.com"); CHECK_EQ(url.authority(), "user%40email.com@mamba.org:8000"); - url.set_password("password"); - CHECK_EQ(url.authority(), "user%40email.com:password@mamba.org:8000"); + CHECK_EQ(url.authority(URL::Credentials::Show), "user%40email.com@mamba.org:8000"); + CHECK_EQ(url.authority(URL::Credentials::Hide), "user%40email.com:*****@mamba.org:8000"); + CHECK_EQ(url.authority(URL::Credentials::Remove), "mamba.org:8000"); + + url.set_password("pass"); + CHECK_EQ(url.authority(), "user%40email.com:pass@mamba.org:8000"); + CHECK_EQ(url.authority(URL::Credentials::Show), "user%40email.com:pass@mamba.org:8000"); + CHECK_EQ(url.authority(URL::Credentials::Hide), "user%40email.com:*****@mamba.org:8000"); + CHECK_EQ(url.authority(URL::Credentials::Remove), "mamba.org:8000"); } TEST_CASE("Equality") diff --git a/libmamba/tests/src/util/test_url_manip.cpp b/libmamba/tests/src/util/test_url_manip.cpp index c0397832b..79f60ce35 100644 --- a/libmamba/tests/src/util/test_url_manip.cpp +++ b/libmamba/tests/src/util/test_url_manip.cpp @@ -179,6 +179,19 @@ TEST_SUITE("util::url_manip") } } + TEST_CASE("abs_path_or_url_to_url") + { + SUBCASE("/users/test/miniconda3") + { + CHECK_EQ(abs_path_or_url_to_url("/users/test/miniconda3"), "file:///users/test/miniconda3"); + } + + SUBCASE("file:///tmp/bar") + { + CHECK_EQ(abs_path_or_url_to_url("file:///tmp/bar"), "file:///tmp/bar"); + } + } + TEST_CASE("path_to_url") { const std::string win_drive = fs::absolute(fs::u8path("/")).string().substr(0, 1); @@ -216,6 +229,21 @@ TEST_SUITE("util::url_manip") CHECK_EQ(url, "file:///tmp/foo%20bar"); } } + + SUBCASE("./folder/./../folder") + { + auto url = path_to_url("./folder/./../folder"); + if (on_win) + { + CHECK(starts_with(url, concat("file://", win_drive, ":/"))); + CHECK(ends_with(url, "/folder")); + } + else + { + const auto expected_folder = fs::absolute("folder").lexically_normal(); + CHECK_EQ(url, concat("file://", expected_folder.string())); + } + } } TEST_CASE("path_or_url_to_url") diff --git a/libmambapy/libmambapy/__init__.pyi b/libmambapy/libmambapy/__init__.pyi index 2f5cd2a84..5a3fbd75f 100644 --- a/libmambapy/libmambapy/__init__.pyi +++ b/libmambapy/libmambapy/__init__.pyi @@ -136,8 +136,8 @@ class Channel: def platform_url(self, platform: str, with_credentials: bool = True) -> str: ... def platform_urls( self, with_credentials: bool = True - ) -> typing.List[typing.Tuple[str, str]]: ... - def urls(self, with_credentials: bool = True) -> typing.List[str]: ... + ) -> typing.Set[typing.Tuple[str, str]]: ... + def urls(self, with_credentials: bool = True) -> typing.Set[str]: ... @property def auth(self) -> typing.Optional[str]: """ @@ -164,9 +164,9 @@ class Channel: :type: typing.Optional[str] """ @property - def platforms(self) -> typing.List[str]: + def platforms(self) -> typing.Set[str]: """ - :type: typing.List[str] + :type: typing.Set[str] """ @property def scheme(self) -> str: diff --git a/libmambapy/src/flat_set_caster.hpp b/libmambapy/src/flat_set_caster.hpp new file mode 100644 index 000000000..8c7236f27 --- /dev/null +++ b/libmambapy/src/flat_set_caster.hpp @@ -0,0 +1,25 @@ +// Copyright (c) 2023, QuantStack and Mamba Contributors +// +// Distributed under the terms of the BSD 3-Clause License. +// +// The full license is in the file LICENSE, distributed with this software. + +#include + +#include "mamba/util/flat_set.hpp" + +#ifndef MAMBA_PY_SET_CASTER_HPP +#define MAMBA_PY_SET_CASTER_HPP + +namespace PYBIND11_NAMESPACE +{ + namespace detail + { + template + struct type_caster> + : set_caster, Key> + { + }; + } +} +#endif diff --git a/libmambapy/src/main.cpp b/libmambapy/src/main.cpp index 05e84edb5..cc28a3c7c 100644 --- a/libmambapy/src/main.cpp +++ b/libmambapy/src/main.cpp @@ -38,6 +38,8 @@ #include "mamba/util/flat_set.hpp" #include "mamba/util/string.hpp" +#include "flat_set_caster.hpp" + namespace py = pybind11; namespace query @@ -52,18 +54,6 @@ namespace query }; } -namespace PYBIND11_NAMESPACE -{ - namespace detail - { - template - struct type_caster> - : set_caster, Key> - { - }; - } -} - void deprecated(const char* message) { diff --git a/mamba/tests/testserver.sh b/mamba/tests/testserver.sh index 2d11ab529..c0e7052a0 100755 --- a/mamba/tests/testserver.sh +++ b/mamba/tests/testserver.sh @@ -81,6 +81,6 @@ python "${reposerver}" \ -d "${repo}" -n defaults --token private-token -- \ -d "${channel_a}" -n channel_a --user user@email.com --password test -- \ -d "${channel_b}" -n channel_b --auth none & PID=$! -mamba create -y -q -n "env-${RANDOM}" --override-channels -c http://localhost:8000/defaults/t/private-token test-package --json +mamba create -y -q -n "env-${RANDOM}" --override-channels -c http://localhost:8000/t/private-token/defaults test-package --json mamba create -y -q -n "env-${RANDOM}" --override-channels -c http://user%40email.com:test@localhost:8000/channel_a _r-mutex --json kill -TERM $PID diff --git a/micromamba/src/env.cpp b/micromamba/src/env.cpp index b87b2b42f..880fcce03 100644 --- a/micromamba/src/env.cpp +++ b/micromamba/src/env.cpp @@ -173,7 +173,7 @@ set_env_command(CLI::App* com, Configuration& config) continue; } - const Channel& channel = channel_context.make_channel(v.url); + const Channel& channel = channel_context.make_channel(v.channel); if (from_history) {