Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/core/uri/include/sourcemeta/core/uri.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
// NOLINTEND(misc-include-cleaner)

#include <cstdint> // std::uint32_t
#include <filesystem> // std::filesystem
#include <istream> // std::istream
#include <memory> // std::unique_ptr
#include <optional> // std::optional
#include <span> // std::span
Expand Down Expand Up @@ -439,6 +441,19 @@ class SOURCEMETA_CORE_URI_EXPORT URI {
/// ```
static auto from_fragment(std::string_view fragment) -> URI;

/// Create a URI from a file system path. For example:
///
/// ```cpp
/// #include <sourcemeta/core/uri.h>
/// #include <cassert>
/// #include <filesystem>
///
/// const std::filesystem::path path{"/foo/bar"};
/// const sourcemeta::core::URI uri{sourcemeta::core::URI::from_path(path)};
/// assert(uri.recompose() == "file:///foo/bar");
/// ```
static auto from_path(const std::filesystem::path &path) -> URI;

/// A convenient method to canonicalize and recompose a URI from a string. For
/// example:
///
Expand Down
58 changes: 56 additions & 2 deletions src/core/uri/uri.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

#include <sourcemeta/core/uri.h>

#include <algorithm> // std::replace
#include <cassert> // assert
#include <cstdint> // std::uint32_t
#include <filesystem> // std::filesystem
#include <istream> // std::istream
#include <optional> // std::optional
#include <sstream> // std::ostringstream
#include <sstream> // std::ostringstream, std::istringstream
#include <stdexcept> // std::length_error, std::runtime_error
#include <string> // std::stoul, std::string, std::tolower
#include <tuple> // std::tie
Expand Down Expand Up @@ -114,6 +114,20 @@ auto canonicalize_path(const std::string &path) -> std::optional<std::string> {
return canonical_path;
}

auto uri_escape_for_path(const std::string &value) -> std::string {
std::istringstream input{value};
std::ostringstream output;
uri_escape(input, output, sourcemeta::core::URIEscapeMode::SkipSubDelims);
auto result{output.str()};
// We don't want to escape ":" for Windows paths
std::string::size_type position = 0;
while ((position = result.find("%3A", position)) != std::string::npos) {
result.replace(position, 3, ":");
}

return result;
}

} // namespace

namespace sourcemeta::core {
Expand Down Expand Up @@ -708,4 +722,44 @@ auto URI::canonicalize(const std::string &input) -> std::string {
return URI{input}.canonicalize().recompose();
}

auto URI::from_path(const std::filesystem::path &path) -> URI {
auto normalized{path.lexically_normal().string()};
const auto is_unc{normalized.starts_with("\\\\")};
const auto is_windows_absolute{normalized.size() >= 2 &&
normalized[1] == ':'};
std::replace(normalized.begin(), normalized.end(), '\\', '/');
const auto is_unix_absolute{normalized.starts_with("/")};
if (!is_unix_absolute && !is_windows_absolute && !is_unc) {
throw URIError(
"It is not valid to construct a file:// URI out of a relative path");
}

normalized.erase(0, normalized.find_first_not_of('/'));
const std::filesystem::path final_path{normalized};

URI result{"file://"};

auto iterator{final_path.begin()};
if (is_unc) {
result.host_ = uri_escape_for_path(iterator->string());
std::advance(iterator, 1);
}

for (; iterator != final_path.end(); ++iterator) {
if (iterator->empty()) {
result.append_path("/");
} else if (*iterator == "/") {
if (std::next(iterator) == final_path.end()) {
result.append_path("/");
}
} else if (result.path_.has_value()) {
result.append_path(uri_escape_for_path(iterator->string()));
} else {
result.path_ = uri_escape_for_path(iterator->string());
}
}

return result;
}

} // namespace sourcemeta::core
1 change: 1 addition & 0 deletions test/uri/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ sourcemeta_googletest(NAMESPACE sourcemeta PROJECT core NAME uri
uri_empty_test.cc
uri_host_test.cc
uri_path_test.cc
uri_from_path_test.cc
uri_parse_test.cc
uri_port_test.cc
uri_scheme_test.cc
Expand Down
122 changes: 122 additions & 0 deletions test/uri/uri_from_path_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#include <gtest/gtest.h>

#include <sourcemeta/core/uri.h>

TEST(URI_from_path, unix_absolute) {
const std::filesystem::path example{"/foo/bar/baz"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(), "file:///foo/bar/baz");
}

TEST(URI_from_path, unix_with_space_and_reserved) {
const std::filesystem::path example{"/foo/My Folder/has#hash?value%"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(), "file:///foo/My%20Folder/has%23hash%3Fvalue%25");
}

TEST(URI_from_path, unix_trailing_slash) {
const std::filesystem::path example{"/foo/bar/"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(), "file:///foo/bar/");
}

TEST(URI_from_path, windows_drive_absolute) {
const std::filesystem::path example{R"(C:\Program Files\Test)"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(), "file:///C:/Program%20Files/Test");
}

TEST(URI_from_path, windows_drive_lowercase) {
const std::filesystem::path example{R"(c:\temp\logs)"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(), "file:///c:/temp/logs");
}

TEST(URI_from_path, windows_drive_root) {
const std::filesystem::path example{R"(D:\)"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(), "file:///D:/");
}

TEST(URI_from_path, windows_trailing_slash) {
const std::filesystem::path example{R"(C:\foo\bar\)"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(), "file:///C:/foo/bar/");
}

TEST(URI_from_path, windows_percent_and_plus) {
// '%' → %25, '+' is allowed unencoded
const std::filesystem::path example{R"(C:\path\50%+plus.txt)"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(), "file:///C:/path/50%25+plus.txt");
}

TEST(URI_from_path, windows_unc_simple) {
const std::filesystem::path example{R"(\\server\share\file.txt)"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(),
// For UNC, host=server, path=/share/file.txt
"file://server/share/file.txt");
}

TEST(URI_from_path, windows_unc_with_space) {
const std::filesystem::path example{R"(\\srv\My Docs\a b.txt)"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(), "file://srv/My%20Docs/a%20b.txt");
}

TEST(URI_from_path, unicode_unix) {
// U+00E9 (é) should be UTF-8 percent-encoded as %C3%A9
const std::filesystem::path example{u8"/data/éclair.txt"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(), "file:///data/%C3%A9clair.txt");
}

TEST(URI_from_path, unicode_windows) {
// U+00E9 (é) should be UTF-8 percent-encoded as %C3%A9
const std::filesystem::path example{u8R"(C:\data\résumé.doc)"};
const auto uri{sourcemeta::core::URI::from_path(example)};
EXPECT_EQ(uri.recompose(), "file:///C:/data/r%C3%A9sum%C3%A9.doc");
}

TEST(URI_from_path, unix_relative_simple) {
const std::filesystem::path example{"foo/bar/baz"};
EXPECT_THROW(sourcemeta::core::URI::from_path(example),
sourcemeta::core::URIError);
}

TEST(URI_from_path, unix_relative_with_dot) {
const std::filesystem::path example{"./foo/bar"};
EXPECT_THROW(sourcemeta::core::URI::from_path(example),
sourcemeta::core::URIError);
}

TEST(URI_from_path, unix_relative_with_dotdot) {
const std::filesystem::path example{"../parent/dir"};
EXPECT_THROW(sourcemeta::core::URI::from_path(example),
sourcemeta::core::URIError);
}

TEST(URI_from_path, unix_empty_path) {
const std::filesystem::path example{""};
EXPECT_THROW(sourcemeta::core::URI::from_path(example),
sourcemeta::core::URIError);
}

TEST(URI_from_path, windows_relative_simple) {
const std::filesystem::path example{"folder\\file.txt"};
EXPECT_THROW(sourcemeta::core::URI::from_path(example),
sourcemeta::core::URIError);
}

TEST(URI_from_path, windows_relative_with_dot) {
const std::filesystem::path example{".\\foo\\bar"};
EXPECT_THROW(sourcemeta::core::URI::from_path(example),
sourcemeta::core::URIError);
}

TEST(URI_from_path, windows_relative_with_dotdot) {
const std::filesystem::path example{"..\\up\\one\\level"};
EXPECT_THROW(sourcemeta::core::URI::from_path(example),
sourcemeta::core::URIError);
}