Add CondaURL (#2805)

* No build pattern in util::URL

* URL move string for already encoded operations

* Add exclusion characters to util::url_encode

* Add encoding of URL path

* Simplify URL file empty host handling

* Add raw URL::str

* Add path_get_drive_letter

* Url encode with char exclude

* Handle Windows drive encoding

* Add URL::clear_xxx

* Add CondaURL

* Add specs::known_platforms

* Fix CondaURL::token

* Add CondaURL::platform

* Fix pybind tests

* Add CondaURL::package

* Rename specs/url.hpp > specs/conda_url.hpp
This commit is contained in:
Antoine Prouvost 2023-09-07 16:59:46 +02:00 committed by GitHub
parent 69a2cd30e2
commit a527511c14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1337 additions and 316 deletions

View File

@ -70,6 +70,7 @@ QualifierAlignment: Custom # Experimental
QualifierOrder: [inline, static, constexpr, const, volatile, type]
ReflowComments: 'true'
SortIncludes: CaseInsensitive
SortUsingDeclarations: Never
SpaceAfterCStyleCast: 'true'
SpaceAfterTemplateKeyword: 'true'
SpaceBeforeAssignmentOperators: 'true'

View File

@ -76,6 +76,7 @@ jobs:
cache-environment: true
create-args: >-
conda-build
pre-commit
python=${{ matrix.python_version }}
- uses: hendrikmuhs/ccache-action@main
with:

View File

@ -137,6 +137,7 @@ set(LIBMAMBA_SOURCES
# Implementation of version and matching specs
${LIBMAMBA_SOURCE_DIR}/specs/archive.cpp
${LIBMAMBA_SOURCE_DIR}/specs/platform.cpp
${LIBMAMBA_SOURCE_DIR}/specs/conda_url.cpp
${LIBMAMBA_SOURCE_DIR}/specs/version.cpp
${LIBMAMBA_SOURCE_DIR}/specs/version_spec.cpp
${LIBMAMBA_SOURCE_DIR}/specs/repo_data.cpp
@ -226,6 +227,7 @@ set(LIBMAMBA_PUBLIC_HEADERS
# Implementation of version and matching specs
${LIBMAMBA_INCLUDE_DIR}/mamba/specs/archive.hpp
${LIBMAMBA_INCLUDE_DIR}/mamba/specs/platform.hpp
${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/repo_data.hpp

View File

@ -0,0 +1,141 @@
// 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_CONDA_URL_HPP
#define MAMBA_SPECS_CONDA_URL_HPP
#include <string_view>
#include "mamba/specs/platform.hpp"
#include "mamba/util/url.hpp"
namespace mamba::specs
{
class CondaURL : private util::URL
{
using Base = typename util::URL;
public:
using Base::StripScheme;
using Base::HidePassword;
using Base::Encode;
using Base::Decode;
using Base::https;
using Base::localhost;
inline static constexpr std::string_view token_prefix = "/t/";
[[nodiscard]] static auto parse(std::string_view url) -> CondaURL;
/** Create a local URL. */
CondaURL() = default;
using Base::scheme;
using Base::set_scheme;
using Base::user;
using Base::set_user;
using Base::clear_user;
using Base::password;
using Base::set_password;
using Base::clear_password;
using Base::authentication;
using Base::host;
using Base::set_host;
using Base::clear_host;
using Base::port;
using Base::set_port;
using Base::clear_port;
using Base::authority;
using Base::path;
using Base::set_path;
using Base::clear_path;
using Base::append_path;
using Base::query;
using Base::set_query;
using Base::clear_query;
using Base::fragment;
using Base::set_fragment;
using Base::clear_fragment;
/** Return the Conda token, as delimited with "/t/", or empty if there isn't any. */
[[nodiscard]] auto token() const -> std::string_view;
/** Set a token if the URL already contains one, or throw an error. */
void set_token(std::string_view token);
/** Clear the token and return ``true`` if it exists, otherwise return ``false``. */
auto clear_token() -> bool;
/** Return the platform if part of the URL path. */
[[nodiscard]] auto platform() const -> std::optional<Platform>;
/**
* Return the platform if part of the URL path, or empty.
*
* If a platform is found, it is returned as a view onto the path without normalization
* (for instance the capitalization isn't changed).
*/
[[nodiscard]] auto platform_name() const -> std::string_view;
/** Set the platform if the URL already contains one, or throw an error. */
void set_platform(Platform platform);
/**
* Set the platform if the URL already contains one, or throw an error.
*
* If the input @p platform is a valid platform, it is inserted as it is into the path
* (for instance the capitalization isn't changed).
*/
void set_platform(std::string_view platform);
/** Clear the token and return true if it exists, otherwise return ``false``. */
auto clear_platform() -> bool;
/**
* Return the encoded package name, or empty otherwise.
*
* Package name are at the end of the path and end with a archive extension.
*
* @see has_archive_extension
*/
[[nodiscard]] auto package(Decode::yes_type = Decode::yes) const -> std::string;
/**
* Return the decoded package name, or empty otherwise.
*
* Package name are at the end of the path and end with a archive extension.
*
* @see has_archive_extension
*/
[[nodiscard]] auto package(Decode::no_type) const -> std::string_view;
/**
* Change the package file name with a not encoded value.
*
* If a package file name is present, replace it, otherwise add it at the end
* of the path.
*/
void set_package(std::string_view pkg, Encode::yes_type = Encode::yes);
/**
* Change the package file name with already encoded value.
*
* If a package file name is present, replace it, otherwise add it at the end
* of the path.
*/
void set_package(std::string_view pkg, Encode::no_type);
/** Clear the package and return true if it exists, otherwise return ``false``. */
auto clear_package() -> bool;
private:
explicit CondaURL(URL&& url);
void set_platform_no_check_input(std::string_view platform);
};
}
#endif

View File

@ -8,6 +8,7 @@
#ifndef MAMBA_SPECS_PLATFORM_HPP
#define MAMBA_SPECS_PLATFORM_HPP
#include <array>
#include <optional>
#include <string_view>
@ -15,16 +16,22 @@
namespace mamba::specs
{
/**
* All platforms known to Conda.
*
* When one platform name is the substring of another, the longest appears first so that
* it makes it easier to use in a parser.
*/
enum class Platform
{
noarch,
noarch = 0,
linux_32,
linux_64,
linux_armv6l,
linux_armv7l,
linux_aarch64,
linux_ppc64,
linux_ppc64le,
linux_ppc64,
linux_s390x,
linux_riscv32,
linux_riscv64,
@ -33,12 +40,24 @@ namespace mamba::specs
win_32,
win_64,
win_arm64,
// For reflexion purposes only
count_,
};
constexpr auto known_platforms_count() -> std::size_t
{
return static_cast<std::size_t>(Platform::count_);
}
constexpr auto known_platforms() -> std::array<Platform, known_platforms_count()>;
constexpr auto known_platform_names() -> std::array<std::string_view, known_platforms_count()>;
/**
* Convert the enumeration to its conda string.
*/
auto platform_name(Platform p) -> std::string_view;
constexpr auto platform_name(Platform p) -> std::string_view;
/**
* Return the enum matching the platform name.
@ -61,5 +80,71 @@ namespace mamba::specs
*/
void from_json(const nlohmann::json& j, Platform& p);
/********************
* Implementation *
********************/
constexpr auto platform_name(Platform p) -> std::string_view
{
switch (p)
{
case Platform::noarch:
return "noarch";
case Platform::linux_32:
return "linux-32";
case Platform::linux_64:
return "linux-64";
case Platform::linux_armv6l:
return "linux-armv6l";
case Platform::linux_armv7l:
return "linux-armv7l";
case Platform::linux_aarch64:
return "linux-aarch64";
case Platform::linux_ppc64:
return "linux-ppc64";
case Platform::linux_ppc64le:
return "linux-ppc64le";
case Platform::linux_s390x:
return "linux-s390x";
case Platform::linux_riscv32:
return "linux-riscv32";
case Platform::linux_riscv64:
return "linux-riscv64";
case Platform::osx_64:
return "osx-64";
case Platform::osx_arm64:
return "osx-arm64";
case Platform::win_32:
return "win-32";
case Platform::win_64:
return "win-64";
case Platform::win_arm64:
return "win-arm64";
default:
return "";
}
}
constexpr auto known_platforms() -> std::array<Platform, known_platforms_count()>
{
auto out = std::array<Platform, known_platforms_count()>{};
for (std::size_t idx = 0; idx < out.size(); ++idx)
{
out[idx] = static_cast<Platform>(idx);
}
return out;
}
constexpr auto known_platform_names() -> std::array<std::string_view, known_platforms_count()>
{
auto out = std::array<std::string_view, known_platforms_count()>{};
auto iter = out.begin();
for (auto p : known_platforms())
{
*(iter++) = platform_name(p);
}
return out;
}
}
#endif

View File

@ -7,6 +7,7 @@
#ifndef MAMBA_UTIL_PATH_MANIP_HPP
#define MAMBA_UTIL_PATH_MANIP_HPP
#include <optional>
#include <string>
#include <string_view>
@ -25,6 +26,11 @@ namespace mamba::util
*/
[[nodiscard]] auto is_explicit_path(std::string_view input) -> bool;
/**
* Return the path drive letter, if any, or none.
*/
[[nodiscard]] auto path_get_drive_letter(std::string_view path) -> std::optional<char>;
/**
* Check if a Windows path (not URL) starts with a drive letter.
*/

View File

@ -82,6 +82,8 @@ namespace mamba::util
bool ends_with(std::string_view str, std::string_view::value_type c);
bool contains(std::string_view str, std::string_view sub_str);
bool contains(std::string_view str, char c);
bool contains(char c1, char c2);
/**
* Check if any of the strings starts with the prefix.

View File

@ -24,7 +24,11 @@ namespace mamba::util
// clang-format off
enum class StripScheme : bool { no, yes };
enum class HidePassword : bool { no, yes };
enum class Encode : bool { no, yes };
struct Encode
{
inline static constexpr struct yes_type {} yes = {};
inline static constexpr struct no_type {} no = {};
};
struct Decode
{
inline static constexpr struct yes_type {} yes = {};
@ -57,7 +61,7 @@ namespace mamba::util
[[nodiscard]] auto scheme() const -> const std::string&;
/** Set a non-empty scheme. */
auto set_scheme(std::string_view scheme) -> URL&;
void set_scheme(std::string_view scheme);
/** Return the encoded user, or empty if none. */
[[nodiscard]] auto user(Decode::no_type) const -> const std::string&;
@ -65,8 +69,14 @@ namespace mamba::util
/** Retrun the decoded user, or empty if none. */
[[nodiscard]] auto user(Decode::yes_type = Decode::yes) const -> std::string;
/** Set or clear the user. */
auto set_user(std::string_view user, Encode encode = Encode::yes) -> URL&;
/** Set the user from a not encoded value. */
void set_user(std::string_view user, Encode::yes_type = Encode::yes);
/** Set the user from an already encoded value. */
void set_user(std::string user, Encode::no_type);
/** Clear and return the encoded user. */
auto clear_user() -> std::string;
/** Return the encoded password, or empty if none. */
[[nodiscard]] auto password(Decode::no_type) const -> const std::string&;
@ -74,82 +84,133 @@ namespace mamba::util
/** Return the decoded password, or empty if none. */
[[nodiscard]] auto password(Decode::yes_type = Decode::yes) const -> std::string;
/** Set or clear the password. */
auto set_password(std::string_view password, Encode encode = Encode::yes) -> URL&;
/** Set the password from a not encoded value. */
void set_password(std::string_view password, Encode::yes_type = Encode::yes);
/** Set the password from an already encoded value. */
void set_password(std::string password, Encode::no_type);
/** Clear and return the encoded password. */
auto clear_password() -> std::string;
/** Return the encoded basic authentication string. */
[[nodiscard]] auto authentication() const -> std::string;
/** Return the encoded host, always non-empty. */
[[nodiscard]] auto host(Decode::no_type) const -> const std::string&;
/** Return the encoded host, always non-empty except for file scheme. */
[[nodiscard]] auto host(Decode::no_type) const -> std::string_view;
/** Return the decoded host, always non-empty. */
/** Return the decoded host, always non-empty except for file scheme. */
[[nodiscard]] auto host(Decode::yes_type = Decode::yes) const -> std::string;
/** Set a non-empty host. */
auto set_host(std::string_view host, Encode encode = Encode::yes) -> URL&;
/** Set the host from a not encoded value. */
void set_host(std::string_view host, Encode::yes_type = Encode::yes);
/** Set the host from an already encoded value. */
void set_host(std::string host, Encode::no_type);
/** Clear and return the encoded hostname. */
auto clear_host() -> std::string;
/** Return the port, or empty if none. */
[[nodiscard]] auto port() const -> const std::string&;
/** Set or clear the port. */
auto set_port(std::string_view port) -> URL&;
void set_port(std::string_view port);
/** Clear and return the port number. */
auto clear_port() -> std::string;
/** Return the encoded autority part of the URL. */
[[nodiscard]] auto authority() const -> std::string;
/** Return the path, always starts with a '/'. */
[[nodiscard]] auto path() const -> const std::string&;
/** Return the encoded path, always starts with a '/'. */
[[nodiscard]] auto path(Decode::no_type) const -> const std::string&;
/** Return the decoded path, always starts with a '/'. */
[[nodiscard]] auto path(Decode::yes_type = Decode::yes) const -> std::string;
/**
* Return the path.
* 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.
*/
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. */
void set_path(std::string path, Encode::no_type);
/** Clear the path and return the encoded path, always starts with a '/'. */
auto clear_path() -> std::string;
/**
* Return the decoded path.
*
* For a "file" scheme, with a Windows path containing a drive, the leading '/' is
* stripped.
*/
[[nodiscard]] auto pretty_path() const -> std::string_view;
/** Set the path, a leading '/' is added if abscent. */
auto set_path(std::string_view path) -> URL&;
[[nodiscard]] auto pretty_path() const -> std::string;
/**
* Append a sub path to the current path.
* 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.
*/
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 '/'.
*/
auto append_path(std::string_view subpath) -> URL&;
void append_path(std::string_view path, Encode::no_type);
/** Return the query, or empty if none. */
[[nodiscard]] auto query() const -> const std::string&;
/** Set or clear the query. */
auto set_query(std::string_view query) -> URL&;
void set_query(std::string_view query);
/** Clear and return the query. */
auto clear_query() -> std::string;
/** Return the fragment, or empty if none. */
[[nodiscard]] auto fragment() const -> const std::string&;
/** Set or clear the fragment. */
auto set_fragment(std::string_view fragment) -> URL&;
void set_fragment(std::string_view fragment);
/** Clear and return the fragment. */
auto clear_fragment() -> std::string;
/** Return the full, exact, encoded URL. */
[[nodiscard]] auto str() -> std::string;
/**
* Return the full encoded url.
* Return the full decoded url.
*
* Due to decoding, the outcome may not be understood by parser and usable to reach an
* 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_password If true, hide password in the decoded string.
*/
[[nodiscard]] auto
str(StripScheme strip_scheme = StripScheme::no,
[[nodiscard]] auto pretty_str(
StripScheme strip_scheme = StripScheme::no,
char rstrip_path = 0,
HidePassword hide_password = HidePassword::no) const -> std::string;
HidePassword hide_password = HidePassword::no
) const -> std::string;
private:
std::string m_scheme = std::string(https);
std::string m_user = {};
std::string m_password = {};
std::string m_host = std::string(localhost);
std::string m_host = {};
std::string m_path = "/";
std::string m_port = {};
std::string m_query = {};

View File

@ -26,11 +26,15 @@ namespace mamba::util
/**
* Escape reserved URL reserved characters with '%' encoding.
*
* Does not parse URL in any way so '/' in "http://mamba.org/page" get encoded.
* The secons argument can be used to specify characters to exclude from encoding,
* so that for instance path can be encoded without splitting them (if they have no '/' other
* than separators).
*
* @see url_decode
*/
[[nodiscard]] auto url_encode(std::string_view url) -> std::string;
[[nodiscard]] auto url_encode(std::string_view url, std::string_view exclude) -> std::string;
[[nodiscard]] auto url_encode(std::string_view url, char exclude) -> std::string;
/**
* Unescape percent encoded string to their URL reserved characters.

View File

@ -114,22 +114,24 @@ namespace mamba
)
{
auto spath = std::string(util::rstrip(path, '/'));
std::string url = util::URL() //
.set_scheme(scheme)
.set_host(host)
.set_port(port)
.set_path(spath)
.str(util::URL::StripScheme::yes);
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().set_host(host).set_port(port).str(
util::URL::StripScheme::yes,
/* rstrip_path= */ '/'
);
auto l_url = util::URL();
l_url.set_host(host);
l_url.set_port(port);
return channel_configuration{
/* location= */ std::move(l_url),
/* location= */ l_url.pretty_str(util::URL::StripScheme::yes, /* rstrip_path= */ '/'),
/* name= */ "",
/* scheme= */ scheme,
/* auth= */ "",
@ -189,12 +191,11 @@ namespace mamba
// Case 7: fallback, channel_location = host:port and channel_name = path
spath = util::lstrip(spath, '/');
std::string location = util::URL().set_host(host).set_port(port).str(
util::URL::StripScheme::yes,
/* rstrip_path= */ '/'
);
auto location = util::URL();
location.set_host(host);
location.set_port(port);
return channel_configuration{
/* location= */ std::move(location),
/* location= */ location.pretty_str(util::URL::StripScheme::yes, /* rstrip_path= */ '/'),
/* name= */ spath,
/* scheme= */ scheme,
/* auth= */ "",
@ -452,10 +453,10 @@ namespace mamba
{
std::string full_url = util::concat_scheme_url(scheme, location);
const auto parser = util::URL::parse(full_url);
location = util::URL()
.set_host(parser.host())
.set_port(parser.port())
.str(util::URL::StripScheme::yes, /* rstrip_path= */ '/');
auto url = util::URL();
url.set_host(parser.host());
url.set_port(parser.port());
location = url.pretty_str(util::URL::StripScheme::yes, /* rstrip_path= */ '/');
name = util::lstrip(parser.pretty_path(), '/');
}
}

View File

@ -1449,7 +1449,7 @@ namespace mamba::validation
auto dl_target = std::make_unique<mamba::DownloadTarget>(
context,
"key_mgr.json",
url.str(),
url.pretty_str(),
tmp_metadata_path.string()
);
@ -1614,7 +1614,7 @@ namespace mamba::validation
auto dl_target = std::make_unique<mamba::DownloadTarget>(
context,
"pkg_mgr.json",
url.str(),
url.pretty_str(),
tmp_metadata_path.string()
);

View File

@ -0,0 +1,287 @@
// 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 <algorithm>
#include <cassert>
#include <string_view>
#include <tuple>
#include <fmt/format.h>
#include "mamba/specs/archive.hpp"
#include "mamba/specs/conda_url.hpp"
#include "mamba/util/string.hpp"
#include "mamba/util/url_manip.hpp"
namespace mamba::specs
{
namespace
{
[[nodiscard]] auto is_token_char(char c) -> bool
{
return util::is_alphanum(c) || (c == '-');
}
[[nodiscard]] auto is_token_first_char(char c) -> bool
{
return is_token_char(c) || (c == '_');
}
[[nodiscard]] auto is_token(std::string_view str) -> bool
{
// usernames on anaconda.org can have a underscore, which influences the first two
// characters
static constexpr std::size_t token_start_size = 2;
if (str.size() < token_start_size)
{
return false;
}
const auto token_first = str.substr(0, token_start_size);
const auto token_rest = str.substr(token_start_size);
return std::all_of(token_first.cbegin(), token_first.cend(), &is_token_first_char)
&& std::all_of(token_rest.cbegin(), token_rest.cend(), &is_token_char);
}
[[nodiscard]] auto find_token_and_prefix(std::string_view path)
-> std::pair<std::size_t, std::size_t>
{
static constexpr auto npos = std::string_view::npos;
const auto prefix_pos = path.find(CondaURL::token_prefix);
if (prefix_pos == npos)
{
return std::pair{ std::string_view::npos, 0ul };
}
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_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 std::pair{ std::string_view::npos, 0ul };
}
}
CondaURL::CondaURL(URL&& url)
: Base(std::move(url))
{
}
auto CondaURL::parse(std::string_view url) -> CondaURL
{
return CondaURL(URL::parse(url));
}
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))
{
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);
}
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))
{
throw std::invalid_argument(
fmt::format(R"(No token template in orignial path "{}")", path(Decode::no))
);
}
assert(token_prefix.size() < len);
std::string l_path = clear_path(); // percent encoded
const auto token_len = (len != npos) ? len - token_prefix.size() : npos;
l_path.replace(pos + token_prefix.size(), token_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))
{
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);
return true;
}
namespace
{
[[nodiscard]] auto find_slash_and_platform(std::string_view path)
-> std::tuple<std::size_t, std::size_t, std::optional<Platform>>
{
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 };
}
}
auto CondaURL::platform() const -> std::optional<Platform>
{
const auto& l_path = path(Decode::no);
const auto [pos, count, plat] = find_slash_and_platform(l_path);
return plat;
}
auto CondaURL::platform_name() const -> std::string_view
{
static constexpr auto npos = std::string_view::npos;
const auto& l_path = path(Decode::no);
const auto [pos, len, plat] = find_slash_and_platform(l_path);
if (!plat.has_value())
{
return "";
}
assert(1 < len);
const auto plat_len = (len != npos) ? len - 1 : npos;
return std::string_view(l_path).substr(pos + 1, plat_len);
}
void CondaURL::set_platform_no_check_input(std::string_view platform)
{
static constexpr auto npos = std::string_view::npos;
const auto [pos, len, plat] = find_slash_and_platform(path(Decode::no));
if (!plat.has_value())
{
throw std::invalid_argument(
fmt::format(R"(No platform in orignial path "{}")", path(Decode::no))
);
}
assert(1 < len);
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);
}
void CondaURL::set_platform(std::string_view platform)
{
if (!platform_parse(platform).has_value())
{
throw std::invalid_argument(fmt::format(R"(Invalid CondaURL platform "{}")", platform));
}
return set_platform_no_check_input(platform);
}
void CondaURL::set_platform(Platform platform)
{
return set_platform_no_check_input(specs::platform_name(platform));
}
auto CondaURL::clear_platform() -> bool
{
const auto [pos, count, plat] = find_slash_and_platform(path(Decode::no));
if (!plat.has_value())
{
return false;
}
assert(1 < count);
std::string l_path = clear_path(); // percent encoded
l_path.erase(pos, count);
set_path(std::move(l_path), Encode::no);
return true;
}
auto CondaURL::package(Decode::yes_type) const -> std::string
{
return util::url_decode(package(Decode::no));
}
auto CondaURL::package(Decode::no_type) const -> std::string_view
{
// Must not decode to find the meaningful '/' spearators
const auto& l_path = path(Decode::no);
if (has_archive_extension(l_path))
{
auto [head, pkg] = util::rstrip_if_parts(l_path, [](char c) { return c != '/'; });
return pkg;
}
return "";
}
void CondaURL::set_package(std::string_view pkg, Encode::yes_type)
{
return set_package(util::url_encode(pkg), Encode::no);
}
void CondaURL::set_package(std::string_view pkg, Encode::no_type)
{
if (!has_archive_extension(pkg))
{
throw std::invalid_argument(
fmt::format(R"(Invalid CondaURL package "{}", use path_append instead)", pkg)
);
}
// Must not decode to find the meaningful '/' spearators
if (has_archive_extension(path(Decode::no)))
{
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);
}
else
{
append_path(pkg, Encode::no);
}
}
auto CondaURL::clear_package() -> bool
{
// Must not decode to find the meaningful '/' spearators
if (has_archive_extension(path(Decode::no)))
{
auto l_path = clear_path();
l_path.erase(l_path.rfind('/'));
set_path(std::move(l_path), Encode::no);
return true;
}
return false;
}
}

View File

@ -16,73 +16,10 @@
namespace mamba::specs
{
/**
* Convert the enumeration to its conda string.
*/
auto platform_name(Platform p) -> std::string_view
{
switch (p)
{
case Platform::noarch:
return "noarch";
case Platform::linux_32:
return "linux-32";
case Platform::linux_64:
return "linux-64";
case Platform::linux_armv6l:
return "linux-armv6l";
case Platform::linux_armv7l:
return "linux-armv7l";
case Platform::linux_aarch64:
return "linux-aarch64";
case Platform::linux_ppc64:
return "linux-ppc64";
case Platform::linux_ppc64le:
return "linux-ppc64le";
case Platform::linux_s390x:
return "linux-s390x";
case Platform::linux_riscv32:
return "linux-riscv32";
case Platform::linux_riscv64:
return "linux-riscv64";
case Platform::osx_64:
return "osx-64";
case Platform::osx_arm64:
return "osx-arm64";
case Platform::win_32:
return "win-32";
case Platform::win_64:
return "win-64";
case Platform::win_arm64:
return "win-arm64";
default:
// All enum cases must be handled
assert(false);
return "";
}
}
auto platform_parse(std::string_view str) -> std::optional<Platform>
{
std::string const str_clean = util::to_lower(util::strip(str));
for (const auto p : {
Platform::noarch,
Platform::linux_32,
Platform::linux_64,
Platform::linux_armv6l,
Platform::linux_armv7l,
Platform::linux_aarch64,
Platform::linux_ppc64,
Platform::linux_ppc64le,
Platform::linux_s390x,
Platform::linux_riscv32,
Platform::linux_riscv64,
Platform::osx_64,
Platform::osx_arm64,
Platform::win_32,
Platform::win_64,
Platform::win_arm64,
})
for (const auto p : known_platforms())
{
if (str_clean == platform_name(p))
{

View File

@ -38,13 +38,19 @@ namespace mamba::util
return false;
}
auto path_get_drive_letter(std::string_view path) -> std::optional<char>
{
if (path_has_drive_letter(path))
{
return { path.front() };
}
return std::nullopt;
}
auto path_has_drive_letter(std::string_view path) -> bool
{
static constexpr auto is_drive_char = [](char c) -> bool { return is_alphanum(c); };
auto [drive, rest] = lstrip_if_parts(path, is_drive_char);
return !drive.empty() && (rest.size() >= 2) && (rest[0] == ':')
&& ((rest[1] == '/') || (rest[1] == '\\'));
return (path.size() >= 3) && is_alpha(path[0]) && (path[1] == ':')
&& ((path[2] == '/') || (path[2] == '\\'));
}
auto path_win_to_posix(std::string path) -> std::string

View File

@ -236,6 +236,17 @@ namespace mamba::util
return str.find(sub_str) != std::string::npos;
}
// TODO(C++20) This is a method of string_view
bool contains(std::string_view str, char c)
{
return str.find(c) != std::string::npos;
}
bool contains(char c1, char c2)
{
return c1 == c2;
}
// TODO(C++20) This is a method of string_view
bool ends_with(std::string_view str, std::string_view suffix)
{

View File

@ -13,6 +13,7 @@
#include <curl/urlapi.h>
#include <fmt/format.h>
#include "mamba/util/build.hpp"
#include "mamba/util/path_manip.hpp"
#include "mamba/util/string.hpp"
#include "mamba/util/url.hpp"
@ -179,14 +180,14 @@ 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)))
.set_user(handle.get_part(CURLUPART_USER).value_or(""), Encode::no)
.set_password(handle.get_part(CURLUPART_PASSWORD).value_or(""), Encode::no)
.set_host(handle.get_part(CURLUPART_HOST).value_or(std::string(URL::localhost)))
.set_path(handle.get_part(CURLUPART_PATH).value_or("/"))
.set_port(handle.get_part(CURLUPART_PORT).value_or(""))
.set_query(handle.get_part(CURLUPART_QUERY).value_or(""))
.set_fragment(handle.get_part(CURLUPART_FRAGMENT).value_or(""));
out.set_scheme(handle.get_part(CURLUPART_SCHEME).value_or(std::string(URL::https)));
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(""));
out.set_path(handle.get_part(CURLUPART_PATH).value_or("/"));
out.set_port(handle.get_part(CURLUPART_PORT).value_or(""));
out.set_query(handle.get_part(CURLUPART_QUERY).value_or(""));
out.set_fragment(handle.get_part(CURLUPART_FRAGMENT).value_or(""));
}
return out;
}
@ -196,14 +197,13 @@ namespace mamba::util
return m_scheme;
}
auto URL::set_scheme(std::string_view scheme) -> URL&
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));
return *this;
}
auto URL::user(Decode::no_type) const -> const std::string&
@ -213,20 +213,22 @@ namespace mamba::util
auto URL::user(Decode::yes_type) const -> std::string
{
return url_decode(m_user);
return url_decode(user(Decode::no));
}
auto URL::set_user(std::string_view user, Encode encode) -> URL&
void URL::set_user(std::string_view user, Encode::yes_type)
{
if (encode == Encode::yes)
{
m_user = url_encode(user);
}
else
{
m_user = user;
}
return *this;
return set_user(url_encode(user), Encode::no);
}
void URL::set_user(std::string user, Encode::no_type)
{
m_user = std::move(user);
}
auto URL::clear_user() -> std::string
{
return std::exchange(m_user, "");
}
auto URL::password(Decode::no_type) const -> const std::string&
@ -236,20 +238,22 @@ namespace mamba::util
auto URL::password(Decode::yes_type) const -> std::string
{
return url_decode(m_password);
return url_decode(password(Decode::no));
}
auto URL::set_password(std::string_view password, Encode encode) -> URL&
void URL::set_password(std::string_view password, Encode::yes_type)
{
if (encode == Encode::yes)
{
m_password = url_encode(password);
}
else
{
m_password = password;
}
return *this;
return set_password(url_encode(password), Encode::no);
}
void URL::set_password(std::string password, Encode::no_type)
{
m_password = std::move(password);
}
auto URL::clear_password() -> std::string
{
return std::exchange(m_password, "");
}
auto URL::authentication() const -> std::string
@ -259,33 +263,46 @@ namespace mamba::util
return p.empty() ? u : util::concat(u, ':', p);
}
auto URL::host(Decode::no_type) const -> const std::string&
auto URL::host(Decode::no_type) const -> std::string_view
{
if ((m_scheme != "file") && m_host.empty())
{
return localhost;
}
return m_host;
}
auto URL::host(Decode::yes_type) const -> std::string
{
return url_decode(m_host);
return url_decode(host(Decode::no));
}
auto URL::set_host(std::string_view host, Encode encode) -> URL&
void URL::set_host(std::string_view host, Encode::yes_type)
{
std::string new_host = {};
if (encode == Encode::yes)
return set_host(url_encode(host), Encode::no);
}
void URL::set_host(std::string host, Encode::no_type)
{
std::transform(
host.cbegin(),
host.cend(),
host.begin(),
[](char c) { return util::to_lower(c); }
);
m_host = std::move(host);
}
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())
{
new_host = url_encode(host);
auto out = std::string(l_host);
set_host("", Encode::no);
return out;
}
else
{
new_host = util::strip(host); // spaces are illegal if not encoded
}
if (new_host.empty())
{
throw std::invalid_argument("Cannot set empty host");
}
m_host = util::to_lower(new_host);
return *this;
return std::exchange(m_host, "");
}
auto URL::port() const -> const std::string&
@ -293,14 +310,18 @@ namespace mamba::util
return m_port;
}
auto URL::set_port(std::string_view port) -> URL&
void URL::set_port(std::string_view port)
{
if (!std::all_of(port.cbegin(), port.cend(), [](char c) { return util::is_digit(c); }))
{
throw std::invalid_argument(fmt::format(R"(Port must be a number, got "{}")", port));
}
m_port = port;
return *this;
}
auto URL::clear_port() -> std::string
{
return std::exchange(m_port, "");
}
auto URL::authority() const -> std::string
@ -319,44 +340,86 @@ namespace mamba::util
);
}
auto URL::path() const -> const std::string&
auto URL::path(Decode::no_type) const -> const std::string&
{
return m_path;
}
auto URL::pretty_path() const -> std::string_view
auto URL::path(Decode::yes_type) const -> std::string
{
// All paths start with a '/' except those like "file://C:/folder/file.txt"
return url_decode(path(Decode::no));
}
void URL::set_path(std::string_view path, Encode::yes_type)
{
// Drive colon must not be encoded
if (on_win && (scheme() == "file"))
{
auto [slashes, no_slash_path] = lstrip_parts(path, '/');
if (slashes.empty())
{
slashes = "/";
}
if ((no_slash_path.size() >= 2) && path_has_drive_letter(no_slash_path))
{
m_path = concat(
slashes,
no_slash_path.substr(0, 2),
url_encode(no_slash_path.substr(2), '/')
);
}
else
{
m_path = concat(slashes, url_encode(no_slash_path, '/'));
}
}
else
{
return set_path(url_encode(path, '/'), Encode::no);
}
}
void URL::set_path(std::string path, Encode::no_type)
{
if (!util::starts_with(path, '/'))
{
path.insert(0, 1, '/');
}
m_path = path;
}
auto URL::clear_path() -> std::string
{
return std::exchange(m_path, "/");
}
auto URL::pretty_path() const -> std::string
{
// All paths start with a '/' except those like "file:///C:/folder/file.txt"
if (m_scheme == "file")
{
assert(util::starts_with(m_path, '/'));
auto path_no_slash = std::string_view(m_path).substr(1);
auto path_no_slash = url_decode(std::string_view(m_path).substr(1));
if (path_has_drive_letter(path_no_slash))
{
return path_no_slash;
}
}
return m_path;
return url_decode(m_path);
}
auto URL::set_path(std::string_view path) -> URL&
void URL::append_path(std::string_view subpath, Encode::yes_type)
{
if (!util::starts_with(path, '/'))
if (path(Decode::no) == "/")
{
m_path.reserve(path.size() + 1);
m_path = '/';
m_path += path;
// Allow hanldling of Windows drive letter encoding
return set_path(std::string(subpath), Encode::yes);
}
else
{
m_path = path;
}
return *this;
return append_path(url_encode(subpath, '/'), Encode::no);
}
auto URL::append_path(std::string_view subpath) -> URL&
void URL::append_path(std::string_view subpath, Encode::no_type)
{
subpath = util::strip(subpath);
m_path.reserve(m_path.size() + 1 + subpath.size());
const bool trailing = util::ends_with(m_path, '/');
const bool leading = util::starts_with(subpath, '/');
@ -369,7 +432,6 @@ namespace mamba::util
m_path.pop_back();
}
m_path += subpath;
return *this;
}
auto URL::query() const -> const std::string&
@ -377,10 +439,14 @@ namespace mamba::util
return m_query;
}
auto URL::set_query(std::string_view query) -> URL&
void URL::set_query(std::string_view query)
{
m_query = query;
return *this;
}
auto URL::clear_query() -> std::string
{
return std::exchange(m_query, "");
}
auto URL::fragment() const -> const std::string&
@ -388,36 +454,59 @@ namespace mamba::util
return m_fragment;
}
auto URL::set_fragment(std::string_view fragment) -> URL&
void URL::set_fragment(std::string_view fragment)
{
m_fragment = fragment;
return *this;
}
auto URL::str(StripScheme strip_scheme, char rstrip_path, HidePassword hide_password) const
auto URL::clear_fragment() -> std::string
{
return std::exchange(m_fragment, "");
}
auto URL::str() -> std::string
{
return util::concat(
scheme(),
"://",
user(Decode::no),
m_password.empty() ? "" : ":",
password(Decode::no),
m_user.empty() ? "" : "@",
host(Decode::no),
m_port.empty() ? "" : ":",
port(),
path(Decode::no),
m_query.empty() ? "" : "?",
m_query,
m_fragment.empty() ? "" : "#",
m_fragment
);
}
auto URL::pretty_str(StripScheme strip_scheme, char rstrip_path, HidePassword hide_password) const
-> std::string
{
// Not showing "localhost" on file URI
std::string_view computed_host = m_host;
if ((m_scheme == "file") && (m_host == localhost))
{
computed_host = "";
}
std::string computed_path = {};
// When stripping file scheme, not showing leading '/' for Windows path with drive
std::string_view computed_path = m_path;
if ((m_scheme == "file") && (strip_scheme == StripScheme::yes) && computed_host.empty())
if ((m_scheme == "file") && (strip_scheme == StripScheme::yes) && host(Decode::no).empty())
{
computed_path = pretty_path();
}
else
{
computed_path = path(Decode::yes);
}
computed_path = util::rstrip(computed_path, rstrip_path);
return util::concat(
(strip_scheme == StripScheme::no) ? m_scheme : "",
(strip_scheme == StripScheme::no) ? "://" : "",
m_user,
user(Decode::yes),
m_password.empty() ? "" : ":",
(hide_password == HidePassword::no) ? m_password : "*****",
(hide_password == HidePassword::no) ? password(Decode::yes) : "*****",
m_user.empty() ? "" : "@",
computed_host,
host(Decode::yes),
m_port.empty() ? "" : ":",
m_port,
computed_path,

View File

@ -68,25 +68,40 @@ namespace mamba::util
return static_cast<char>((hex_offset[idx10] << 4) | hex_offset[idx1]);
}
template <typename Str>
auto url_encode_impl(std::string_view url, Str exclude) -> std::string
{
std::string out = {};
out.reserve(url.size());
for (char c : url)
{
if (url_is_unreserved_char(c) || contains(exclude, c))
{
out += c;
}
else
{
const auto encoding = url_encode_char(c);
out += std::string_view(encoding.data(), encoding.size());
}
}
return out;
}
}
auto url_encode(std::string_view url) -> std::string
{
std::string out = {};
out.reserve(url.size());
for (char c : url)
{
if (url_is_unreserved_char(c))
{
out += c;
}
else
{
const auto encoding = url_encode_char(c);
out += std::string_view(encoding.data(), encoding.size());
}
}
return out;
return url_encode_impl(url, 'a'); // Already not encoded
}
auto url_encode(std::string_view url, std::string_view exclude) -> std::string
{
return url_encode_impl(url, exclude);
}
auto url_encode(std::string_view url, char exclude) -> std::string
{
return url_encode_impl(url, exclude);
}
auto url_decode(std::string_view url) -> std::string
@ -253,7 +268,7 @@ namespace mamba::util
auth = url_parsed.authentication();
url_parsed.set_user("");
url_parsed.set_password("");
remaining_url = util::rstrip(url_parsed.str(URL::StripScheme::yes), '/');
remaining_url = util::rstrip(url_parsed.pretty_str(URL::StripScheme::yes), '/');
}
bool compare_cleaned_url(const std::string& url1, const std::string& url2)

View File

@ -37,6 +37,7 @@ set(LIBMAMBA_TEST_SRCS
# Implementation of version and matching specs
src/specs/test_archive.cpp
src/specs/test_platform.cpp
src/specs/test_conda_url.cpp
src/specs/test_version.cpp
src/specs/test_version_spec.cpp
src/specs/test_repo_data.cpp

View File

@ -0,0 +1,252 @@
// 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 <doctest/doctest.h>
#include "mamba/specs/conda_url.hpp"
using namespace mamba::specs;
TEST_SUITE("specs::CondaURL")
{
TEST_CASE("Token")
{
CondaURL url{};
url.set_scheme("https");
url.set_host("repo.mamba.pm");
SUBCASE("https://repo.mamba.pm/folder/file.txt")
{
url.set_path("/folder/file.txt");
CHECK_EQ(url.token(), "");
CHECK_THROWS_AS(url.set_token("token"), std::invalid_argument);
CHECK_EQ(url.path(), "/folder/file.txt");
CHECK_FALSE(url.clear_token());
CHECK_EQ(url.path(), "/folder/file.txt");
}
SUBCASE("https://repo.mamba.pm/t/xy-12345678-1234/conda-forge/linux-64")
{
url.set_path("/t/xy-12345678-1234/conda-forge/linux-64");
CHECK_EQ(url.token(), "xy-12345678-1234");
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(), "/t/xy-12345678-1234/conda-forge/linux-64");
}
SUBCASE("Clear token")
{
CHECK(url.clear_token());
CHECK_EQ(url.token(), "");
CHECK_EQ(url.path(), "/conda-forge/linux-64");
}
SUBCASE("Set token")
{
url.set_token("abcd");
CHECK_EQ(url.token(), "abcd");
CHECK_EQ(url.path(), "/t/abcd/conda-forge/linux-64");
}
}
SUBCASE("https://repo.mamba.pm/t/xy-12345678-1234-1234-1234-123456789012")
{
url.set_path("/t/xy-12345678-1234-1234-1234-123456789012");
CHECK_EQ(url.token(), "xy-12345678-1234-1234-1234-123456789012");
url.set_token("abcd");
CHECK_EQ(url.token(), "abcd");
CHECK_EQ(url.path(), "/t/abcd");
CHECK(url.clear_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");
url.set_token("abcd");
CHECK_EQ(url.token(), "abcd");
CHECK_EQ(url.path(), "/bar/t/abcd/");
CHECK(url.clear_token());
CHECK_EQ(url.path(), "/bar/");
}
}
TEST_CASE("Platform")
{
CondaURL url{};
url.set_scheme("https");
url.set_host("repo.mamba.pm");
SUBCASE("https://repo.mamba.pm/")
{
CHECK_FALSE(url.platform().has_value());
CHECK_EQ(url.platform_name(), "");
CHECK_THROWS_AS(url.set_platform(Platform::linux_64), std::invalid_argument);
CHECK_EQ(url.path(), "/");
CHECK_FALSE(url.clear_platform());
CHECK_EQ(url.path(), "/");
}
SUBCASE("https://repo.mamba.pm/conda-forge")
{
url.set_path("conda-forge");
CHECK_FALSE(url.platform().has_value());
CHECK_EQ(url.platform_name(), "");
CHECK_THROWS_AS(url.set_platform(Platform::linux_64), std::invalid_argument);
CHECK_EQ(url.path(), "/conda-forge");
CHECK_FALSE(url.clear_platform());
CHECK_EQ(url.path(), "/conda-forge");
}
SUBCASE("https://repo.mamba.pm/conda-forge/")
{
url.set_path("conda-forge/");
CHECK_FALSE(url.platform().has_value());
CHECK_EQ(url.platform_name(), "");
CHECK_THROWS_AS(url.set_platform(Platform::linux_64), std::invalid_argument);
CHECK_EQ(url.path(), "/conda-forge/");
CHECK_FALSE(url.clear_platform());
CHECK_EQ(url.path(), "/conda-forge/");
}
SUBCASE("https://repo.mamba.pm/conda-forge/win-64")
{
url.set_path("conda-forge/win-64");
CHECK_EQ(url.platform(), Platform::win_64);
CHECK_EQ(url.platform_name(), "win-64");
url.set_platform(Platform::linux_64);
CHECK_EQ(url.platform(), Platform::linux_64);
CHECK_EQ(url.path(), "/conda-forge/linux-64");
CHECK(url.clear_platform());
CHECK_EQ(url.path(), "/conda-forge");
}
SUBCASE("https://repo.mamba.pm/conda-forge/OSX-64/")
{
url.set_path("conda-forge/OSX-64");
CHECK_EQ(url.platform(), Platform::osx_64);
CHECK_EQ(url.platform_name(), "OSX-64"); // Captialization not changed
url.set_platform("Win-64");
CHECK_EQ(url.platform(), Platform::win_64);
CHECK_EQ(url.path(), "/conda-forge/Win-64"); // Captialization not changed
CHECK(url.clear_platform());
CHECK_EQ(url.path(), "/conda-forge");
}
SUBCASE("https://repo.mamba.pm/conda-forge/linux-64/micromamba-1.5.1-0.tar.bz2")
{
url.set_path("/conda-forge/linux-64/micromamba-1.5.1-0.tar.bz2");
CHECK_EQ(url.platform(), Platform::linux_64);
CHECK_EQ(url.platform_name(), "linux-64");
url.set_platform("osx-64");
CHECK_EQ(url.platform(), Platform::osx_64);
CHECK_EQ(url.path(), "/conda-forge/osx-64/micromamba-1.5.1-0.tar.bz2");
CHECK(url.clear_platform());
CHECK_EQ(url.path(), "/conda-forge/micromamba-1.5.1-0.tar.bz2");
}
}
TEST_CASE("Package")
{
CondaURL url{};
url.set_scheme("https");
url.set_host("repo.mamba.pm");
SUBCASE("https://repo.mamba.pm/")
{
CHECK_EQ(url.package(), "");
CHECK_THROWS_AS(url.set_package("not-package/"), std::invalid_argument);
CHECK_EQ(url.path(), "/");
CHECK_FALSE(url.clear_package());
CHECK_EQ(url.package(), "");
CHECK_EQ(url.path(), "/");
url.set_package("micromamba-1.5.1-0.tar.bz2");
CHECK_EQ(url.package(), "micromamba-1.5.1-0.tar.bz2");
CHECK_EQ(url.path(), "/micromamba-1.5.1-0.tar.bz2");
CHECK(url.clear_package());
CHECK_EQ(url.package(), "");
CHECK_EQ(url.path(), "/");
}
SUBCASE("https://repo.mamba.pm/conda-forge")
{
url.set_path("conda-forge");
CHECK_EQ(url.package(), "");
url.set_package("micromamba-1.5.1-0.tar.bz2");
CHECK_EQ(url.package(), "micromamba-1.5.1-0.tar.bz2");
CHECK_EQ(url.path(), "/conda-forge/micromamba-1.5.1-0.tar.bz2");
CHECK(url.clear_package());
CHECK_EQ(url.package(), "");
CHECK_EQ(url.path(), "/conda-forge");
}
SUBCASE("https://repo.mamba.pm/conda-forge/")
{
url.set_path("conda-forge/");
CHECK_EQ(url.package(), "");
url.set_package("micromamba-1.5.1-0.tar.bz2");
CHECK_EQ(url.package(), "micromamba-1.5.1-0.tar.bz2");
CHECK_EQ(url.path(), "/conda-forge/micromamba-1.5.1-0.tar.bz2");
CHECK(url.clear_package());
CHECK_EQ(url.package(), "");
CHECK_EQ(url.path(), "/conda-forge");
}
SUBCASE("https://repo.mamba.pm/conda-forge/linux-64/micromamba-1.5.1-0.tar.bz2")
{
url.set_path("/conda-forge/linux-64/micromamba-1.5.1-0.tar.bz2");
CHECK_EQ(url.package(), "micromamba-1.5.1-0.tar.bz2");
url.set_package("mamba-1.5.1-0.tar.bz2");
CHECK_EQ(url.package(), "mamba-1.5.1-0.tar.bz2");
CHECK_EQ(url.path(), "/conda-forge/linux-64/mamba-1.5.1-0.tar.bz2");
CHECK(url.clear_package());
CHECK_EQ(url.package(), "");
CHECK_EQ(url.path(), "/conda-forge/linux-64");
}
}
}

View File

@ -10,7 +10,7 @@
using namespace mamba::specs;
TEST_SUITE("platform")
TEST_SUITE("specs::platform")
{
TEST_CASE("name")
{
@ -26,4 +26,16 @@ TEST_SUITE("platform")
CHECK_EQ(platform_parse(" OSX-64"), Platform::osx_64);
CHECK_EQ(platform_parse("linus-46"), std::nullopt);
}
TEST_CASE("known_platform")
{
static constexpr decltype(known_platform_names()) expected{
"noarch", "linux-32", "linux-64", "linux-armv6l",
"linux-armv7l", "linux-aarch64", "linux-ppc64le", "linux-ppc64",
"linux-s390x", "linux-riscv32", "linux-riscv64", "osx-64",
"osx-arm64", "win-32", "win-64", "win-arm64",
};
CHECK_EQ(expected, known_platform_names());
}
}

View File

@ -35,7 +35,9 @@ TEST_SUITE("util::path_manip")
TEST_CASE("path_has_drive_letter")
{
CHECK(path_has_drive_letter("C:/folder/file"));
CHECK_EQ(path_get_drive_letter("C:/folder/file"), 'C');
CHECK(path_has_drive_letter(R"(C:\folder\file)"));
CHECK_EQ(path_get_drive_letter(R"(C:\folder\file)"), 'C');
CHECK_FALSE(path_has_drive_letter("/folder/file"));
CHECK_FALSE(path_has_drive_letter("folder/file"));
CHECK_FALSE(path_has_drive_letter(R"(\folder\file)"));

View File

@ -62,9 +62,11 @@ namespace mamba::util
TEST_CASE("contains")
{
CHECK(contains('c', 'c'));
CHECK_FALSE(contains('c', 'a'));
CHECK(contains(":hello&", ""));
CHECK(contains(":hello&", "&"));
CHECK(contains(":hello&", ":"));
CHECK(contains(":hello&", '&'));
CHECK(contains(":hello&", ':'));
CHECK(contains(":hello&", "ll"));
CHECK_FALSE(contains(":hello&", "eo"));
CHECK(contains("áäᜩgþhëb®hüghœ©®xb", "ëb®"));

View File

@ -10,6 +10,7 @@
#include <doctest/doctest.h>
#include "mamba/util/build.hpp"
#include "mamba/util/url.hpp"
using namespace mamba::util;
@ -22,35 +23,76 @@ TEST_SUITE("util::URL")
{
URL url{};
CHECK_EQ(url.scheme(), URL::https);
CHECK_EQ(url.host(), URL::localhost);
CHECK_EQ(url.path(), "/");
CHECK_EQ(url.pretty_path(), "/");
CHECK_EQ(url.user(), "");
CHECK_EQ(url.password(), "");
CHECK_EQ(url.port(), "");
CHECK_EQ(url.host(), URL::localhost);
CHECK_EQ(url.path(), "/");
CHECK_EQ(url.pretty_path(), "/");
CHECK_EQ(url.query(), "");
CHECK_EQ(url.clear_user(), "");
CHECK_EQ(url.user(), "");
CHECK_EQ(url.clear_password(), "");
CHECK_EQ(url.password(), "");
CHECK_EQ(url.clear_port(), "");
CHECK_EQ(url.port(), "");
CHECK_EQ(url.clear_host(), URL::localhost);
CHECK_EQ(url.host(), URL::localhost);
CHECK_EQ(url.clear_path(), "/");
CHECK_EQ(url.path(), "/");
CHECK_EQ(url.clear_query(), "");
CHECK_EQ(url.query(), "");
CHECK_EQ(url.clear_fragment(), "");
CHECK_EQ(url.fragment(), "");
}
SUBCASE("Complete")
{
URL url{};
url.set_scheme("https")
.set_host("mamba.org")
.set_user("user")
.set_password("password")
.set_port("8080")
.set_path("/folder/file.html")
.set_query("param=value")
.set_fragment("fragment");
url.set_scheme("https");
url.set_host("mamba.org");
url.set_user("user");
url.set_password("pass:word");
url.set_port("8080");
url.set_path("/folder/file.html");
url.set_query("param=value");
url.set_fragment("fragment");
CHECK_EQ(url.scheme(), "https");
CHECK_EQ(url.host(), "mamba.org");
CHECK_EQ(url.user(), "user");
CHECK_EQ(url.password(), "password");
CHECK_EQ(url.password(), "pass:word");
CHECK_EQ(url.port(), "8080");
CHECK_EQ(url.path(), "/folder/file.html");
CHECK_EQ(url.pretty_path(), "/folder/file.html");
CHECK_EQ(url.query(), "param=value");
CHECK_EQ(url.fragment(), "fragment");
CHECK_EQ(url.clear_user(), "user");
CHECK_EQ(url.user(), "");
CHECK_EQ(url.clear_password(), "pass%3Aword");
CHECK_EQ(url.password(), "");
CHECK_EQ(url.clear_port(), "8080");
CHECK_EQ(url.port(), "");
CHECK_EQ(url.clear_host(), "mamba.org");
CHECK_EQ(url.host(), URL::localhost);
CHECK_EQ(url.clear_path(), "/folder/file.html");
CHECK_EQ(url.path(), "/");
CHECK_EQ(url.clear_query(), "param=value");
CHECK_EQ(url.query(), "");
CHECK_EQ(url.clear_fragment(), "fragment");
CHECK_EQ(url.fragment(), "");
}
SUBCASE("File")
{
URL url{};
url.set_scheme("file");
url.set_path("/folder/file.txt");
CHECK_EQ(url.scheme(), "file");
CHECK_EQ(url.host(), "");
CHECK_EQ(url.path(), "/folder/file.txt");
}
SUBCASE("Path")
@ -64,7 +106,8 @@ TEST_SUITE("util::URL")
SUBCASE("Windows path")
{
URL url{};
url.set_scheme("file").set_path("C:/folder/file.txt");
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");
}
@ -72,7 +115,8 @@ TEST_SUITE("util::URL")
SUBCASE("Case")
{
URL url{};
url.set_scheme("FtP").set_host("sOme_Host.COM");
url.set_scheme("FtP");
url.set_host("sOme_Host.COM");
CHECK_EQ(url.scheme(), "ftp");
CHECK_EQ(url.host(), "some_host.com");
}
@ -81,7 +125,6 @@ TEST_SUITE("util::URL")
{
URL url{};
CHECK_THROWS_AS(url.set_scheme(""), std::invalid_argument);
CHECK_THROWS_AS(url.set_host(""), std::invalid_argument);
CHECK_THROWS_AS(url.set_port("not-a-number"), std::invalid_argument);
}
@ -212,34 +255,36 @@ TEST_SUITE("util::URL")
SUBCASE("file://C:/Users/wolfv/test/document.json")
{
#ifdef _WIN32
const URL url = URL::parse("file://C:/Users/wolfv/test/document.json");
CHECK_EQ(url.scheme(), "file");
CHECK_EQ(url.host(), URL::localhost);
CHECK_EQ(url.path(), "/C:/Users/wolfv/test/document.json");
CHECK_EQ(url.pretty_path(), "C:/Users/wolfv/test/document.json");
CHECK_EQ(url.user(), "");
CHECK_EQ(url.password(), "");
CHECK_EQ(url.port(), "");
CHECK_EQ(url.query(), "");
CHECK_EQ(url.fragment(), "");
#endif
if (on_win)
{
const URL url = URL::parse("file://C:/Users/wolfv/test/document.json");
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");
CHECK_EQ(url.user(), "");
CHECK_EQ(url.password(), "");
CHECK_EQ(url.port(), "");
CHECK_EQ(url.query(), "");
CHECK_EQ(url.fragment(), "");
}
}
SUBCASE("file:///home/wolfv/test/document.json")
{
#ifndef _WIN32
const URL url = URL::parse("file:///home/wolfv/test/document.json");
CHECK_EQ(url.scheme(), "file");
CHECK_EQ(url.host(), URL::localhost);
CHECK_EQ(url.path(), "/home/wolfv/test/document.json");
CHECK_EQ(url.pretty_path(), "/home/wolfv/test/document.json");
CHECK_EQ(url.user(), "");
CHECK_EQ(url.password(), "");
CHECK_EQ(url.port(), "");
CHECK_EQ(url.query(), "");
CHECK_EQ(url.fragment(), "");
#endif
if (!on_win)
{
const URL url = URL::parse("file:///home/wolfv/test/document.json");
CHECK_EQ(url.scheme(), "file");
CHECK_EQ(url.host(), "");
CHECK_EQ(url.path(), "/home/wolfv/test/document.json");
CHECK_EQ(url.pretty_path(), "/home/wolfv/test/document.json");
CHECK_EQ(url.user(), "");
CHECK_EQ(url.password(), "");
CHECK_EQ(url.port(), "");
CHECK_EQ(url.query(), "");
CHECK_EQ(url.fragment(), "");
}
}
SUBCASE("https://169.254.0.0/page")
@ -286,7 +331,7 @@ TEST_SUITE("util::URL")
}
}
TEST_CASE("str")
TEST_CASE("pretty_str options")
{
SUBCASE("scheme option")
{
@ -295,15 +340,15 @@ TEST_SUITE("util::URL")
SUBCASE("defaut scheme")
{
CHECK_EQ(url.str(URL::StripScheme::no), "https://mamba.org/");
CHECK_EQ(url.str(URL::StripScheme::yes), "mamba.org/");
CHECK_EQ(url.pretty_str(URL::StripScheme::no), "https://mamba.org/");
CHECK_EQ(url.pretty_str(URL::StripScheme::yes), "mamba.org/");
}
SUBCASE("ftp scheme")
{
url.set_scheme("ftp");
CHECK_EQ(url.str(URL::StripScheme::no), "ftp://mamba.org/");
CHECK_EQ(url.str(URL::StripScheme::yes), "mamba.org/");
CHECK_EQ(url.pretty_str(URL::StripScheme::no), "ftp://mamba.org/");
CHECK_EQ(url.pretty_str(URL::StripScheme::yes), "mamba.org/");
}
}
@ -311,83 +356,120 @@ TEST_SUITE("util::URL")
{
URL url = {};
url.set_host("mamba.org");
CHECK_EQ(url.str(URL::StripScheme::no, 0), "https://mamba.org/");
CHECK_EQ(url.str(URL::StripScheme::no, '/'), "https://mamba.org");
CHECK_EQ(url.pretty_str(URL::StripScheme::no, 0), "https://mamba.org/");
CHECK_EQ(url.pretty_str(URL::StripScheme::no, '/'), "https://mamba.org");
url.set_path("/page/");
CHECK_EQ(url.str(URL::StripScheme::no, ':'), "https://mamba.org/page/");
CHECK_EQ(url.str(URL::StripScheme::no, '/'), "https://mamba.org/page");
CHECK_EQ(url.pretty_str(URL::StripScheme::no, ':'), "https://mamba.org/page/");
CHECK_EQ(url.pretty_str(URL::StripScheme::no, '/'), "https://mamba.org/page");
}
SUBCASE("Hide password option")
{
URL url = {};
url.set_user("user").set_password("pass");
url.set_user("user");
url.set_password("pass");
CHECK_EQ(
url.str(URL::StripScheme::no, 0, URL::HidePassword::no),
url.pretty_str(URL::StripScheme::no, 0, URL::HidePassword::no),
"https://user:pass@localhost/"
);
CHECK_EQ(
url.str(URL::StripScheme::no, 0, URL::HidePassword::yes),
url.pretty_str(URL::StripScheme::no, 0, URL::HidePassword::yes),
"https://user:*****@localhost/"
);
}
}
TEST_CASE("str and pretty_str")
{
SUBCASE("https://user:password@mamba.org:8080/folder/file.html?param=value#fragment")
{
URL url{};
url.set_scheme("https")
.set_host("mamba.org")
.set_user("user")
.set_password("password")
.set_port("8080")
.set_path("/folder/file.html")
.set_query("param=value")
.set_fragment("fragment");
url.set_scheme("https");
url.set_host("mamba.org");
url.set_user("user");
url.set_password("password");
url.set_port("8080");
url.set_path("/folder/file.html");
url.set_query("param=value");
url.set_fragment("fragment");
CHECK_EQ(
url.str(),
"https://user:password@mamba.org:8080/folder/file.html?param=value#fragment"
);
CHECK_EQ(
url.pretty_str(),
"https://user:password@mamba.org:8080/folder/file.html?param=value#fragment"
);
}
SUBCASE("user@mamba.org")
{
URL url{};
url.set_host("mamba.org").set_user("user");
url.set_host("mamba.org");
url.set_user("user");
CHECK_EQ(url.pretty_str(), "https://user@mamba.org/");
CHECK_EQ(url.str(), "https://user@mamba.org/");
CHECK_EQ(url.str(URL::StripScheme::yes), "user@mamba.org/");
CHECK_EQ(url.pretty_str(URL::StripScheme::yes), "user@mamba.org/");
}
SUBCASE("https://mamba.org")
{
URL url{};
url.set_scheme("https").set_host("mamba.org");
url.set_scheme("https");
url.set_host("mamba.org");
CHECK_EQ(url.str(), "https://mamba.org/");
CHECK_EQ(url.str(URL::StripScheme::yes), "mamba.org/");
CHECK_EQ(url.pretty_str(), "https://mamba.org/");
CHECK_EQ(url.pretty_str(URL::StripScheme::yes), "mamba.org/");
}
SUBCASE("file:////folder/file.txt")
{
URL url{};
url.set_scheme("file").set_path("//folder/file.txt");
url.set_scheme("file");
url.set_path("//folder/file.txt");
CHECK_EQ(url.str(), "file:////folder/file.txt");
CHECK_EQ(url.str(URL::StripScheme::yes), "//folder/file.txt");
CHECK_EQ(url.pretty_str(), "file:////folder/file.txt");
CHECK_EQ(url.pretty_str(URL::StripScheme::yes), "//folder/file.txt");
}
SUBCASE("file:///folder/file.txt")
{
URL url{};
url.set_scheme("file").set_path("/folder/file.txt");
url.set_scheme("file");
url.set_path("/folder/file.txt");
CHECK_EQ(url.str(), "file:///folder/file.txt");
CHECK_EQ(url.str(URL::StripScheme::yes), "/folder/file.txt");
CHECK_EQ(url.pretty_str(), "file:///folder/file.txt");
CHECK_EQ(url.pretty_str(URL::StripScheme::yes), "/folder/file.txt");
}
SUBCASE("file:///C:/folder/file.txt")
SUBCASE("file:///C:/folder&/file.txt")
{
URL url{};
url.set_scheme("file").set_path("C:/folder/file.txt");
CHECK_EQ(url.str(), "file:///C:/folder/file.txt");
CHECK_EQ(url.str(URL::StripScheme::yes), "C:/folder/file.txt");
url.set_scheme("file");
url.set_path("C:/folder&/file.txt");
if (on_win)
{
CHECK_EQ(url.str(), "file:///C:/folder%26/file.txt");
}
else
{
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");
}
SUBCASE("https://user@email.com:pw%rd@mamba.org/some /path$/")
{
URL url{};
url.set_scheme("https");
url.set_host("mamba.org");
url.set_user("user@email.com");
url.set_password("pw%rd");
url.set_path("/some /path$/");
CHECK_EQ(url.str(), "https://user%40email.com:pw%25rd@mamba.org/some%20/path%24/");
CHECK_EQ(url.pretty_str(), "https://user@email.com:pw%rd@mamba.org/some /path$/");
}
}
@ -404,11 +486,11 @@ TEST_SUITE("util::URL")
TEST_CASE("authority")
{
URL url{};
url.set_scheme("https")
.set_host("mamba.org")
.set_path("/folder/file.html")
.set_query("param=value")
.set_fragment("fragment");
url.set_scheme("https");
url.set_host("mamba.org");
url.set_path("/folder/file.html");
url.set_query("param=value");
url.set_fragment("fragment");
CHECK_EQ(url.authority(), "mamba.org");
url.set_port("8000");
CHECK_EQ(url.authority(), "mamba.org:8000");
@ -437,20 +519,37 @@ TEST_SUITE("util::URL")
{
auto url = URL();
CHECK_EQ(url.path(), "/");
CHECK_EQ((url / "").path(), "/");
CHECK_EQ((url / " ").path(), "/");
CHECK_EQ((url / "/").path(), "/");
CHECK_EQ((url / "page").path(), "/page");
CHECK_EQ((url / "/page").path(), "/page");
CHECK_EQ((url / " /page").path(), "/page");
CHECK_EQ(url.path(), "/"); // unchanged
SUBCASE("Add components")
{
CHECK_EQ(url.path(), "/");
CHECK_EQ((url / "").path(), "/");
CHECK_EQ((url / " ").path(), "/ ");
CHECK_EQ((url / "/").path(), "/");
CHECK_EQ((url / "page").path(), "/page");
CHECK_EQ((url / "/page").path(), "/page");
CHECK_EQ((url / " /page").path(), "/ /page");
CHECK_EQ(url.path(), "/"); // unchanged
url.append_path("folder");
CHECK_EQ(url.path(), "/folder");
CHECK_EQ((url / "").path(), "/folder");
CHECK_EQ((url / "/").path(), "/folder/");
CHECK_EQ((url / "page").path(), "/folder/page");
CHECK_EQ((url / "/page").path(), "/folder/page");
url.append_path("folder");
CHECK_EQ(url.path(), "/folder");
CHECK_EQ((url / "").path(), "/folder");
CHECK_EQ((url / "/").path(), "/folder/");
CHECK_EQ((url / "page").path(), "/folder/page");
CHECK_EQ((url / "/page").path(), "/folder/page");
}
SUBCASE("Absolute paths")
{
url.set_scheme("file");
url.append_path("C:/folder/file.txt");
if (on_win)
{
CHECK_EQ(url.str(), "file:///C:/folder/file.txt");
}
else
{
CHECK_EQ(url.str(), "file:///C%3A/folder/file.txt");
}
}
}
}

View File

@ -31,9 +31,13 @@ TEST_SUITE("util::url_manip")
// Does NOT parse URL
CHECK_EQ(url_encode("https://foo/"), "https%3A%2F%2Ffoo%2F");
// Exclude characters
CHECK_EQ(url_encode(" /word%", '/'), "%20/word%25");
CHECK_EQ(url_decode(""), "");
CHECK_EQ(url_decode("page"), "page");
CHECK_EQ(url_decode("%20%2Fword%25"), " /word%");
CHECK_EQ(url_decode(" /word%25"), " /word%");
CHECK_EQ(url_decode("user%40email.com"), "user@email.com");
CHECK_EQ(url_decode("https%3A%2F%2Ffoo%2F"), "https://foo/");
CHECK_EQ(