Repository: mamba-org/mamba (C++)

Issue:
Question: syntax for abs paths passed as `file://` URLs in Windows?
What's the actual way? Given this absolute path in Windows `D:\a_\temp\popen-gw0\test_croot_with_spaces0\space path`:

```python
>>> from libmambapy.specs import CondaURL
>>> CondaURL.parse("file:///D:/a/_temp/popen-gw0/test_croot_with_spaces0/space path")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
libmambapy.bindings.specs.ParseError: Failed to parse URL "file:///D:/a/_temp/popen-gw0/test_croot_with_spaces0/space path": Malformed input to a URL function
>>> CondaURL.parse("file:///D:/a/_temp/popen-gw0/test_croot_with_spaces0/space%20path
")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
libmambapy.bindings.specs.ParseError: Failed to parse URL "file:///D:/a/_temp/popen-gw0/test_croot_with_spaces0/space%20path": Bad file:// URL
>>> CondaURL.parse("file:///D/a/_temp/popen-gw0/test_croot_with_spaces0/space path")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
libmambapy.bindings.specs.ParseError: Failed to parse URL "file:///D/a/_temp/popen-gw0/test_croot_with_spaces0/space path": Malformed input to a URL function
>>> CondaURL.parse("file:///D/a/_temp/popen-gw0/test_croot_with_spaces0/space%20path"
)
file:///D/a/_temp/popen-gw0/test_croot_with_spaces0/space%20path
```

Only the last one (without the `:` in the drive letter) is accepted, but not sure if that's understood as a Windows path internally? Should I always remove the `:` in the drive?


Discussion: Seeing comments like this one:

https://github.com/mamba-org/mamba/blob/31b23473e059b47c03b065e5592ea5a7cf77fc9b/libmamba/include/mamba/util/url.hpp#L186-L192

makes me think it should be accepted?
Per this [blogpost](https://learn.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows) from the IE team back in 2006, the proper syntax is as follows:

<img width="516" alt="Screenshot 2025-01-06 at 16 17 23" src="https://github.com/user-attachments/assets/9bbc0aea-9dbd-43e2-84d7-a8bca3af8ee5" />

Reading the `file_uri_unc2_to_unc4` implementation:

https://github.com/mamba-org/mamba/blob/0abd7d54e727111448f8ea0d547189a4ce7a64a1/libmamba/src/util/url_manip.cpp#L88-L133

And testing some more, it seems `CondaURL` wants 4 leading `/` and `%`-encoded spaces:

```pycon
>>> from libmambapy.specs import CondaURL
>>> CondaURL.parse("file:////D:/a_/temp/popen-gw0/test_croot_with_spaces0/space%20path")
file:////D:/a_/temp/popen-gw0/test_croot_with_spaces0/space%20path
```
It works with 4+ leading `/`, so I don't think it was intended. `file:///C:/path/to/file` should be legal imo.
Is this a question or more like a bug? 🤔 
When using 2 or 3 slashes, the error is the same (`Bad file:// URL`), and comes from `libcurl` when calling `curl_url_set` [here](https://github.com/mamba-org/mamba/blob/main/libmamba/src/util/url.cpp#L106-L110).
From `curl` [documentation](https://curl.se/docs/manpage.html):
```
FILE
Read or write local files. curl does not support accessing file:// URL remotely, but when running on Microsoft Windows using the native UNC approach works. 
```
It seems that only `file://\\` is accepted, thus the 4 slashes.
I believe that indeed `file:///C:/...` should work, it also work for me correctly when I test with `curl` on my Windows.
Also should work:
- `file://C:\\...\\...`
- `file://\\C: ...`

It's not clear to me why a 4th `/` is added at the end of that function, it doesnt make sense path-wise.
> I believe that indeed file:///C:/... should work, it also work for me correctly when I test with curl on my Windows.

Hum, what do you use exactly when you test with curl? Is it a command line?
So far, the issue that I observed `Bad file:// URL` occurs when using `curl_url_set`.
> Hum, what do you use exactly when you test with curl? Is it a command line? 

Yes that's when using the `curl` provided with `git-bash` (`C:\Program Files\Git\mingw64\bin\curl.exe`, but I have other versions installed like the system one `C:\Windows\System32\curl.exe` which confirms the same output). I can also try to make a test exe and test with calling libcurl (from conda-forge packages) directly to confirm.

My understanding so far is that in no cases does `file:////` is supposed to be valid. `file://\\` would be as it's just 1 (anti-)slash for absolute path. Is my understanding correct? (I might be missing something here).

> Yes that's when using the curl provided with git-bash (C:\Program Files\Git\mingw64\bin\curl.exe, but I have other versions installed like the system one C:\Windows\System32\curl.exe which confirms the same output). I can also try to make a test exe and test with calling libcurl (from conda-forge packages) directly to confirm.

Yes I mean the issue seems to be related to `curl_url_set` function specifically (in the `mamba` parsing case at least), so if you are running a command with `curl`, the behavior may be different...
So what's the status here? Are we supposed to pass four slashes, three, two...? Or should this be handled internally in libmamba so library users are not exposed to implementation details in libcurl?
Sorry for the late answer, Mamba should supports any valid URI, independently from curl. Its implementation detail should not leak to the end user.

EDIT: labelling this as a bug.



Fix (patch):
diff --git a/libmamba/include/mamba/specs/conda_url.hpp b/libmamba/include/mamba/specs/conda_url.hpp
index caa46ba94f..27d511b030 100644
--- a/libmamba/include/mamba/specs/conda_url.hpp
+++ b/libmamba/include/mamba/specs/conda_url.hpp
@@ -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. */
diff --git a/libmamba/include/mamba/util/url_manip.hpp b/libmamba/include/mamba/util/url_manip.hpp
index 768b1c3608..90fa42d1b0 100644
--- a/libmamba/include/mamba/util/url_manip.hpp
+++ b/libmamba/include/mamba/util/url_manip.hpp
@@ -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.
      *
diff --git a/libmamba/src/util/url.cpp b/libmamba/src/util/url.cpp
index 480f2012e6..2966a9a78c 100644
--- a/libmamba/src/util/url.cpp
+++ b/libmamba/src/util/url.cpp
@@ -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
                 {
diff --git a/libmamba/src/util/url_manip.cpp b/libmamba/src/util/url_manip.cpp
index 6891a17c34..c771338862 100644
--- a/libmamba/src/util/url_manip.cpp
+++ b/libmamba/src/util/url_manip.cpp
@@ -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);