Skip to content

Commit 1b3b9e1

Browse files
authored
[Unix] Fix slashes usage in file urls (#3871)
1 parent dd30a5c commit 1b3b9e1

File tree

7 files changed

+222
-12
lines changed

7 files changed

+222
-12
lines changed

libmamba/include/mamba/specs/conda_url.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ namespace mamba::specs
2929

3030
inline static constexpr std::string_view token_prefix = "/t/";
3131

32+
/** Parse a string url.
33+
* The url must be percent encoded beforehand.
34+
* cf. https://en.wikipedia.org/wiki/Percent-encoding
35+
*/
3236
[[nodiscard]] static auto parse(std::string_view url) -> expected_parse_t<CondaURL>;
3337

3438
/** Create a local URL. */

libmamba/include/mamba/util/url_manip.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ namespace mamba::util
6363
template <typename... Args>
6464
[[nodiscard]] auto url_concat(const Args&... args) -> std::string;
6565

66+
[[nodiscard]] auto make_curl_compatible(std::string url) -> std::string;
67+
6668
/**
6769
* Convert UNC2 file URI to UNC4.
6870
*

libmamba/src/util/url.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,10 @@ namespace mamba::util
185185
{
186186
return tl::make_unexpected(ParseError{ "Empty URL" });
187187
}
188-
return CurlUrl::parse(file_uri_unc2_to_unc4(url), CURLU_NON_SUPPORT_SCHEME | CURLU_DEFAULT_SCHEME)
188+
return CurlUrl::parse(
189+
make_curl_compatible(file_uri_unc2_to_unc4(url)),
190+
CURLU_NON_SUPPORT_SCHEME | CURLU_DEFAULT_SCHEME
191+
)
189192
.transform(
190193
[&](CurlUrl&& handle) -> URL
191194
{

libmamba/src/util/url_manip.cpp

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,44 @@ namespace mamba::util
8585
return path_to_url(path);
8686
}
8787

88-
auto file_uri_unc2_to_unc4(std::string_view uri) -> std::string
88+
auto check_file_scheme_and_slashes(std::string_view uri)
89+
-> std::tuple<bool, std::string_view, std::string_view>
8990
{
9091
static constexpr std::string_view file_scheme = "file:";
9192

9293
// Not "file:" scheme
9394
if (!util::starts_with(uri, file_scheme))
95+
{
96+
return { false, {}, {} };
97+
}
98+
99+
auto [slashes, rest] = util::lstrip_parts(util::remove_prefix(uri, file_scheme), '/');
100+
return { true, slashes, rest };
101+
}
102+
103+
auto make_curl_compatible(std::string uri) -> std::string
104+
{
105+
// Convert `file://` and `file:///` to `file:////`
106+
// when followed with a drive letter
107+
// to make it compatible with libcurl on unix
108+
auto [is_file_scheme, slashes, rest] = check_file_scheme_and_slashes(uri);
109+
if (!on_win && is_file_scheme && path_has_drive_letter(rest)
110+
&& ((slashes.size() == 2) || (slashes.size() == 3)))
111+
{
112+
return util::concat("file:////", rest);
113+
}
114+
return uri;
115+
}
116+
117+
auto file_uri_unc2_to_unc4(std::string_view uri) -> std::string
118+
{
119+
auto [is_file_scheme, slashes, rest] = check_file_scheme_and_slashes(uri);
120+
if (!is_file_scheme)
94121
{
95122
return std::string(uri);
96123
}
97124

98125
// No hostname set in "file://hostname/path/to/data.xml"
99-
auto [slashes, rest] = util::lstrip_parts(util::remove_prefix(uri, file_scheme), '/');
100126
if (slashes.size() != 2)
101127
{
102128
return std::string(uri);

libmamba/tests/src/specs/test_conda_url.cpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <catch2/catch_all.hpp>
99

1010
#include "mamba/specs/conda_url.hpp"
11+
#include "mamba/util/build.hpp"
1112

1213
using namespace mamba::specs;
1314

@@ -488,4 +489,96 @@ namespace
488489
REQUIRE(url.pretty_str() == "https://[email protected]:*****@mamba.org/some /path$/");
489490
}
490491
}
492+
493+
TEST_CASE("CondaURL::parse")
494+
{
495+
SECTION("File URL with 4 slashes, a drive letter, and percent encoded space")
496+
{
497+
// The URL passed to `CondaURL::parse` must be percent encoded
498+
auto url = CondaURL::parse("file:////D:/a/_temp/popen-gw0/some_other_parts%20spaces").value();
499+
REQUIRE(url.path() == "//D:/a/_temp/popen-gw0/some_other_parts spaces");
500+
REQUIRE(
501+
url.path(CondaURL::Decode::no) == "//D:/a/_temp/popen-gw0/some_other_parts%20spaces"
502+
);
503+
REQUIRE(url.str() == "file:////D:/a/_temp/popen-gw0/some_other_parts%20spaces");
504+
REQUIRE(url.pretty_str() == "file:////D:/a/_temp/popen-gw0/some_other_parts spaces");
505+
}
506+
507+
SECTION("File URL with 4 slashes, a drive letter, and non-encoded space")
508+
{
509+
// The URL passed to `CondaURL::parse` must be percent encoded
510+
REQUIRE_FALSE(
511+
CondaURL::parse("file:////D:/a/_temp/popen-gw0/some_other_parts spaces").has_value()
512+
);
513+
}
514+
515+
SECTION("File URL with 4 slashes")
516+
{
517+
auto url = CondaURL::parse("file:////ab/_temp/popen-gw0/some_other_parts").value();
518+
REQUIRE(url.path() == "//ab/_temp/popen-gw0/some_other_parts");
519+
REQUIRE(url.str() == "file:////ab/_temp/popen-gw0/some_other_parts");
520+
REQUIRE(url.pretty_str() == "file:////ab/_temp/popen-gw0/some_other_parts");
521+
}
522+
523+
SECTION("File URL with 3 slashes and drive letter")
524+
{
525+
auto url = CondaURL::parse("file:///D:/a/_temp/popen-gw0/some_other_parts").value();
526+
if (mamba::util::on_win)
527+
{
528+
REQUIRE(url.path() == "/D:/a/_temp/popen-gw0/some_other_parts");
529+
REQUIRE(url.str() == "file:///D:/a/_temp/popen-gw0/some_other_parts");
530+
REQUIRE(url.pretty_str() == "file:///D:/a/_temp/popen-gw0/some_other_parts");
531+
}
532+
else
533+
{
534+
REQUIRE(url.path() == "//D:/a/_temp/popen-gw0/some_other_parts");
535+
REQUIRE(url.str() == "file:////D:/a/_temp/popen-gw0/some_other_parts");
536+
REQUIRE(url.pretty_str() == "file:////D:/a/_temp/popen-gw0/some_other_parts");
537+
}
538+
}
539+
540+
SECTION("File URL with 3 slashes")
541+
{
542+
auto url = CondaURL::parse("file:///ab/_temp/popen-gw0/some_other_parts").value();
543+
REQUIRE(url.path() == "/ab/_temp/popen-gw0/some_other_parts");
544+
REQUIRE(url.str() == "file:///ab/_temp/popen-gw0/some_other_parts");
545+
REQUIRE(url.pretty_str() == "file:///ab/_temp/popen-gw0/some_other_parts");
546+
}
547+
548+
SECTION("File URL with 2 slashes and drive letter")
549+
{
550+
auto url = CondaURL::parse("file://D:/a/_temp/popen-gw0/some_other_parts").value();
551+
if (mamba::util::on_win)
552+
{
553+
REQUIRE(url.path() == "/D:/a/_temp/popen-gw0/some_other_parts");
554+
REQUIRE(url.str() == "file:///D:/a/_temp/popen-gw0/some_other_parts");
555+
REQUIRE(url.pretty_str() == "file:///D:/a/_temp/popen-gw0/some_other_parts");
556+
}
557+
else
558+
{
559+
REQUIRE(url.path() == "//D:/a/_temp/popen-gw0/some_other_parts");
560+
REQUIRE(url.str() == "file:////D:/a/_temp/popen-gw0/some_other_parts");
561+
REQUIRE(url.pretty_str() == "file:////D:/a/_temp/popen-gw0/some_other_parts");
562+
}
563+
}
564+
565+
SECTION("File URL with 2 slashes")
566+
{
567+
auto url = CondaURL::parse("file://ab/_temp/popen-gw0/some_other_parts").value();
568+
REQUIRE(url.path() == "//ab/_temp/popen-gw0/some_other_parts");
569+
REQUIRE(url.str() == "file:////ab/_temp/popen-gw0/some_other_parts");
570+
REQUIRE(url.pretty_str() == "file:////ab/_temp/popen-gw0/some_other_parts");
571+
}
572+
573+
// NOTE This is not valid on any platform:
574+
// "file://\\D:/a/_temp/popen-gw0/some_other_parts"
575+
576+
SECTION("file://\\abcd/_temp/popen-gw0/some_other_parts")
577+
{
578+
auto url = CondaURL::parse("file://\\abcd/_temp/popen-gw0/some_other_parts").value();
579+
REQUIRE(url.path() == "//\\abcd/_temp/popen-gw0/some_other_parts");
580+
REQUIRE(url.str() == "file:////\\abcd/_temp/popen-gw0/some_other_parts");
581+
REQUIRE(url.pretty_str() == "file:////\\abcd/_temp/popen-gw0/some_other_parts");
582+
}
583+
}
491584
}

libmamba/tests/src/util/test_url.cpp

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ namespace
200200
}
201201
}
202202

203-
TEST_CASE("UTL parse")
203+
TEST_CASE("URL parse")
204204
{
205205
SECTION("Empty")
206206
{
@@ -311,19 +311,25 @@ namespace
311311

312312
SECTION("file://C:/Users/wolfv/test/document.json")
313313
{
314+
const URL url = URL::parse("file://C:/Users/wolfv/test/document.json").value();
315+
REQUIRE(url.scheme() == "file");
316+
REQUIRE(url.host() == "");
317+
REQUIRE(url.user() == "");
318+
REQUIRE(url.password() == "");
319+
REQUIRE(url.port() == "");
320+
REQUIRE(url.query() == "");
321+
REQUIRE(url.fragment() == "");
314322
if (on_win)
315323
{
316-
const URL url = URL::parse("file://C:/Users/wolfv/test/document.json").value();
317-
REQUIRE(url.scheme() == "file");
318-
REQUIRE(url.host() == "");
319324
REQUIRE(url.path() == "/C:/Users/wolfv/test/document.json");
320325
REQUIRE(url.path(URL::Decode::no) == "/C:/Users/wolfv/test/document.json");
321326
REQUIRE(url.pretty_path() == "C:/Users/wolfv/test/document.json");
322-
REQUIRE(url.user() == "");
323-
REQUIRE(url.password() == "");
324-
REQUIRE(url.port() == "");
325-
REQUIRE(url.query() == "");
326-
REQUIRE(url.fragment() == "");
327+
}
328+
else
329+
{
330+
REQUIRE(url.path() == "//C:/Users/wolfv/test/document.json");
331+
REQUIRE(url.path(URL::Decode::no) == "//C:/Users/wolfv/test/document.json");
332+
REQUIRE(url.pretty_path() == "//C:/Users/wolfv/test/document.json");
327333
}
328334
}
329335

@@ -341,6 +347,42 @@ namespace
341347
REQUIRE(url.fragment() == "");
342348
}
343349

350+
SECTION("file:///D:/a/_temp/popen-gw0/some_other_parts")
351+
{
352+
const URL url = URL::parse("file:///D:/a/_temp/popen-gw0/some_other_parts").value();
353+
REQUIRE(url.scheme() == "file");
354+
REQUIRE(url.host() == "");
355+
REQUIRE(url.user() == "");
356+
REQUIRE(url.password() == "");
357+
REQUIRE(url.port() == "");
358+
REQUIRE(url.query() == "");
359+
REQUIRE(url.fragment() == "");
360+
if (on_win)
361+
{
362+
REQUIRE(url.path() == "/D:/a/_temp/popen-gw0/some_other_parts");
363+
REQUIRE(url.pretty_path() == "D:/a/_temp/popen-gw0/some_other_parts");
364+
}
365+
else
366+
{
367+
REQUIRE(url.path() == "//D:/a/_temp/popen-gw0/some_other_parts");
368+
REQUIRE(url.pretty_path() == "//D:/a/_temp/popen-gw0/some_other_parts");
369+
}
370+
}
371+
372+
SECTION("file:////D:/a/_temp/popen-gw0/some_other_parts")
373+
{
374+
const URL url = URL::parse("file:////D:/a/_temp/popen-gw0/some_other_parts").value();
375+
REQUIRE(url.scheme() == "file");
376+
REQUIRE(url.host() == "");
377+
REQUIRE(url.path() == "//D:/a/_temp/popen-gw0/some_other_parts");
378+
REQUIRE(url.pretty_path() == "//D:/a/_temp/popen-gw0/some_other_parts");
379+
REQUIRE(url.user() == "");
380+
REQUIRE(url.password() == "");
381+
REQUIRE(url.port() == "");
382+
REQUIRE(url.query() == "");
383+
REQUIRE(url.fragment() == "");
384+
}
385+
344386
SECTION("file:///home/great:doc.json")
345387
{
346388
// Not a valid IETF RFC 3986+ URL, but Curl parses it anyways.

libmamba/tests/src/util/test_url_manip.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,46 @@ namespace
154154
);
155155
}
156156

157+
TEST_CASE("make_curl_compatible")
158+
{
159+
for (const std::string uri : {
160+
"http://example.com/test",
161+
R"(file:////C:/Program\ (x74)/Users/hello\ world)",
162+
"file:////server/share",
163+
"file:///server/share",
164+
"file://absolute/path",
165+
R"(file://\\D:/server/share)",
166+
R"(file://\\server\path)",
167+
})
168+
{
169+
CAPTURE(uri);
170+
REQUIRE(make_curl_compatible(uri) == uri);
171+
}
172+
173+
if (on_win)
174+
{
175+
REQUIRE(
176+
make_curl_compatible(R"(file://C:/Program\ (x74)/Users/hello\ world)")
177+
== R"(file://C:/Program\ (x74)/Users/hello\ world)"
178+
);
179+
REQUIRE(
180+
make_curl_compatible(R"(file:///C:/Program\ (x74)/Users/hello\ world)")
181+
== R"(file:///C:/Program\ (x74)/Users/hello\ world)"
182+
);
183+
}
184+
else
185+
{
186+
REQUIRE(
187+
make_curl_compatible(R"(file://C:/Program\ (x74)/Users/hello\ world)")
188+
== R"(file:////C:/Program\ (x74)/Users/hello\ world)"
189+
);
190+
REQUIRE(
191+
make_curl_compatible(R"(file:///C:/Program\ (x74)/Users/hello\ world)")
192+
== R"(file:////C:/Program\ (x74)/Users/hello\ world)"
193+
);
194+
}
195+
}
196+
157197
TEST_CASE("file_uri_unc2_to_unc4")
158198
{
159199
for (const std::string uri : {

0 commit comments

Comments
 (0)