From 659e22eb6d78ad91a1b70064377dcc363f6c0c9c Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 19 May 2026 17:51:45 -0400 Subject: [PATCH 1/2] Fix Windows UNC support on `URI::to_path()` Signed-off-by: Juan Cruz Viotti --- src/core/uri/filesystem.cc | 20 ++++++++++++++++++++ test/uri/uri_to_path_test.cc | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/core/uri/filesystem.cc b/src/core/uri/filesystem.cc index d9d8da269..864011999 100644 --- a/src/core/uri/filesystem.cc +++ b/src/core/uri/filesystem.cc @@ -17,6 +17,26 @@ auto URI::to_path() const -> std::filesystem::path { return path; } + // RFC 8089: a non-empty, non-localhost host on a file URI denotes a UNC + // server. The "localhost" host is equivalent to no host + const auto host_value = this->host(); + const auto is_unc = host_value.has_value() && !host_value->empty() && + host_value.value() != "localhost"; + if (is_unc) { + if (!path.empty() && path.front() == '/') { + path.erase(0, 1); + } + std::ranges::replace(path, '/', '\\'); + uri_unescape_all_inplace(path); + std::string unc{"\\\\"}; + unc.append(host_value.value()); + if (!path.empty()) { + unc.push_back('\\'); + unc.append(path); + } + return unc; + } + // Check for Windows absolute path (e.g., /C:/) const auto is_windows_absolute = path.size() >= 3 && path[0] == '/' && path[2] == ':'; diff --git a/test/uri/uri_to_path_test.cc b/test/uri/uri_to_path_test.cc index 6422f95e3..6175990ce 100644 --- a/test/uri/uri_to_path_test.cc +++ b/test/uri/uri_to_path_test.cc @@ -92,3 +92,27 @@ TEST(URI_to_path, unicode_windows) { const std::filesystem::path expected{u8R"(C:\data\résumé.doc)"}; EXPECT_EQ(uri.to_path(), expected); } + +TEST(URI_to_path, windows_unc_simple) { + const sourcemeta::core::URI uri{"file://server/share/file.txt"}; + const std::filesystem::path expected{R"(\\server\share\file.txt)"}; + EXPECT_EQ(uri.to_path(), expected); +} + +TEST(URI_to_path, windows_unc_with_space) { + const sourcemeta::core::URI uri{"file://srv/My%20Docs/a%20b.txt"}; + const std::filesystem::path expected{R"(\\srv\My Docs\a b.txt)"}; + EXPECT_EQ(uri.to_path(), expected); +} + +TEST(URI_to_path, windows_unc_unicode) { + const sourcemeta::core::URI uri{"file://server/data/%C3%A9clair.txt"}; + const std::filesystem::path expected{u8R"(\\server\data\éclair.txt)"}; + EXPECT_EQ(uri.to_path(), expected); +} + +TEST(URI_to_path, localhost_treated_as_no_host) { + const sourcemeta::core::URI uri{"file://localhost/foo/bar"}; + const std::filesystem::path expected{"/foo/bar"}; + EXPECT_EQ(uri.to_path(), expected); +} From c98ca746ba157122254bf38669d23655329795e4 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 19 May 2026 17:59:17 -0400 Subject: [PATCH 2/2] More Signed-off-by: Juan Cruz Viotti --- src/core/uri/filesystem.cc | 24 +++++++++++++++++++----- test/uri/uri_to_path_test.cc | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/core/uri/filesystem.cc b/src/core/uri/filesystem.cc index 864011999..d1650cc24 100644 --- a/src/core/uri/filesystem.cc +++ b/src/core/uri/filesystem.cc @@ -2,10 +2,24 @@ #include "escaping.h" -#include // std::ranges::replace -#include // std::filesystem -#include // std::advance, std::next -#include // std::string +#include // std::ranges::equal, std::ranges::replace +#include // std::tolower +#include // std::filesystem +#include // std::advance, std::next +#include // std::string +#include // std::string_view + +namespace { + +auto is_localhost_host(const std::string_view host) -> bool { + constexpr std::string_view localhost{"localhost"}; + return std::ranges::equal( + host, localhost, [](const char left, const char right) { + return std::tolower(static_cast(left)) == right; + }); +} + +} // namespace namespace sourcemeta::core { @@ -21,7 +35,7 @@ auto URI::to_path() const -> std::filesystem::path { // server. The "localhost" host is equivalent to no host const auto host_value = this->host(); const auto is_unc = host_value.has_value() && !host_value->empty() && - host_value.value() != "localhost"; + !is_localhost_host(host_value.value()); if (is_unc) { if (!path.empty() && path.front() == '/') { path.erase(0, 1); diff --git a/test/uri/uri_to_path_test.cc b/test/uri/uri_to_path_test.cc index 6175990ce..5d1837675 100644 --- a/test/uri/uri_to_path_test.cc +++ b/test/uri/uri_to_path_test.cc @@ -116,3 +116,21 @@ TEST(URI_to_path, localhost_treated_as_no_host) { const std::filesystem::path expected{"/foo/bar"}; EXPECT_EQ(uri.to_path(), expected); } + +TEST(URI_to_path, localhost_with_windows_drive) { + const sourcemeta::core::URI uri{"file://localhost/C:/foo"}; + const std::filesystem::path expected{R"(C:\foo)"}; + EXPECT_EQ(uri.to_path(), expected); +} + +TEST(URI_to_path, localhost_uppercase) { + const sourcemeta::core::URI uri{"file://LOCALHOST/foo/bar"}; + const std::filesystem::path expected{"/foo/bar"}; + EXPECT_EQ(uri.to_path(), expected); +} + +TEST(URI_to_path, localhost_mixed_case) { + const sourcemeta::core::URI uri{"file://LocalHost/foo/bar"}; + const std::filesystem::path expected{"/foo/bar"}; + EXPECT_EQ(uri.to_path(), expected); +}