#include "nix/util/url.hh" #include "nix/util/tests/gmock-matchers.hh" #include #include #include namespace nix { /* ----------- tests for url.hh --------------------------------------------------*/ using Authority = ParsedURL::Authority; using HostType = Authority::HostType; struct FixGitURLParam { std::string input; std::string expected; ParsedURL parsed; }; std::ostream & operator<<(std::ostream & os, const FixGitURLParam & param) { return os << "Input: \"" << param.input << "\", Expected: \"" << param.expected << "\""; } class FixGitURLTestSuite : public ::testing::TestWithParam {}; INSTANTIATE_TEST_SUITE_P( FixGitURLs, FixGitURLTestSuite, ::testing::Values( // https://github.com/NixOS/nix/issues/5958 // Already proper URL with git+ssh FixGitURLParam{ .input = "git+ssh://user@domain:1234/path", .expected = "git+ssh://user@domain:1234/path", .parsed = ParsedURL{ .scheme = "git+ssh", .authority = ParsedURL::Authority{ .host = "domain", .user = "user", .port = 1234, }, .path = {"", "path"}, }, }, // SCP-like URL (rewritten to ssh://) FixGitURLParam{ .input = "git@github.com:owner/repo.git", .expected = "ssh://git@github.com/owner/repo.git", .parsed = ParsedURL{ .scheme = "ssh", .authority = ParsedURL::Authority{ .host = "github.com", .user = "git", }, .path = {"", "owner", "repo.git"}, }, }, // Absolute path (becomes file:) FixGitURLParam{ .input = "/home/me/repo", .expected = "file:///home/me/repo", .parsed = ParsedURL{ .scheme = "file", .authority = ParsedURL::Authority{}, .path = {"", "home", "me", "repo"}, }, }, // Already file: scheme // NOTE: Git/SCP treat this as a `:`, so we are // failing to "fix up" this case. FixGitURLParam{ .input = "file:/var/repos/x", .expected = "file:/var/repos/x", .parsed = ParsedURL{ .scheme = "file", .authority = std::nullopt, .path = {"", "var", "repos", "x"}, }, }, // IPV6 test case FixGitURLParam{ .input = "user@[2001:db8:1::2]:/home/file", .expected = "ssh://user@[2001:db8:1::2]//home/file", .parsed = ParsedURL{ .scheme = "ssh", .authority = ParsedURL::Authority{ .hostType = HostType::IPv6, .host = "2001:db8:1::2", .user = "user", }, .path = {"", "", "home", "file"}, }, })); TEST_P(FixGitURLTestSuite, parsesVariedGitUrls) { auto & p = GetParam(); const auto actual = fixGitURL(p.input); EXPECT_EQ(actual, p.parsed); EXPECT_EQ(actual.to_string(), p.expected); } TEST(FixGitURLTestSuite, scpLikeNoUserParsesPoorly) { // SCP-like URL (no user) // Cannot "to_string" this because has illegal path not starting // with `/`. EXPECT_EQ( fixGitURL("github.com:owner/repo.git"), (ParsedURL{ .scheme = "file", .authority = ParsedURL::Authority{}, .path = {"github.com:owner", "repo.git"}, })); } TEST(FixGitURLTestSuite, properlyRejectFileURLWithAuthority) { /* From the underlying `parseURL` validations. */ EXPECT_THAT( []() { fixGitURL("file://var/repos/x"); }, ::testing::ThrowsMessage( testing::HasSubstrIgnoreANSIMatcher("file:// URL 'file://var/repos/x' has unexpected authority 'var'"))); } TEST(FixGitURLTestSuite, scpLikePathLeadingSlashParsesPoorly) { // SCP-like URL (no user) // Cannot "to_string" this because has illegal path not starting // with `/`. EXPECT_EQ( fixGitURL("github.com:/owner/repo.git"), (ParsedURL{ .scheme = "file", .authority = ParsedURL::Authority{}, .path = {"github.com:", "owner", "repo.git"}, })); } TEST(FixGitURLTestSuite, relativePathParsesPoorly) { // Relative path (becomes file:// absolute) // Cannot "to_string" this because has illegal path not starting // with `/`. EXPECT_EQ( fixGitURL("relative/repo"), (ParsedURL{ .scheme = "file", .authority = ParsedURL::Authority{ .hostType = ParsedURL::Authority::HostType::Name, .host = "", }, .path = {"relative", "repo"}})); } struct ParseURLSuccessCase { std::string_view input; ParsedURL expected; }; class ParseURLSuccess : public ::testing::TestWithParam {}; INSTANTIATE_TEST_SUITE_P( ParseURLSuccessCases, ParseURLSuccess, ::testing::Values( ParseURLSuccessCase{ .input = "http://www.example.org/file.tar.gz", .expected = ParsedURL{ .scheme = "http", .authority = Authority{.hostType = HostType::Name, .host = "www.example.org"}, .path = {"", "file.tar.gz"}, .query = (StringMap) {}, .fragment = "", }, }, ParseURLSuccessCase{ .input = "https://www.example.org/file.tar.gz", .expected = ParsedURL{ .scheme = "https", .authority = Authority{.hostType = HostType::Name, .host = "www.example.org"}, .path = {"", "file.tar.gz"}, .query = (StringMap) {}, .fragment = "", }, }, ParseURLSuccessCase{ .input = "https://www.example.org/file.tar.gz?download=fast&when=now#hello", .expected = ParsedURL{ .scheme = "https", .authority = Authority{.hostType = HostType::Name, .host = "www.example.org"}, .path = {"", "file.tar.gz"}, .query = (StringMap) {{"download", "fast"}, {"when", "now"}}, .fragment = "hello", }, }, ParseURLSuccessCase{ .input = "file+https://www.example.org/video.mp4", .expected = ParsedURL{ .scheme = "file+https", .authority = Authority{.hostType = HostType::Name, .host = "www.example.org"}, .path = {"", "video.mp4"}, .query = (StringMap) {}, .fragment = "", }, }, ParseURLSuccessCase{ .input = "http://127.0.0.1:8080/file.tar.gz?download=fast&when=now#hello", .expected = ParsedURL{ .scheme = "http", .authority = Authority{.hostType = HostType::IPv4, .host = "127.0.0.1", .port = 8080}, .path = {"", "file.tar.gz"}, .query = (StringMap) {{"download", "fast"}, {"when", "now"}}, .fragment = "hello", }, }, ParseURLSuccessCase{ .input = "http://[fe80::818c:da4d:8975:415c\%25enp0s25]:8080", .expected = ParsedURL{ .scheme = "http", .authority = Authority{ .hostType = HostType::IPv6, .host = "fe80::818c:da4d:8975:415c\%enp0s25", .port = 8080}, .path = {""}, .query = (StringMap) {}, .fragment = "", }, }, ParseURLSuccessCase{ .input = "http://[2a02:8071:8192:c100:311d:192d:81ac:11ea]:8080", .expected = ParsedURL{ .scheme = "http", .authority = Authority{ .hostType = HostType::IPv6, .host = "2a02:8071:8192:c100:311d:192d:81ac:11ea", .port = 8080, }, .path = {""}, .query = (StringMap) {}, .fragment = "", }, })); TEST_P(ParseURLSuccess, parsesAsExpected) { auto & p = GetParam(); const auto parsed = parseURL(p.input); EXPECT_EQ(parsed, p.expected); } TEST_P(ParseURLSuccess, toStringRoundTrips) { auto & p = GetParam(); const auto parsed = parseURL(p.input); EXPECT_EQ(p.input, parsed.to_string()); } TEST_P(ParseURLSuccess, makeSureFixGitURLDoesNotModify) { auto & p = GetParam(); const auto parsed = fixGitURL(std::string{p.input}); EXPECT_EQ(p.input, parsed.to_string()); } TEST(parseURL, parsesSimpleHttpUrlWithComplexFragment) { auto s = "http://www.example.org/file.tar.gz?field=value#?foo=bar%23"; auto parsed = parseURL(s); ParsedURL expected{ .scheme = "http", .authority = Authority{.hostType = HostType::Name, .host = "www.example.org"}, .path = {"", "file.tar.gz"}, .query = (StringMap) {{"field", "value"}}, .fragment = "?foo=bar#", }; ASSERT_EQ(parsed, expected); } TEST(parseURL, rejectsAuthorityInUrlsWithFileTransportation) { EXPECT_THAT( []() { parseURL("file://www.example.org/video.mp4"); }, ::testing::ThrowsMessage( testing::HasSubstrIgnoreANSIMatcher("has unexpected authority 'www.example.org'"))); } TEST(parseURL, parseEmptyQueryParams) { auto s = "http://127.0.0.1:8080/file.tar.gz?&&&&&"; auto parsed = parseURL(s); ASSERT_EQ(parsed.query, (StringMap) {}); } TEST(parseURL, parseUserPassword) { auto s = "http://user:pass@www.example.org:8080/file.tar.gz"; auto parsed = parseURL(s); ParsedURL expected{ .scheme = "http", .authority = Authority{ .hostType = HostType::Name, .host = "www.example.org", .user = "user", .password = "pass", .port = 8080, }, .path = {"", "file.tar.gz"}, .query = (StringMap) {}, .fragment = "", }; ASSERT_EQ(parsed, expected); ASSERT_EQ(s, parsed.to_string()); } TEST(parseURL, parseFileURLWithQueryAndFragment) { auto s = "file:///none/of//your/business"; auto parsed = parseURL(s); ParsedURL expected{ .scheme = "file", .authority = Authority{}, .path = {"", "none", "of", "", "your", "business"}, .query = (StringMap) {}, .fragment = "", }; ASSERT_EQ(parsed.renderPath(), "/none/of//your/business"); ASSERT_EQ(parsed, expected); ASSERT_EQ(s, parsed.to_string()); } TEST(parseURL, parseFileURL) { auto s = "file:/none/of/your/business/"; auto parsed = parseURL(s); ParsedURL expected{ .scheme = "file", .authority = std::nullopt, .path = {"", "none", "of", "your", "business", ""}, }; ASSERT_EQ(parsed.renderPath(), "/none/of/your/business/"); ASSERT_EQ(parsed, expected); ASSERT_EQ(s, parsed.to_string()); } TEST(parseURL, parseFileURLWithAuthority) { auto s = "file://///of/your/business//"; auto parsed = parseURL(s); ParsedURL expected{ .scheme = "file", .authority = Authority{.host = ""}, .path = {"", "", "", "of", "your", "business", "", ""}, }; ASSERT_EQ(parsed.path, expected.path); ASSERT_EQ(parsed.renderPath(), "///of/your/business//"); ASSERT_EQ(parsed, expected); ASSERT_EQ(s, parsed.to_string()); } TEST(parseURL, parseFileURLNoLeadingSlash) { auto s = "file:none/of/your/business/"; auto parsed = parseURL(s); ParsedURL expected{ .scheme = "file", .authority = std::nullopt, .path = {"none", "of", "your", "business", ""}, }; ASSERT_EQ(parsed.renderPath(), "none/of/your/business/"); ASSERT_EQ(parsed, expected); ASSERT_EQ("file:none/of/your/business/", parsed.to_string()); } TEST(parseURL, parseHttpTrailingSlash) { auto s = "http://example.com/"; auto parsed = parseURL(s); ParsedURL expected{ .scheme = "http", .authority = Authority{.host = "example.com"}, .path = {"", ""}, }; ASSERT_EQ(parsed.renderPath(), "/"); ASSERT_EQ(parsed, expected); ASSERT_EQ(s, parsed.to_string()); } TEST(parseURL, parsedUrlsIsEqualToItself) { auto s = "http://www.example.org/file.tar.gz"; auto url = parseURL(s); ASSERT_TRUE(url == url); } TEST(parseURL, parsedUrlsWithUnescapedChars) { /* Test for back-compat. Behavior is rather questionable, but * is ingrained pretty deep into how URL parsing is shared between * flakes and libstore. * 1. Unescaped spaces, quotes and shevron (^) in fragment. * 2. Unescaped spaces and quotes in query. */ auto s = "http://www.example.org/file.tar.gz?query \"= 123\"#shevron^quote\"space "; /* Without leniency for back compat, this should throw. */ EXPECT_THROW(parseURL(s), Error); /* With leniency for back compat, this should parse. */ auto url = parseURL(s, /*lenient=*/true); EXPECT_EQ(url.fragment, "shevron^quote\"space "); auto query = StringMap{ {"query \"", " 123\""}, }; EXPECT_EQ(url.query, query); } TEST(parseURL, parseFTPUrl) { auto s = "ftp://ftp.nixos.org/downloads/nixos.iso"; auto parsed = parseURL(s); ParsedURL expected{ .scheme = "ftp", .authority = Authority{.hostType = HostType::Name, .host = "ftp.nixos.org"}, .path = {"", "downloads", "nixos.iso"}, .query = (StringMap) {}, .fragment = "", }; ASSERT_EQ(parsed, expected); ASSERT_EQ(s, parsed.to_string()); } TEST(parseURL, parsesAnythingInUriFormat) { auto s = "whatever://github.com/NixOS/nixpkgs.git"; auto parsed = parseURL(s); ASSERT_EQ(s, parsed.to_string()); } TEST(parseURL, parsesAnythingInUriFormatWithoutDoubleSlash) { auto s = "whatever:github.com/NixOS/nixpkgs.git"; auto parsed = parseURL(s); ASSERT_EQ(s, parsed.to_string()); } TEST(parseURL, emptyStringIsInvalidURL) { ASSERT_THROW(parseURL(""), Error); } TEST(parseURL, parsesHttpUrlWithEmptyPort) { auto s = "http://www.example.org:/file.tar.gz?foo=bar"; auto parsed = parseURL(s); ParsedURL expected{ .scheme = "http", .authority = Authority{.hostType = HostType::Name, .host = "www.example.org"}, .path = {"", "file.tar.gz"}, .query = (StringMap) {{"foo", "bar"}}, .fragment = "", }; ASSERT_EQ(parsed, expected); ASSERT_EQ("http://www.example.org/file.tar.gz?foo=bar", parsed.to_string()); } /* ---------------------------------------------------------------------------- * parseURLRelative * --------------------------------------------------------------------------*/ TEST(parseURLRelative, resolvesRelativePath) { ParsedURL base = parseURL("http://example.org/dir/page.html"); auto parsed = parseURLRelative("subdir/file.txt", base); ParsedURL expected{ .scheme = "http", .authority = ParsedURL::Authority{.hostType = HostType::Name, .host = "example.org"}, .path = {"", "dir", "subdir", "file.txt"}, .query = {}, .fragment = "", }; ASSERT_EQ(parsed, expected); } TEST(parseURLRelative, baseUrlIpv6AddressWithoutZoneId) { ParsedURL base = parseURL("http://[fe80::818c:da4d:8975:415c]/dir/page.html"); auto parsed = parseURLRelative("subdir/file.txt", base); ParsedURL expected{ .scheme = "http", .authority = ParsedURL::Authority{.hostType = HostType::IPv6, .host = "fe80::818c:da4d:8975:415c"}, .path = {"", "dir", "subdir", "file.txt"}, .query = {}, .fragment = "", }; ASSERT_EQ(parsed, expected); } TEST(parseURLRelative, resolvesRelativePathIpv6AddressWithZoneId) { ParsedURL base = parseURL("http://[fe80::818c:da4d:8975:415c\%25enp0s25]:8080/dir/page.html"); auto parsed = parseURLRelative("subdir/file2.txt", base); ParsedURL expected{ .scheme = "http", .authority = Authority{.hostType = HostType::IPv6, .host = "fe80::818c:da4d:8975:415c\%enp0s25", .port = 8080}, .path = {"", "dir", "subdir", "file2.txt"}, .query = {}, .fragment = "", }; ASSERT_EQ(parsed, expected); } TEST(parseURLRelative, resolvesRelativePathWithDot) { ParsedURL base = parseURL("http://example.org/dir/page.html"); auto parsed = parseURLRelative("./subdir/file.txt", base); ParsedURL expected{ .scheme = "http", .authority = ParsedURL::Authority{.hostType = HostType::Name, .host = "example.org"}, .path = {"", "dir", "subdir", "file.txt"}, .query = {}, .fragment = "", }; ASSERT_EQ(parsed, expected); } TEST(parseURLRelative, resolvesParentDirectory) { ParsedURL base = parseURL("http://example.org:234/dir/page.html"); auto parsed = parseURLRelative("../up.txt", base); ParsedURL expected{ .scheme = "http", .authority = ParsedURL::Authority{.hostType = HostType::Name, .host = "example.org", .port = 234}, .path = {"", "up.txt"}, .query = {}, .fragment = "", }; ASSERT_EQ(parsed, expected); } TEST(parseURLRelative, resolvesParentDirectoryNotTrickedByEscapedSlash) { ParsedURL base = parseURL("http://example.org:234/dir\%2Ffirst-trick/another-dir\%2Fsecond-trick/page.html"); auto parsed = parseURLRelative("../up.txt", base); ParsedURL expected{ .scheme = "http", .authority = ParsedURL::Authority{.hostType = HostType::Name, .host = "example.org", .port = 234}, .path = {"", "dir/first-trick", "up.txt"}, .query = {}, .fragment = "", }; ASSERT_EQ(parsed, expected); } TEST(parseURLRelative, replacesPathWithAbsoluteRelative) { ParsedURL base = parseURL("http://example.org/dir/page.html"); auto parsed = parseURLRelative("/rooted.txt", base); ParsedURL expected{ .scheme = "http", .authority = ParsedURL::Authority{.hostType = HostType::Name, .host = "example.org"}, .path = {"", "rooted.txt"}, .query = {}, .fragment = "", }; ASSERT_EQ(parsed, expected); } TEST(parseURLRelative, keepsQueryAndFragmentFromRelative) { // But discard query params on base URL ParsedURL base = parseURL("https://www.example.org/path/index.html?z=3"); auto parsed = parseURLRelative("other.html?x=1&y=2#frag", base); ParsedURL expected{ .scheme = "https", .authority = ParsedURL::Authority{.hostType = HostType::Name, .host = "www.example.org"}, .path = {"", "path", "other.html"}, .query = {{"x", "1"}, {"y", "2"}}, .fragment = "frag", }; ASSERT_EQ(parsed, expected); } TEST(parseURLRelative, absOverride) { ParsedURL base = parseURL("http://example.org/path/page.html"); std::string_view abs = "https://127.0.0.1.org/secure"; auto parsed = parseURLRelative(abs, base); auto parsedAbs = parseURL(abs); ASSERT_EQ(parsed, parsedAbs); } TEST(parseURLRelative, absOverrideWithZoneId) { ParsedURL base = parseURL("http://example.org/path/page.html"); std::string_view abs = "https://[fe80::818c:da4d:8975:415c\%25enp0s25]/secure?foo=bar"; auto parsed = parseURLRelative(abs, base); auto parsedAbs = parseURL(abs); ASSERT_EQ(parsed, parsedAbs); } TEST(parseURLRelative, bothWithoutAuthority) { ParsedURL base = parseURL("mailto:mail-base@bar.baz?bcc=alice@asdf.com"); std::string_view over = "mailto:mail-override@foo.bar?subject=url-testing"; auto parsed = parseURLRelative(over, base); auto parsedOverride = parseURL(over); ASSERT_EQ(parsed, parsedOverride); } TEST(parseURLRelative, emptyRelative) { ParsedURL base = parseURL("https://www.example.org/path/index.html?a\%20b=5\%206&x\%20y=34#frag"); auto parsed = parseURLRelative("", base); ParsedURL expected{ .scheme = "https", .authority = ParsedURL::Authority{.hostType = HostType::Name, .host = "www.example.org"}, .path = {"", "path", "index.html"}, .query = {{"a b", "5 6"}, {"x y", "34"}}, .fragment = "", }; EXPECT_EQ(base.fragment, "frag"); EXPECT_EQ(parsed, expected); } TEST(parseURLRelative, fragmentRelative) { ParsedURL base = parseURL("https://www.example.org/path/index.html?a\%20b=5\%206&x\%20y=34#frag"); auto parsed = parseURLRelative("#frag2", base); ParsedURL expected{ .scheme = "https", .authority = ParsedURL::Authority{.hostType = HostType::Name, .host = "www.example.org"}, .path = {"", "path", "index.html"}, .query = {{"a b", "5 6"}, {"x y", "34"}}, .fragment = "frag2", }; EXPECT_EQ(parsed, expected); } TEST(parseURLRelative, queryRelative) { ParsedURL base = parseURL("https://www.example.org/path/index.html?a\%20b=5\%206&x\%20y=34#frag"); auto parsed = parseURLRelative("?asdf\%20qwer=1\%202\%203", base); ParsedURL expected{ .scheme = "https", .authority = ParsedURL::Authority{.hostType = HostType::Name, .host = "www.example.org"}, .path = {"", "path", "index.html"}, .query = {{"asdf qwer", "1 2 3"}}, .fragment = "", }; EXPECT_EQ(parsed, expected); } TEST(parseURLRelative, queryFragmentRelative) { ParsedURL base = parseURL("https://www.example.org/path/index.html?a\%20b=5\%206&x\%20y=34#frag"); auto parsed = parseURLRelative("?asdf\%20qwer=1\%202\%203#frag2", base); ParsedURL expected{ .scheme = "https", .authority = ParsedURL::Authority{.hostType = HostType::Name, .host = "www.example.org"}, .path = {"", "path", "index.html"}, .query = {{"asdf qwer", "1 2 3"}}, .fragment = "frag2", }; EXPECT_EQ(parsed, expected); } /* ---------------------------------------------------------------------------- * decodeQuery * --------------------------------------------------------------------------*/ TEST(decodeQuery, emptyStringYieldsEmptyMap) { auto d = decodeQuery(""); ASSERT_EQ(d, (StringMap) {}); } TEST(decodeQuery, simpleDecode) { auto d = decodeQuery("yi=one&er=two"); ASSERT_EQ(d, ((StringMap) {{"yi", "one"}, {"er", "two"}})); } TEST(decodeQuery, decodeUrlEncodedArgs) { auto d = decodeQuery("arg=%3D%3D%40%3D%3D"); ASSERT_EQ(d, ((StringMap) {{"arg", "==@=="}})); } TEST(decodeQuery, decodeArgWithEmptyValue) { auto d = decodeQuery("arg="); ASSERT_EQ(d, ((StringMap) {{"arg", ""}})); } /* ---------------------------------------------------------------------------- * percentDecode * --------------------------------------------------------------------------*/ TEST(percentDecode, decodesUrlEncodedString) { std::string s = "==@=="; std::string d = percentDecode("%3D%3D%40%3D%3D"); ASSERT_EQ(d, s); } TEST(percentDecode, multipleDecodesAreIdempotent) { std::string once = percentDecode("%3D%3D%40%3D%3D"); std::string twice = percentDecode(once); ASSERT_EQ(once, twice); } TEST(percentDecode, trailingPercent) { std::string s = "==@==%"; std::string d = percentDecode("%3D%3D%40%3D%3D%25"); ASSERT_EQ(d, s); } TEST(percentDecode, incompleteEncoding) { ASSERT_THAT( []() { percentDecode("%1"); }, ::testing::ThrowsMessage( testing::HasSubstrIgnoreANSIMatcher("error: invalid URI parameter '%1': incomplete pct-encoding"))); } /* ---------------------------------------------------------------------------- * percentEncode * --------------------------------------------------------------------------*/ TEST(percentEncode, encodesUrlEncodedString) { std::string s = percentEncode("==@=="); std::string d = "%3D%3D%40%3D%3D"; ASSERT_EQ(d, s); } TEST(percentEncode, keepArgument) { std::string a = percentEncode("abd / def"); std::string b = percentEncode("abd / def", "/"); ASSERT_EQ(a, "abd%20%2F%20def"); ASSERT_EQ(b, "abd%20/%20def"); } TEST(percentEncode, inverseOfDecode) { std::string original = "%3D%3D%40%3D%3D"; std::string once = percentEncode(original); std::string back = percentDecode(once); ASSERT_EQ(back, original); } TEST(percentEncode, trailingPercent) { std::string s = percentEncode("==@==%"); std::string d = "%3D%3D%40%3D%3D%25"; ASSERT_EQ(d, s); } TEST(percentEncode, yen) { // https://en.wikipedia.org/wiki/Percent-encoding#Character_data std::string s = reinterpret_cast(u8"円"); std::string e = "%E5%86%86"; ASSERT_EQ(percentEncode(s), e); ASSERT_EQ(percentDecode(e), s); } TEST(parseURL, gitlabNamespacedProjectUrls) { // Test GitLab URL patterns with namespaced projects // These should preserve %2F encoding in the path auto s = "https://gitlab.example.com/api/v4/projects/group%2Fsubgroup%2Fproject/repository/archive.tar.gz"; auto parsed = parseURL(s); ParsedURL expected{ .scheme = "https", .authority = Authority{.hostType = HostType::Name, .host = "gitlab.example.com"}, .path = {"", "api", "v4", "projects", "group/subgroup/project", "repository", "archive.tar.gz"}, .query = {}, .fragment = "", }; ASSERT_EQ(parsed, expected); ASSERT_EQ(s, parsed.to_string()); } /* ---------------------------------------------------------------------------- * pathSegments * --------------------------------------------------------------------------*/ struct ParsedURLPathSegmentsTestCase { std::string url; std::vector segments; std::string path; bool skipEmpty; std::string description; }; class ParsedURLPathSegmentsTest : public ::testing::TestWithParam {}; TEST_P(ParsedURLPathSegmentsTest, segmentsAreCorrect) { const auto & testCase = GetParam(); auto segments = parseURL(testCase.url).pathSegments(/*skipEmpty=*/testCase.skipEmpty) | std::ranges::to(); EXPECT_EQ(segments, testCase.segments); EXPECT_EQ(encodeUrlPath(segments), testCase.path); } INSTANTIATE_TEST_SUITE_P( ParsedURL, ParsedURLPathSegmentsTest, ::testing::Values( ParsedURLPathSegmentsTestCase{ .url = "scheme:", .segments = {""}, .path = "", .skipEmpty = false, .description = "no_authority_empty_path", }, ParsedURLPathSegmentsTestCase{ .url = "scheme://", .segments = {""}, .path = "", .skipEmpty = false, .description = "empty_authority_empty_path", }, ParsedURLPathSegmentsTestCase{ .url = "scheme:///", .segments = {"", ""}, .path = "/", .skipEmpty = false, .description = "empty_authority_empty_path_trailing", }, ParsedURLPathSegmentsTestCase{ .url = "scheme://example.com/", .segments = {"", ""}, .path = "/", .skipEmpty = false, .description = "non_empty_authority_empty_path", }, ParsedURLPathSegmentsTestCase{ .url = "scheme://example.com//", .segments = {"", "", ""}, .path = "//", .skipEmpty = false, .description = "non_empty_authority_non_empty_path", }, ParsedURLPathSegmentsTestCase{ .url = "scheme://example.com///path///with//strange/empty///segments////", .segments = {"path", "with", "strange", "empty", "segments"}, .path = "path/with/strange/empty/segments", .skipEmpty = true, .description = "skip_all_empty_segments_with_authority", }, ParsedURLPathSegmentsTestCase{ .url = "scheme://example.com///lots///empty///", .segments = {"", "", "", "lots", "", "", "empty", "", "", ""}, .path = "///lots///empty///", .skipEmpty = false, .description = "empty_segments_with_authority", }, ParsedURLPathSegmentsTestCase{ .url = "scheme:/path///with//strange/empty///segments////", .segments = {"path", "with", "strange", "empty", "segments"}, .path = "path/with/strange/empty/segments", .skipEmpty = true, .description = "skip_all_empty_segments_no_authority_starts_with_slash", }, ParsedURLPathSegmentsTestCase{ .url = "scheme:path///with//strange/empty///segments////", .segments = {"path", "with", "strange", "empty", "segments"}, .path = "path/with/strange/empty/segments", .skipEmpty = true, .description = "skip_all_empty_segments_no_authority_doesnt_start_with_slash", }), [](const auto & info) { return info.param.description; }); TEST(nix, isValidSchemeName) { ASSERT_TRUE(isValidSchemeName("http")); ASSERT_TRUE(isValidSchemeName("https")); ASSERT_TRUE(isValidSchemeName("file")); ASSERT_TRUE(isValidSchemeName("file+https")); ASSERT_TRUE(isValidSchemeName("fi.le")); ASSERT_TRUE(isValidSchemeName("file-ssh")); ASSERT_TRUE(isValidSchemeName("file+")); ASSERT_TRUE(isValidSchemeName("file.")); ASSERT_TRUE(isValidSchemeName("file1")); ASSERT_FALSE(isValidSchemeName("file:")); ASSERT_FALSE(isValidSchemeName("file/")); ASSERT_FALSE(isValidSchemeName("+file")); ASSERT_FALSE(isValidSchemeName(".file")); ASSERT_FALSE(isValidSchemeName("-file")); ASSERT_FALSE(isValidSchemeName("1file")); // regex ok? ASSERT_FALSE(isValidSchemeName("\nhttp")); ASSERT_FALSE(isValidSchemeName("\nhttp\n")); ASSERT_FALSE(isValidSchemeName("http\n")); ASSERT_FALSE(isValidSchemeName("http ")); } } // namespace nix