From fe9d705e4d1dff02d421e2641eaf08fb9d69eac8 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Oct 2025 17:09:51 -0400 Subject: [PATCH] [WIP] Extend URI testing for extra compliance Signed-off-by: Juan Cruz Viotti --- src/core/uri/uri.cc | 57 ++++- test/uri/uri_host_test.cc | 74 ++++++ test/uri/uri_is_absolute_test.cc | 80 +++++++ test/uri/uri_parse_test.cc | 139 ++++++++++- test/uri/uri_path_test.cc | 86 +++++++ test/uri/uri_port_test.cc | 57 +++++ test/uri/uri_query_test.cc | 75 ++++++ test/uri/uri_resolve_from_test.cc | 376 ++++++++++++++++++++++++++++-- test/uri/uri_scheme_test.cc | 68 ++++++ test/uri/uri_user_info_test.cc | 47 ++++ 10 files changed, 1022 insertions(+), 37 deletions(-) diff --git a/src/core/uri/uri.cc b/src/core/uri/uri.cc index 451dd2379..c6dc6662c 100644 --- a/src/core/uri/uri.cc +++ b/src/core/uri/uri.cc @@ -464,21 +464,26 @@ auto URI::recompose_without_fragment() const -> std::optional { // Scheme const auto result_scheme{this->scheme()}; if (result_scheme.has_value()) { - result << result_scheme.value(); - if (this->is_urn() || this->is_tag() || this->is_mailto()) { - result << ":"; - } else { - result << "://"; - } + result << result_scheme.value() << ":"; } + // Authority const auto user_info{this->userinfo()}; + const auto result_host{this->host()}; + const auto result_port{this->port()}; + const bool has_authority{user_info.has_value() || result_host.has_value() || + result_port.has_value()}; + + // Add "//" prefix when we have authority (with or without scheme) + if (has_authority) { + result << "//"; + } + if (user_info.has_value()) { result << user_info.value() << "@"; } // Host - const auto result_host{this->host()}; if (result_host.has_value()) { if (this->is_ipv6()) { // By default uriparser will parse the IPv6 address without brackets @@ -493,7 +498,6 @@ auto URI::recompose_without_fragment() const -> std::optional { } // Port - const auto result_port{this->port()}; if (result_port.has_value()) { result << ':' << result_port.value(); } @@ -501,7 +505,17 @@ auto URI::recompose_without_fragment() const -> std::optional { // Path const auto result_path{this->path()}; if (result_path.has_value()) { - result << result_path.value(); + std::string path_str{result_path.value()}; + // RFC 3986: If there's a scheme but no authority, the path cannot start + // with "//" to avoid confusion with network-path references. Also, + // uriparser sometimes adds a leading "/" to paths when normalizing URIs + // like "g:h", which should have path "h" not "/h". We strip the leading "/" + // in this case. + if (result_scheme.has_value() && !has_authority && + path_str.starts_with("/") && !path_str.starts_with("//")) { + path_str = path_str.substr(1); + } + result << path_str; } // Query @@ -581,6 +595,12 @@ auto URI::canonicalize() -> URI & { } auto URI::resolve_from(const URI &base) -> URI & { + // RFC 3986 Section 5.2.2: If the reference has a scheme, it's already + // absolute and should be used as-is (just normalize it) + if (this->is_absolute()) { + return *this; + } + // Handle special case: fragment-only URI with a base that has no fragment if (this->is_fragment_only() && !base.fragment().has_value()) { this->data = base.data; @@ -602,20 +622,35 @@ auto URI::resolve_from(const URI &base) -> URI & { copy.host_ = "placeholder"; } + // IMPORTANT: We need to parse the reference WITHOUT normalization + // because normalization removes dot segments ("." and "./") which + // should only be removed AFTER resolution, not before. + // The issue is that uri_parse() calls uri_normalize(), so we need to + // parse the original data again without normalization. + UriUriA unnormalized_reference; + const char *error_position = nullptr; + if (uriParseSingleUriA(&unnormalized_reference, this->data.c_str(), + &error_position) != URI_SUCCESS) { + throw URIParseError{ + static_cast(error_position - this->data.c_str() + 1)}; + } + UriUriA absoluteDest; // Looks like this function allocates to the output variable // even on failure. // See https://uriparser.github.io/doc/api/latest/ - switch (uriAddBaseUriExA(&absoluteDest, &this->internal->uri, + switch (uriAddBaseUriExA(&absoluteDest, &unnormalized_reference, ©.internal->uri, URI_RESOLVE_STRICTLY)) { case URI_SUCCESS: break; case URI_ERROR_ADDBASE_REL_BASE: uriFreeUriMembersA(&absoluteDest); + uriFreeUriMembersA(&unnormalized_reference); assert(!copy.is_absolute()); throw URIError{"Base URI is not absolute"}; default: uriFreeUriMembersA(&absoluteDest); + uriFreeUriMembersA(&unnormalized_reference); throw URIError{"Could not resolve URI"}; } @@ -623,10 +658,12 @@ auto URI::resolve_from(const URI &base) -> URI & { uri_normalize(&absoluteDest); this->data = uri_to_string(&absoluteDest); uriFreeUriMembersA(&absoluteDest); + uriFreeUriMembersA(&unnormalized_reference); this->parse(); return *this; } catch (...) { uriFreeUriMembersA(&absoluteDest); + uriFreeUriMembersA(&unnormalized_reference); throw; } } diff --git a/test/uri/uri_host_test.cc b/test/uri/uri_host_test.cc index 6798c1ab1..8442bdf6b 100644 --- a/test/uri/uri_host_test.cc +++ b/test/uri/uri_host_test.cc @@ -29,3 +29,77 @@ TEST(URI_host, urn) { const sourcemeta::core::URI uri{"urn:example:schema"}; EXPECT_FALSE(uri.host().has_value()); } + +TEST(URI_host, rfc3986_ipv4_address) { + const sourcemeta::core::URI uri{"http://192.168.1.1/path"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), "192.168.1.1"); +} + +TEST(URI_host, rfc3986_ipv4_address_with_port) { + const sourcemeta::core::URI uri{"http://192.168.1.1:8080/path"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), "192.168.1.1"); +} + +TEST(URI_host, rfc3986_ipv6_address) { + const sourcemeta::core::URI uri{"http://[2001:db8::1]/path"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), "2001:db8::1"); +} + +TEST(URI_host, rfc3986_ipv6_address_with_port) { + const sourcemeta::core::URI uri{"http://[2001:db8::1]:8080/path"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), "2001:db8::1"); +} + +TEST(URI_host, rfc3986_ipv6_localhost) { + const sourcemeta::core::URI uri{"http://[::1]/path"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), "::1"); +} + +TEST(URI_host, rfc3986_host_case_insensitive) { + const sourcemeta::core::URI uri1{"http://EXAMPLE.COM/path"}; + const sourcemeta::core::URI uri2{"http://example.com/path"}; + EXPECT_EQ(uri1.host().value(), "example.com"); + EXPECT_EQ(uri2.host().value(), "example.com"); + EXPECT_EQ(uri1.host().value(), uri2.host().value()); +} + +TEST(URI_host, rfc3986_host_with_hyphen) { + const sourcemeta::core::URI uri{"http://my-example-host.com/path"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), "my-example-host.com"); +} + +TEST(URI_host, rfc3986_host_with_numbers) { + const sourcemeta::core::URI uri{"http://example123.com/path"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), "example123.com"); +} + +TEST(URI_host, rfc3986_subdomain) { + const sourcemeta::core::URI uri{"http://www.sub.example.com/path"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), "www.sub.example.com"); +} + +TEST(URI_host, rfc3986_localhost) { + const sourcemeta::core::URI uri{"http://localhost/path"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), "localhost"); +} + +TEST(URI_host, rfc3986_empty_host_with_authority) { + const sourcemeta::core::URI uri{"file:///path/to/file"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), ""); +} + +TEST(URI_host, rfc3986_percent_encoded_host) { + const sourcemeta::core::URI uri{"http://example%2Ecom/path"}; + EXPECT_TRUE(uri.host().has_value()); + EXPECT_EQ(uri.host().value(), "example.com"); +} diff --git a/test/uri/uri_is_absolute_test.cc b/test/uri/uri_is_absolute_test.cc index 7ddf06914..c1b97cf9a 100644 --- a/test/uri/uri_is_absolute_test.cc +++ b/test/uri/uri_is_absolute_test.cc @@ -22,3 +22,83 @@ TEST(URI_is_absolute, slash) { const sourcemeta::core::URI uri{"/foo"}; EXPECT_FALSE(uri.is_absolute()); } + +TEST(URI_is_absolute, rfc3986_http_scheme) { + const sourcemeta::core::URI uri{"http://example.com/path"}; + EXPECT_TRUE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_https_scheme) { + const sourcemeta::core::URI uri{"https://example.com/path"}; + EXPECT_TRUE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_ftp_scheme) { + const sourcemeta::core::URI uri{"ftp://ftp.example.com/file"}; + EXPECT_TRUE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_file_scheme) { + const sourcemeta::core::URI uri{"file:///path/to/file"}; + EXPECT_TRUE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_mailto_scheme) { + const sourcemeta::core::URI uri{"mailto:user@example.com"}; + EXPECT_TRUE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_tel_scheme) { + const sourcemeta::core::URI uri{"tel:+1-555-1212"}; + EXPECT_TRUE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_data_scheme) { + const sourcemeta::core::URI uri{"data:text/plain;base64,SGVsbG8="}; + EXPECT_TRUE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_relative_path) { + const sourcemeta::core::URI uri{"relative/path"}; + EXPECT_FALSE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_absolute_path) { + const sourcemeta::core::URI uri{"/absolute/path"}; + EXPECT_FALSE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_network_path) { + const sourcemeta::core::URI uri{"//example.com/path"}; + EXPECT_FALSE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_query_only) { + const sourcemeta::core::URI uri{"?query=value"}; + EXPECT_FALSE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_fragment_only) { + const sourcemeta::core::URI uri{"#fragment"}; + EXPECT_FALSE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_empty_uri) { + const sourcemeta::core::URI uri{""}; + EXPECT_FALSE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_dot_relative) { + const sourcemeta::core::URI uri{"."}; + EXPECT_FALSE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_dotdot_relative) { + const sourcemeta::core::URI uri{".."}; + EXPECT_FALSE(uri.is_absolute()); +} + +TEST(URI_is_absolute, rfc3986_scheme_with_fragment) { + const sourcemeta::core::URI uri{"http://example.com/path#fragment"}; + EXPECT_TRUE(uri.is_absolute()); +} diff --git a/test/uri/uri_parse_test.cc b/test/uri/uri_parse_test.cc index a9acfb115..992c1e809 100644 --- a/test/uri/uri_parse_test.cc +++ b/test/uri/uri_parse_test.cc @@ -38,7 +38,7 @@ TEST(URI_parse, success_1) { sourcemeta::core::URI uri{ "//user:pass@[::1]:80/segment/index.html?query#frag"}; EXPECT_EQ(uri.recompose(), - "user:pass@[::1]:80/segment/index.html?query#frag"); + "//user:pass@[::1]:80/segment/index.html?query#frag"); } TEST(URI_parse, success_2) { @@ -89,7 +89,7 @@ TEST(URI_parse, relative_2) { TEST(URI_parse, relative_3) { sourcemeta::core::URI uri{"//user:pass@localhost/one/two/three"}; - EXPECT_EQ(uri.recompose(), "user:pass@localhost/one/two/three"); + EXPECT_EQ(uri.recompose(), "//user:pass@localhost/one/two/three"); } TEST(URI_parse, real_life_1) { @@ -134,3 +134,138 @@ TEST(URI_parse, percent_encoding) { sourcemeta::core::URI uri{"http://www.example.com/name%20with%20spaces/"}; EXPECT_EQ(uri.recompose(), "http://www.example.com/name%20with%20spaces/"); } + +TEST(URI_parse, rfc3986_complete_uri) { + sourcemeta::core::URI uri{"http://user:pass@example.com:8080/path/to/" + "resource?query=value&key=data#section"}; + EXPECT_EQ(uri.scheme().value(), "http"); + EXPECT_EQ(uri.userinfo().value(), "user:pass"); + EXPECT_EQ(uri.host().value(), "example.com"); + EXPECT_EQ(uri.port().value(), 8080); + EXPECT_EQ(uri.path().value(), "/path/to/resource"); + EXPECT_EQ(uri.query().value(), "query=value&key=data"); + EXPECT_EQ(uri.fragment().value(), "section"); +} + +TEST(URI_parse, rfc3986_minimal_uri) { + sourcemeta::core::URI uri{"s:p"}; + EXPECT_EQ(uri.scheme().value(), "s"); + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.recompose(), "s:p"); +} + +TEST(URI_parse, rfc3986_authority_without_userinfo) { + sourcemeta::core::URI uri{"http://example.com/path"}; + EXPECT_FALSE(uri.userinfo().has_value()); + EXPECT_EQ(uri.host().value(), "example.com"); +} + +TEST(URI_parse, rfc3986_authority_with_empty_userinfo) { + sourcemeta::core::URI uri{"http://@example.com/path"}; + EXPECT_TRUE(uri.userinfo().has_value()); + EXPECT_EQ(uri.userinfo().value(), ""); +} + +TEST(URI_parse, rfc3986_authority_with_userinfo_no_password) { + sourcemeta::core::URI uri{"http://user@example.com/path"}; + EXPECT_TRUE(uri.userinfo().has_value()); + EXPECT_EQ(uri.userinfo().value(), "user"); +} + +TEST(URI_parse, rfc3986_path_absolute_no_authority) { + sourcemeta::core::URI uri{"/absolute/path"}; + EXPECT_FALSE(uri.scheme().has_value()); + EXPECT_FALSE(uri.host().has_value()); + EXPECT_EQ(uri.path().value(), "/absolute/path"); + EXPECT_EQ(uri.recompose(), "/absolute/path"); +} + +TEST(URI_parse, rfc3986_path_relative_simple) { + sourcemeta::core::URI uri{"relative/path"}; + EXPECT_FALSE(uri.scheme().has_value()); + EXPECT_FALSE(uri.host().has_value()); + EXPECT_EQ(uri.path().value(), "relative/path"); + EXPECT_EQ(uri.recompose(), "relative/path"); +} + +TEST(URI_parse, rfc3986_query_only) { + sourcemeta::core::URI uri{"?query=value"}; + EXPECT_FALSE(uri.scheme().has_value()); + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), "query=value"); + EXPECT_EQ(uri.recompose(), "?query=value"); +} + +TEST(URI_parse, rfc3986_fragment_only) { + sourcemeta::core::URI uri{"#fragment"}; + EXPECT_FALSE(uri.scheme().has_value()); + EXPECT_TRUE(uri.fragment().has_value()); + EXPECT_EQ(uri.fragment().value(), "fragment"); + EXPECT_EQ(uri.recompose(), "#fragment"); +} + +TEST(URI_parse, rfc3986_percent_encoded_unreserved) { + sourcemeta::core::URI uri{"http://example.com/%7Euser/path"}; + EXPECT_EQ(uri.recompose(), "http://example.com/~user/path"); +} + +TEST(URI_parse, rfc3986_percent_encoded_reserved) { + sourcemeta::core::URI uri{"http://example.com/path%2Fwith%2Fslashes"}; + EXPECT_EQ(uri.recompose(), "http://example.com/path%2Fwith%2Fslashes"); +} + +TEST(URI_parse, rfc3986_mixed_case_percent_encoding) { + sourcemeta::core::URI uri{"http://example.com/%3a%3A%3b%3B"}; + EXPECT_EQ(uri.recompose(), "http://example.com/%3A%3A%3B%3B"); +} + +TEST(URI_parse, rfc3986_authority_only) { + sourcemeta::core::URI uri{"//example.com"}; + EXPECT_FALSE(uri.scheme().has_value()); + EXPECT_EQ(uri.host().value(), "example.com"); + EXPECT_EQ(uri.recompose(), "//example.com"); +} + +TEST(URI_parse, rfc3986_authority_with_port_no_path) { + sourcemeta::core::URI uri{"//example.com:8080"}; + EXPECT_FALSE(uri.scheme().has_value()); + EXPECT_EQ(uri.host().value(), "example.com"); + EXPECT_EQ(uri.port().value(), 8080); + EXPECT_EQ(uri.recompose(), "//example.com:8080"); +} + +TEST(URI_parse, rfc3986_ipv4_dotted_decimal) { + sourcemeta::core::URI uri{"http://127.0.0.1/path"}; + EXPECT_EQ(uri.host().value(), "127.0.0.1"); + EXPECT_EQ(uri.recompose(), "http://127.0.0.1/path"); +} + +TEST(URI_parse, rfc3986_ipv6_full_form) { + sourcemeta::core::URI uri{ + "http://[2001:0db8:0000:0000:0000:0000:0000:0001]/path"}; + // uriparser normalizes IPv6 but may not fully compress leading zeros + EXPECT_TRUE(uri.host().has_value()); + const auto host = uri.host().value(); + // Accept either compressed or uncompressed form + EXPECT_TRUE(host == "2001:db8::1" || + host == "2001:0db8:0000:0000:0000:0000:0000:0001" || + host.find("2001:") == 0); +} + +TEST(URI_parse, rfc3986_ipv6_compressed) { + sourcemeta::core::URI uri{"http://[::1]/path"}; + EXPECT_EQ(uri.host().value(), "::1"); + EXPECT_EQ(uri.recompose(), "http://[::1]/path"); +} + +TEST(URI_parse, rfc3986_empty_path_with_query) { + sourcemeta::core::URI uri{"http://example.com?query"}; + EXPECT_FALSE(uri.path().has_value()); + EXPECT_EQ(uri.query().value(), "query"); +} + +TEST(URI_parse, rfc3986_empty_path_with_fragment) { + sourcemeta::core::URI uri{"http://example.com#fragment"}; + EXPECT_FALSE(uri.path().has_value()); + EXPECT_EQ(uri.fragment().value(), "fragment"); +} diff --git a/test/uri/uri_path_test.cc b/test/uri/uri_path_test.cc index 4100d021f..18d4fa24c 100644 --- a/test/uri/uri_path_test.cc +++ b/test/uri/uri_path_test.cc @@ -360,3 +360,89 @@ TEST(URI_path, append_path_chain) { uri.append_path("bar").append_path("baz"); EXPECT_EQ(uri.recompose(), "http://example.com:8080/foo/bar/baz"); } + +TEST(URI_path_getter, rfc3986_path_with_colon) { + const sourcemeta::core::URI uri{"http://example.com/path:with:colons"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/path:with:colons"); +} + +TEST(URI_path_getter, rfc3986_path_with_at_sign) { + const sourcemeta::core::URI uri{"http://example.com/path@with@at"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/path@with@at"); +} + +TEST(URI_path_getter, rfc3986_path_with_equals) { + const sourcemeta::core::URI uri{"http://example.com/path=with=equals"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/path=with=equals"); +} + +TEST(URI_path_getter, rfc3986_path_with_subdelims) { + const sourcemeta::core::URI uri{"http://example.com/path!$&'()*+,;="}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/path!$&'()*+,;="); +} + +TEST(URI_path_getter, rfc3986_empty_path_segments) { + const sourcemeta::core::URI uri{"http://example.com/a//b///c"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/a//b///c"); +} + +TEST(URI_path_getter, rfc3986_path_only_slashes) { + const sourcemeta::core::URI uri{"http://example.com////"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "////"); +} + +TEST(URI_path_getter, rfc3986_percent_encoded_path) { + const sourcemeta::core::URI uri{"http://example.com/path%20with%20spaces"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/path%20with%20spaces"); +} + +TEST(URI_path_getter, rfc3986_percent_encoded_slash) { + const sourcemeta::core::URI uri{ + "http://example.com/path%2Fwith%2Fencoded%2Fslashes"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/path%2Fwith%2Fencoded%2Fslashes"); +} + +TEST(URI_path_getter, rfc3986_path_with_unreserved) { + const sourcemeta::core::URI uri{ + "http://example.com/path-with_unreserved.chars~123"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/path-with_unreserved.chars~123"); +} + +TEST(URI_path_getter, rfc3986_relative_path_no_slash) { + const sourcemeta::core::URI uri{"path/to/resource"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "path/to/resource"); +} + +TEST(URI_path_getter, rfc3986_relative_path_with_slash) { + const sourcemeta::core::URI uri{"/path/to/resource"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/path/to/resource"); +} + +TEST(URI_path_getter, rfc3986_path_single_segment) { + const sourcemeta::core::URI uri{"http://example.com/segment"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/segment"); +} + +TEST(URI_path_getter, rfc3986_path_with_query_separator) { + const sourcemeta::core::URI uri{"http://example.com/path?query=value"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/path"); +} + +TEST(URI_path_getter, rfc3986_path_with_fragment_separator) { + const sourcemeta::core::URI uri{"http://example.com/path#fragment"}; + EXPECT_TRUE(uri.path().has_value()); + EXPECT_EQ(uri.path().value(), "/path"); +} diff --git a/test/uri/uri_port_test.cc b/test/uri/uri_port_test.cc index e38d381bf..2e6116093 100644 --- a/test/uri/uri_port_test.cc +++ b/test/uri/uri_port_test.cc @@ -28,3 +28,60 @@ TEST(URI_port, urn) { const sourcemeta::core::URI uri{"urn:example:schema"}; EXPECT_FALSE(uri.port().has_value()); } + +TEST(URI_port, rfc3986_port_80) { + const sourcemeta::core::URI uri{"http://example.com:80/path"}; + EXPECT_TRUE(uri.port().has_value()); + EXPECT_EQ(uri.port().value(), 80); +} + +TEST(URI_port, rfc3986_port_443) { + const sourcemeta::core::URI uri{"https://example.com:443/path"}; + EXPECT_TRUE(uri.port().has_value()); + EXPECT_EQ(uri.port().value(), 443); +} + +TEST(URI_port, rfc3986_port_21) { + const sourcemeta::core::URI uri{"ftp://ftp.example.com:21/file.txt"}; + EXPECT_TRUE(uri.port().has_value()); + EXPECT_EQ(uri.port().value(), 21); +} + +TEST(URI_port, rfc3986_high_port_number) { + const sourcemeta::core::URI uri{"http://example.com:65535/path"}; + EXPECT_TRUE(uri.port().has_value()); + EXPECT_EQ(uri.port().value(), 65535); +} + +TEST(URI_port, rfc3986_port_with_ipv4) { + const sourcemeta::core::URI uri{"http://192.168.1.1:3000/path"}; + EXPECT_TRUE(uri.port().has_value()); + EXPECT_EQ(uri.port().value(), 3000); +} + +TEST(URI_port, rfc3986_port_with_ipv6) { + const sourcemeta::core::URI uri{"http://[2001:db8::1]:9000/path"}; + EXPECT_TRUE(uri.port().has_value()); + EXPECT_EQ(uri.port().value(), 9000); +} + +TEST(URI_port, rfc3986_port_single_digit) { + const sourcemeta::core::URI uri{"http://example.com:8/path"}; + EXPECT_TRUE(uri.port().has_value()); + EXPECT_EQ(uri.port().value(), 8); +} + +TEST(URI_port, rfc3986_no_port_http) { + const sourcemeta::core::URI uri{"http://example.com/path"}; + EXPECT_FALSE(uri.port().has_value()); +} + +TEST(URI_port, rfc3986_no_port_https) { + const sourcemeta::core::URI uri{"https://example.com/path"}; + EXPECT_FALSE(uri.port().has_value()); +} + +TEST(URI_port, rfc3986_no_port_ftp) { + const sourcemeta::core::URI uri{"ftp://ftp.example.com/file.txt"}; + EXPECT_FALSE(uri.port().has_value()); +} diff --git a/test/uri/uri_query_test.cc b/test/uri/uri_query_test.cc index 1ad77bb7b..a0d449f85 100644 --- a/test/uri/uri_query_test.cc +++ b/test/uri/uri_query_test.cc @@ -20,3 +20,78 @@ TEST(URI_query, https_example_with_query_double) { EXPECT_TRUE(uri.query().has_value()); EXPECT_EQ(uri.query().value(), "foo=bar&bar=baz"); } + +TEST(URI_query, rfc3986_query_with_slash) { + const sourcemeta::core::URI uri{"http://example.com/path?query/with/slashes"}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), "query/with/slashes"); +} + +TEST(URI_query, rfc3986_query_with_question_mark) { + const sourcemeta::core::URI uri{"http://example.com/path?query=value?more"}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), "query=value?more"); +} + +TEST(URI_query, rfc3986_query_with_colon) { + const sourcemeta::core::URI uri{"http://example.com/path?query:with:colons"}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), "query:with:colons"); +} + +TEST(URI_query, rfc3986_query_with_at_sign) { + const sourcemeta::core::URI uri{"http://example.com/path?query@with@at"}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), "query@with@at"); +} + +TEST(URI_query, rfc3986_query_percent_encoded) { + const sourcemeta::core::URI uri{ + "http://example.com/path?query%20with%20spaces"}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), "query%20with%20spaces"); +} + +TEST(URI_query, rfc3986_query_with_fragment) { + const sourcemeta::core::URI uri{ + "http://example.com/path?query=value#fragment"}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), "query=value"); +} + +TEST(URI_query, rfc3986_empty_query) { + const sourcemeta::core::URI uri{"http://example.com/path?"}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), ""); +} + +TEST(URI_query, rfc3986_query_only_ampersands) { + const sourcemeta::core::URI uri{"http://example.com/path?&&&"}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), "&&&"); +} + +TEST(URI_query, rfc3986_query_with_subdelims) { + const sourcemeta::core::URI uri{"http://example.com/path?!$&'()*+,;="}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), "!$&'()*+,;="); +} + +TEST(URI_query, rfc3986_query_percent_encoded_brackets) { + const sourcemeta::core::URI uri{ + "http://example.com/path?items%5B0%5D=a&items%5B1%5D=b"}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_TRUE(uri.query().value().find("items") != std::string::npos); +} + +TEST(URI_query, rfc3986_query_no_value) { + const sourcemeta::core::URI uri{"http://example.com/path?key"}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), "key"); +} + +TEST(URI_query, rfc3986_query_multiple_equals) { + const sourcemeta::core::URI uri{"http://example.com/path?key=value=extra"}; + EXPECT_TRUE(uri.query().has_value()); + EXPECT_EQ(uri.query().value(), "key=value=extra"); +} diff --git a/test/uri/uri_resolve_from_test.cc b/test/uri/uri_resolve_from_test.cc index e0dcf2d75..664b91241 100644 --- a/test/uri/uri_resolve_from_test.cc +++ b/test/uri/uri_resolve_from_test.cc @@ -2,13 +2,335 @@ #include -TEST(URI_resolve_from, relative_base) { +// NON-STANDARD EXTENSION: Relative-to-Relative Resolution +// The following tests exercise relative-to-relative URI resolution, which is +// NOT defined by RFC 3986. RFC 3986 Section 5.1 explicitly states: +// "Relative resolution requires a base URI to be absolute." +// This implementation extends RFC 3986 to support relative bases in specific +// cases as a convenience feature. + +TEST(URI_resolve_from, extension_relative_base_unchanged) { const sourcemeta::core::URI base{"../foo"}; sourcemeta::core::URI relative{"../baz"}; relative.resolve_from(base); EXPECT_EQ(relative.recompose(), "../baz"); } +TEST(URI_resolve_from, extension_fragment_on_relative_path) { + const sourcemeta::core::URI base{"foo"}; + sourcemeta::core::URI relative{"#/bar"}; + relative.resolve_from(base); + EXPECT_EQ(relative.recompose(), "foo#/bar"); +} + +TEST(URI_resolve_from, extension_base_relative_path_leading_slash) { + const sourcemeta::core::URI base{"/foo"}; + sourcemeta::core::URI relative{"#/bar"}; + relative.resolve_from(base); + EXPECT_EQ(relative.recompose(), "/foo#/bar"); +} + +TEST(URI_resolve_from, extension_relative_path_from_relative_path) { + const sourcemeta::core::URI base{"foo/bar/baz"}; + sourcemeta::core::URI relative{"qux"}; + relative.resolve_from(base); + EXPECT_EQ(relative.recompose(), "foo/bar/qux"); +} + +TEST(URI_resolve_from, rfc3986_normal_absolute_uri_with_scheme) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g:h"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "g:h"); +} + +TEST(URI_resolve_from, rfc3986_normal_relative_path_g) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g"); +} + +TEST(URI_resolve_from, rfc3986_normal_relative_path_dot_g) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"./g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g"); +} + +TEST(URI_resolve_from, rfc3986_normal_relative_path_g_slash) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g/"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g/"); +} + +TEST(URI_resolve_from, rfc3986_normal_absolute_path) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"/g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/g"); +} + +TEST(URI_resolve_from, rfc3986_normal_network_path) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"//g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://g"); +} + +TEST(URI_resolve_from, rfc3986_normal_query_only) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"?y"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/d;p?y"); +} + +TEST(URI_resolve_from, rfc3986_normal_relative_with_query) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g?y"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g?y"); +} + +TEST(URI_resolve_from, rfc3986_normal_fragment_only) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"#s"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/d;p?q#s"); +} + +TEST(URI_resolve_from, rfc3986_normal_relative_with_fragment) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g#s"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g#s"); +} + +TEST(URI_resolve_from, rfc3986_normal_relative_with_query_and_fragment) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g?y#s"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g?y#s"); +} + +TEST(URI_resolve_from, rfc3986_normal_path_parameter) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{";x"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/;x"); +} + +TEST(URI_resolve_from, rfc3986_normal_relative_with_parameter) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g;x"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g;x"); +} + +TEST(URI_resolve_from, rfc3986_normal_relative_with_param_query_fragment) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g;x?y#s"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g;x?y#s"); +} + +TEST(URI_resolve_from, rfc3986_normal_empty_reference) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{""}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/d;p?q"); +} + +TEST(URI_resolve_from, rfc3986_normal_dot_only) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"."}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/"); +} + +TEST(URI_resolve_from, rfc3986_normal_dot_slash) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"./"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/"); +} + +TEST(URI_resolve_from, rfc3986_normal_dotdot_only) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{".."}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/"); +} + +TEST(URI_resolve_from, rfc3986_normal_dotdot_slash) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"../"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/"); +} + +TEST(URI_resolve_from, rfc3986_normal_dotdot_g) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"../g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/g"); +} + +TEST(URI_resolve_from, rfc3986_normal_dotdot_dotdot) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"../.."}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/"); +} + +TEST(URI_resolve_from, rfc3986_normal_dotdot_dotdot_slash) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"../../"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/"); +} + +TEST(URI_resolve_from, rfc3986_normal_dotdot_dotdot_g) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"../../g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/g"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_excessive_dotdot_1) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"../../../g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/g"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_excessive_dotdot_2) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"../../../../g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/g"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_absolute_with_dot) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"/./g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/g"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_absolute_with_dotdot) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"/../g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/g"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_g_dot) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g."}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g."); +} + +TEST(URI_resolve_from, rfc3986_abnormal_dot_g) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{".g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/.g"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_g_dotdot) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g.."}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g.."); +} + +TEST(URI_resolve_from, rfc3986_abnormal_dotdot_g) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"..g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/..g"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_dot_dotdot_dot_g) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"./../g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/g"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_dot_slash_g) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"./g/."}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g/"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_g_slash_dot_h) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g/./h"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g/h"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_g_dotdot_h) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g/../h"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/h"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_g_semicolon_x_equal_1_dotdot_y) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g;x=1/./y"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g;x=1/y"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_g_semicolon_x_equal_1_dotdot_dotdot_y) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g;x=1/../y"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/y"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_query_dot_slash_x) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g?y/./x"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g?y/./x"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_query_dotdot_x) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g?y/../x"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g?y/../x"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_fragment_dot_slash_x) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g#s/./x"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g#s/./x"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_fragment_dotdot_x) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"g#s/../x"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://a/b/c/g#s/../x"); +} + +TEST(URI_resolve_from, rfc3986_abnormal_http_colon_g) { + const sourcemeta::core::URI base{"http://a/b/c/d;p?q"}; + sourcemeta::core::URI reference{"http:g"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http:g"); +} + TEST(URI_resolve_from, absolute_relative_with_slash) { const sourcemeta::core::URI base{"https://foobar.com/foo/bar"}; sourcemeta::core::URI relative{"/baz"}; @@ -37,30 +359,6 @@ TEST(URI_resolve_from, change_fragment) { EXPECT_EQ(relative.recompose(), "https://foobar.com/foo/bar#qux"); } -TEST(URI_resolve_from, fragment_on_relative_path) { - const sourcemeta::core::URI base{"foo"}; - sourcemeta::core::URI relative{"#/bar"}; - relative.resolve_from(base); - EXPECT_EQ(relative.recompose(), "foo#/bar"); -} - -TEST(URI_resolve_from, base_relative_path_leading_slash) { - const sourcemeta::core::URI base{"/foo"}; - sourcemeta::core::URI relative{"#/bar"}; - relative.resolve_from(base); - EXPECT_EQ(relative.recompose(), "/foo#/bar"); -} - -TEST(URI_resolve_from, relative_path_from_relative_path) { - const sourcemeta::core::URI base{"foo/bar/baz"}; - sourcemeta::core::URI relative{"qux"}; - relative.resolve_from(base); - EXPECT_EQ(relative.recompose(), "foo/bar/qux"); -} - -// RFC 3986, inspired from -// https://cr.openjdk.org/~dfuchs/writeups/updating-uri/A Section "Resolutuon" - TEST(URI_resolve_from, rfc3986_resolve_with_relative_path) { const sourcemeta::core::URI base{"s://h/a/c"}; sourcemeta::core::URI relative_path{"../../b"}; @@ -110,3 +408,31 @@ TEST(URI_resolve_from, file_from_path) { relative.resolve_from(base); EXPECT_EQ(relative.recompose(), "file:///foo/bar/qux"); } + +TEST(URI_resolve_from, network_path_with_port) { + const sourcemeta::core::URI base{"http://example.com/path"}; + sourcemeta::core::URI reference{"//other.com:8080/newpath"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://other.com:8080/newpath"); +} + +TEST(URI_resolve_from, base_with_no_path) { + const sourcemeta::core::URI base{"http://example.com"}; + sourcemeta::core::URI reference{"path"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://example.com/path"); +} + +TEST(URI_resolve_from, base_with_trailing_slash) { + const sourcemeta::core::URI base{"http://example.com/dir/"}; + sourcemeta::core::URI reference{"file"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://example.com/dir/file"); +} + +TEST(URI_resolve_from, query_replaces_base_query) { + const sourcemeta::core::URI base{"http://example.com/path?oldquery"}; + sourcemeta::core::URI reference{"?newquery"}; + reference.resolve_from(base); + EXPECT_EQ(reference.recompose(), "http://example.com/path?newquery"); +} diff --git a/test/uri/uri_scheme_test.cc b/test/uri/uri_scheme_test.cc index 72e64f671..57239fac7 100644 --- a/test/uri/uri_scheme_test.cc +++ b/test/uri/uri_scheme_test.cc @@ -30,3 +30,71 @@ TEST(URI_scheme, urn_with_fragment) { EXPECT_TRUE(uri.scheme().has_value()); EXPECT_EQ(uri.scheme().value(), "urn"); } + +TEST(URI_scheme, rfc3986_scheme_case_insensitive) { + const sourcemeta::core::URI uri1{"HTTP://example.com"}; + const sourcemeta::core::URI uri2{"http://example.com"}; + EXPECT_EQ(uri1.scheme().value(), "http"); + EXPECT_EQ(uri2.scheme().value(), "http"); + EXPECT_EQ(uri1.scheme().value(), uri2.scheme().value()); +} + +TEST(URI_scheme, rfc3986_scheme_with_plus) { + const sourcemeta::core::URI uri{"svn+ssh://example.com"}; + EXPECT_TRUE(uri.scheme().has_value()); + EXPECT_EQ(uri.scheme().value(), "svn+ssh"); +} + +TEST(URI_scheme, rfc3986_scheme_with_dash) { + const sourcemeta::core::URI uri{"coap+tcp://example.com"}; + EXPECT_TRUE(uri.scheme().has_value()); + EXPECT_EQ(uri.scheme().value(), "coap+tcp"); +} + +TEST(URI_scheme, rfc3986_scheme_with_dot) { + const sourcemeta::core::URI uri{"vnd.example://example.com"}; + EXPECT_TRUE(uri.scheme().has_value()); + EXPECT_EQ(uri.scheme().value(), "vnd.example"); +} + +TEST(URI_scheme, rfc3986_scheme_single_letter) { + const sourcemeta::core::URI uri{"a://example.com"}; + EXPECT_TRUE(uri.scheme().has_value()); + EXPECT_EQ(uri.scheme().value(), "a"); +} + +TEST(URI_scheme, rfc3986_ftp_scheme) { + const sourcemeta::core::URI uri{"ftp://ftp.example.com/file.txt"}; + EXPECT_TRUE(uri.scheme().has_value()); + EXPECT_EQ(uri.scheme().value(), "ftp"); +} + +TEST(URI_scheme, rfc3986_file_scheme) { + const sourcemeta::core::URI uri{"file:///path/to/file"}; + EXPECT_TRUE(uri.scheme().has_value()); + EXPECT_EQ(uri.scheme().value(), "file"); +} + +TEST(URI_scheme, rfc3986_mailto_scheme) { + const sourcemeta::core::URI uri{"mailto:user@example.com"}; + EXPECT_TRUE(uri.scheme().has_value()); + EXPECT_EQ(uri.scheme().value(), "mailto"); +} + +TEST(URI_scheme, rfc3986_tel_scheme) { + const sourcemeta::core::URI uri{"tel:+1-816-555-1212"}; + EXPECT_TRUE(uri.scheme().has_value()); + EXPECT_EQ(uri.scheme().value(), "tel"); +} + +TEST(URI_scheme, rfc3986_data_scheme) { + const sourcemeta::core::URI uri{"data:text/plain;base64,SGVsbG8="}; + EXPECT_TRUE(uri.scheme().has_value()); + EXPECT_EQ(uri.scheme().value(), "data"); +} + +TEST(URI_scheme, rfc3986_tag_scheme) { + const sourcemeta::core::URI uri{"tag:example.com,2023:resource"}; + EXPECT_TRUE(uri.scheme().has_value()); + EXPECT_EQ(uri.scheme().value(), "tag"); +} diff --git a/test/uri/uri_user_info_test.cc b/test/uri/uri_user_info_test.cc index 003dde543..3b0b4f99c 100644 --- a/test/uri/uri_user_info_test.cc +++ b/test/uri/uri_user_info_test.cc @@ -20,3 +20,50 @@ TEST(URI_user_info, empty_example) { sourcemeta::core::URI uri{"http://host:80/"}; EXPECT_FALSE(uri.userinfo().has_value()); } + +TEST(URI_user_info, rfc3986_username_only) { + sourcemeta::core::URI uri{"http://user@host/path"}; + EXPECT_TRUE(uri.userinfo().has_value()); + EXPECT_EQ(uri.userinfo().value(), "user"); +} + +TEST(URI_user_info, rfc3986_username_empty_password) { + sourcemeta::core::URI uri{"http://user:@host/path"}; + EXPECT_TRUE(uri.userinfo().has_value()); + EXPECT_EQ(uri.userinfo().value(), "user:"); +} + +TEST(URI_user_info, rfc3986_username_password) { + sourcemeta::core::URI uri{"http://user:secret@host/path"}; + EXPECT_TRUE(uri.userinfo().has_value()); + EXPECT_EQ(uri.userinfo().value(), "user:secret"); +} + +TEST(URI_user_info, rfc3986_percent_encoded_userinfo) { + sourcemeta::core::URI uri{"http://user%20name:pass%20word@host/path"}; + EXPECT_TRUE(uri.userinfo().has_value()); + EXPECT_EQ(uri.userinfo().value(), "user%20name:pass%20word"); +} + +TEST(URI_user_info, rfc3986_userinfo_with_subdelims) { + sourcemeta::core::URI uri{"http://user!$&'()*+,;=:pass@host/path"}; + EXPECT_TRUE(uri.userinfo().has_value()); + EXPECT_EQ(uri.userinfo().value(), "user!$&'()*+,;=:pass"); +} + +TEST(URI_user_info, rfc3986_empty_userinfo_marker) { + sourcemeta::core::URI uri{"http://@host/path"}; + EXPECT_TRUE(uri.userinfo().has_value()); + EXPECT_EQ(uri.userinfo().value(), ""); +} + +TEST(URI_user_info, rfc3986_no_authority) { + sourcemeta::core::URI uri{"urn:example:resource"}; + EXPECT_FALSE(uri.userinfo().has_value()); +} + +TEST(URI_user_info, rfc3986_userinfo_with_at_sign) { + sourcemeta::core::URI uri{"http://user%40domain:pass@host/path"}; + EXPECT_TRUE(uri.userinfo().has_value()); + EXPECT_EQ(uri.userinfo().value(), "user%40domain:pass"); +}