[Unix] Fix slashes usage in file urls (#3871)

This commit is contained in:
Hind-M 2025-05-12 13:54:45 +02:00 committed by GitHub
parent dd30a5c287
commit 1b3b9e1b25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 222 additions and 12 deletions

View File

@ -29,6 +29,10 @@ namespace mamba::specs
inline static constexpr std::string_view token_prefix = "/t/";
/** Parse a string url.
* The url must be percent encoded beforehand.
* cf. https://en.wikipedia.org/wiki/Percent-encoding
*/
[[nodiscard]] static auto parse(std::string_view url) -> expected_parse_t<CondaURL>;
/** Create a local URL. */

View File

@ -63,6 +63,8 @@ namespace mamba::util
template <typename... Args>
[[nodiscard]] auto url_concat(const Args&... args) -> std::string;
[[nodiscard]] auto make_curl_compatible(std::string url) -> std::string;
/**
* Convert UNC2 file URI to UNC4.
*

View File

@ -185,7 +185,10 @@ namespace mamba::util
{
return tl::make_unexpected(ParseError{ "Empty URL" });
}
return CurlUrl::parse(file_uri_unc2_to_unc4(url), CURLU_NON_SUPPORT_SCHEME | CURLU_DEFAULT_SCHEME)
return CurlUrl::parse(
make_curl_compatible(file_uri_unc2_to_unc4(url)),
CURLU_NON_SUPPORT_SCHEME | CURLU_DEFAULT_SCHEME
)
.transform(
[&](CurlUrl&& handle) -> URL
{

View File

@ -85,18 +85,44 @@ namespace mamba::util
return path_to_url(path);
}
auto file_uri_unc2_to_unc4(std::string_view uri) -> std::string
auto check_file_scheme_and_slashes(std::string_view uri)
-> std::tuple<bool, std::string_view, std::string_view>
{
static constexpr std::string_view file_scheme = "file:";
// Not "file:" scheme
if (!util::starts_with(uri, file_scheme))
{
return { false, {}, {} };
}
auto [slashes, rest] = util::lstrip_parts(util::remove_prefix(uri, file_scheme), '/');
return { true, slashes, rest };
}
auto make_curl_compatible(std::string uri) -> std::string
{
// Convert `file://` and `file:///` to `file:////`
// when followed with a drive letter
// to make it compatible with libcurl on unix
auto [is_file_scheme, slashes, rest] = check_file_scheme_and_slashes(uri);
if (!on_win && is_file_scheme && path_has_drive_letter(rest)
&& ((slashes.size() == 2) || (slashes.size() == 3)))
{
return util::concat("file:////", rest);
}
return uri;
}
auto file_uri_unc2_to_unc4(std::string_view uri) -> std::string
{
auto [is_file_scheme, slashes, rest] = check_file_scheme_and_slashes(uri);
if (!is_file_scheme)
{
return std::string(uri);
}
// No hostname set in "file://hostname/path/to/data.xml"
auto [slashes, rest] = util::lstrip_parts(util::remove_prefix(uri, file_scheme), '/');
if (slashes.size() != 2)
{
return std::string(uri);

View File

@ -8,6 +8,7 @@
#include <catch2/catch_all.hpp>
#include "mamba/specs/conda_url.hpp"
#include "mamba/util/build.hpp"
using namespace mamba::specs;
@ -488,4 +489,96 @@ namespace
REQUIRE(url.pretty_str() == "https://user@email.com:*****@mamba.org/some /path$/");
}
}
TEST_CASE("CondaURL::parse")
{
SECTION("File URL with 4 slashes, a drive letter, and percent encoded space")
{
// The URL passed to `CondaURL::parse` must be percent encoded
auto url = CondaURL::parse("file:////D:/a/_temp/popen-gw0/some_other_parts%20spaces").value();
REQUIRE(url.path() == "//D:/a/_temp/popen-gw0/some_other_parts spaces");
REQUIRE(
url.path(CondaURL::Decode::no) == "//D:/a/_temp/popen-gw0/some_other_parts%20spaces"
);
REQUIRE(url.str() == "file:////D:/a/_temp/popen-gw0/some_other_parts%20spaces");
REQUIRE(url.pretty_str() == "file:////D:/a/_temp/popen-gw0/some_other_parts spaces");
}
SECTION("File URL with 4 slashes, a drive letter, and non-encoded space")
{
// The URL passed to `CondaURL::parse` must be percent encoded
REQUIRE_FALSE(
CondaURL::parse("file:////D:/a/_temp/popen-gw0/some_other_parts spaces").has_value()
);
}
SECTION("File URL with 4 slashes")
{
auto url = CondaURL::parse("file:////ab/_temp/popen-gw0/some_other_parts").value();
REQUIRE(url.path() == "//ab/_temp/popen-gw0/some_other_parts");
REQUIRE(url.str() == "file:////ab/_temp/popen-gw0/some_other_parts");
REQUIRE(url.pretty_str() == "file:////ab/_temp/popen-gw0/some_other_parts");
}
SECTION("File URL with 3 slashes and drive letter")
{
auto url = CondaURL::parse("file:///D:/a/_temp/popen-gw0/some_other_parts").value();
if (mamba::util::on_win)
{
REQUIRE(url.path() == "/D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.str() == "file:///D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.pretty_str() == "file:///D:/a/_temp/popen-gw0/some_other_parts");
}
else
{
REQUIRE(url.path() == "//D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.str() == "file:////D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.pretty_str() == "file:////D:/a/_temp/popen-gw0/some_other_parts");
}
}
SECTION("File URL with 3 slashes")
{
auto url = CondaURL::parse("file:///ab/_temp/popen-gw0/some_other_parts").value();
REQUIRE(url.path() == "/ab/_temp/popen-gw0/some_other_parts");
REQUIRE(url.str() == "file:///ab/_temp/popen-gw0/some_other_parts");
REQUIRE(url.pretty_str() == "file:///ab/_temp/popen-gw0/some_other_parts");
}
SECTION("File URL with 2 slashes and drive letter")
{
auto url = CondaURL::parse("file://D:/a/_temp/popen-gw0/some_other_parts").value();
if (mamba::util::on_win)
{
REQUIRE(url.path() == "/D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.str() == "file:///D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.pretty_str() == "file:///D:/a/_temp/popen-gw0/some_other_parts");
}
else
{
REQUIRE(url.path() == "//D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.str() == "file:////D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.pretty_str() == "file:////D:/a/_temp/popen-gw0/some_other_parts");
}
}
SECTION("File URL with 2 slashes")
{
auto url = CondaURL::parse("file://ab/_temp/popen-gw0/some_other_parts").value();
REQUIRE(url.path() == "//ab/_temp/popen-gw0/some_other_parts");
REQUIRE(url.str() == "file:////ab/_temp/popen-gw0/some_other_parts");
REQUIRE(url.pretty_str() == "file:////ab/_temp/popen-gw0/some_other_parts");
}
// NOTE This is not valid on any platform:
// "file://\\D:/a/_temp/popen-gw0/some_other_parts"
SECTION("file://\\abcd/_temp/popen-gw0/some_other_parts")
{
auto url = CondaURL::parse("file://\\abcd/_temp/popen-gw0/some_other_parts").value();
REQUIRE(url.path() == "//\\abcd/_temp/popen-gw0/some_other_parts");
REQUIRE(url.str() == "file:////\\abcd/_temp/popen-gw0/some_other_parts");
REQUIRE(url.pretty_str() == "file:////\\abcd/_temp/popen-gw0/some_other_parts");
}
}
}

View File

@ -200,7 +200,7 @@ namespace
}
}
TEST_CASE("UTL parse")
TEST_CASE("URL parse")
{
SECTION("Empty")
{
@ -311,19 +311,25 @@ namespace
SECTION("file://C:/Users/wolfv/test/document.json")
{
const URL url = URL::parse("file://C:/Users/wolfv/test/document.json").value();
REQUIRE(url.scheme() == "file");
REQUIRE(url.host() == "");
REQUIRE(url.user() == "");
REQUIRE(url.password() == "");
REQUIRE(url.port() == "");
REQUIRE(url.query() == "");
REQUIRE(url.fragment() == "");
if (on_win)
{
const URL url = URL::parse("file://C:/Users/wolfv/test/document.json").value();
REQUIRE(url.scheme() == "file");
REQUIRE(url.host() == "");
REQUIRE(url.path() == "/C:/Users/wolfv/test/document.json");
REQUIRE(url.path(URL::Decode::no) == "/C:/Users/wolfv/test/document.json");
REQUIRE(url.pretty_path() == "C:/Users/wolfv/test/document.json");
REQUIRE(url.user() == "");
REQUIRE(url.password() == "");
REQUIRE(url.port() == "");
REQUIRE(url.query() == "");
REQUIRE(url.fragment() == "");
}
else
{
REQUIRE(url.path() == "//C:/Users/wolfv/test/document.json");
REQUIRE(url.path(URL::Decode::no) == "//C:/Users/wolfv/test/document.json");
REQUIRE(url.pretty_path() == "//C:/Users/wolfv/test/document.json");
}
}
@ -341,6 +347,42 @@ namespace
REQUIRE(url.fragment() == "");
}
SECTION("file:///D:/a/_temp/popen-gw0/some_other_parts")
{
const URL url = URL::parse("file:///D:/a/_temp/popen-gw0/some_other_parts").value();
REQUIRE(url.scheme() == "file");
REQUIRE(url.host() == "");
REQUIRE(url.user() == "");
REQUIRE(url.password() == "");
REQUIRE(url.port() == "");
REQUIRE(url.query() == "");
REQUIRE(url.fragment() == "");
if (on_win)
{
REQUIRE(url.path() == "/D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.pretty_path() == "D:/a/_temp/popen-gw0/some_other_parts");
}
else
{
REQUIRE(url.path() == "//D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.pretty_path() == "//D:/a/_temp/popen-gw0/some_other_parts");
}
}
SECTION("file:////D:/a/_temp/popen-gw0/some_other_parts")
{
const URL url = URL::parse("file:////D:/a/_temp/popen-gw0/some_other_parts").value();
REQUIRE(url.scheme() == "file");
REQUIRE(url.host() == "");
REQUIRE(url.path() == "//D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.pretty_path() == "//D:/a/_temp/popen-gw0/some_other_parts");
REQUIRE(url.user() == "");
REQUIRE(url.password() == "");
REQUIRE(url.port() == "");
REQUIRE(url.query() == "");
REQUIRE(url.fragment() == "");
}
SECTION("file:///home/great:doc.json")
{
// Not a valid IETF RFC 3986+ URL, but Curl parses it anyways.

View File

@ -154,6 +154,46 @@ namespace
);
}
TEST_CASE("make_curl_compatible")
{
for (const std::string uri : {
"http://example.com/test",
R"(file:////C:/Program\ (x74)/Users/hello\ world)",
"file:////server/share",
"file:///server/share",
"file://absolute/path",
R"(file://\\D:/server/share)",
R"(file://\\server\path)",
})
{
CAPTURE(uri);
REQUIRE(make_curl_compatible(uri) == uri);
}
if (on_win)
{
REQUIRE(
make_curl_compatible(R"(file://C:/Program\ (x74)/Users/hello\ world)")
== R"(file://C:/Program\ (x74)/Users/hello\ world)"
);
REQUIRE(
make_curl_compatible(R"(file:///C:/Program\ (x74)/Users/hello\ world)")
== R"(file:///C:/Program\ (x74)/Users/hello\ world)"
);
}
else
{
REQUIRE(
make_curl_compatible(R"(file://C:/Program\ (x74)/Users/hello\ world)")
== R"(file:////C:/Program\ (x74)/Users/hello\ world)"
);
REQUIRE(
make_curl_compatible(R"(file:///C:/Program\ (x74)/Users/hello\ world)")
== R"(file:////C:/Program\ (x74)/Users/hello\ world)"
);
}
}
TEST_CASE("file_uri_unc2_to_unc4")
{
for (const std::string uri : {