diff --git a/doc/qbk/quickref.xml b/doc/qbk/quickref.xml index fdacd3d65..b4d1550dc 100644 --- a/doc/qbk/quickref.xml +++ b/doc/qbk/quickref.xml @@ -42,6 +42,7 @@ parse_relative_ref parse_uri parse_uri_reference + relative resolve diff --git a/include/boost/url/authority_view.hpp b/include/boost/url/authority_view.hpp index f0837a533..c5aa5db42 100644 --- a/include/boost/url/authority_view.hpp +++ b/include/boost/url/authority_view.hpp @@ -1013,6 +1013,183 @@ class BOOST_SYMBOL_VISIBLE string_view encoded_host_and_port() const noexcept; + //-------------------------------------------- + // + // Comparison + // + //-------------------------------------------- + + /** Return the result of comparing this with another authority + + This function compares two authorities + according to Syntax-Based comparison + algorithm. + + @par Exception Safety + Throws nothing. + + @par Specification + @li 6.2.2 Syntax-Based Normalization (rfc3986) + + @return -1 if `*this < other`, 0 if + `this == other`, and 1 if `this > other`. + */ + BOOST_URL_DECL + int + compare(authority_view const& other) const noexcept; + + /** Return the result of comparing two authorities + + The authorities are compared component + by component as if they were first + normalized. + + @par Complexity + Linear in `min( a0.size(), a1.size() )` + + @par Exception Safety + Throws nothing + */ + friend + bool + operator==( + authority_view const& a0, + authority_view const& a1) noexcept + { + return a0.compare(a1) == 0; + } + + /** Return the result of comparing two authorities + + The authorities are compared component + by component as if they were first + normalized. + + @par Effects + @code + return url( a0 ).normalize() != url( a1 ).normalize(); + @endcode + + @par Complexity + Linear in `min( a0.size(), a1.size() )` + + @par Exception Safety + Throws nothing + */ + friend + bool + operator!=( + authority_view const& a0, + authority_view const& a1) noexcept + { + return ! (a0 == a1); + } + + /** Return the result of comparing two authorities + + The authorities are compared component + by component as if they were first + normalized. + + @par Effects + @code + return url( a0 ).normalize() < url( a1 ).normalize(); + @endcode + + @par Complexity + Linear in `min( a0.size(), a1.size() )` + + @par Exception Safety + Throws nothing + */ + friend + bool + operator<( + authority_view const& a0, + authority_view const& a1) noexcept + { + return a0.compare(a1) < 0; + } + + /** Return the result of comparing two authorities + + The authorities are compared component + by component as if they were first + normalized. + + @par Effects + @code + return url( a0 ).normalize() <= url( a1 ).normalize(); + @endcode + + @par Complexity + Linear in `min( a0.size(), a1.size() )` + + @par Exception Safety + Throws nothing + */ + friend + bool + operator<=( + authority_view const& a0, + authority_view const& a1) noexcept + { + return a0.compare(a1) <= 0; + } + + /** Return the result of comparing two authorities + + The authorities are compared component + by component as if they were first + normalized. + + @par Effects + @code + return url( a0 ).normalize() > url( a1 ).normalize(); + @endcode + + @par Complexity + Linear in `min( a0.size(), a1.size() )` + + @par Exception Safety + Throws nothing + */ + friend + bool + operator>( + authority_view const& a0, + authority_view const& a1) noexcept + { + return a0.compare(a1) > 0; + } + + /** Return the result of comparing two authorities + + The authorities are compared component + by component as if they were first + normalized. + + @par Effects + @code + return url( a0 ).normalize() >= url( a1 ).normalize(); + @endcode + + @par Complexity + Linear in `min( a0.size(), a1.size() )` + + @par Exception Safety + Throws nothing + */ + friend + bool + operator>=( + authority_view const& a0, + authority_view const& a1) noexcept + { + return a0.compare(a1) >= 0; + } + // hidden friend friend std::ostream& diff --git a/include/boost/url/impl/authority_view.ipp b/include/boost/url/impl/authority_view.ipp index f698ef49f..a4c893a3a 100644 --- a/include/boost/url/impl/authority_view.ipp +++ b/include/boost/url/impl/authority_view.ipp @@ -259,6 +259,65 @@ parse_authority( return grammar::parse(s, authority_rule); } +//------------------------------------------------ +// +// Comparisons +// +//------------------------------------------------ + +int +authority_view:: +compare(const authority_view& other) const noexcept +{ + int comp = has_userinfo() - other.has_userinfo(); + if ( comp != 0 ) + return comp; + + if (has_userinfo()) + { + comp = detail::compare_encoded( + encoded_user(), + other.encoded_user()); + if ( comp != 0 ) + return comp; + + comp = has_password() - other.has_password(); + if ( comp != 0 ) + return comp; + + if (has_password()) + { + comp = detail::compare_encoded( + encoded_password(), + other.encoded_password()); + if ( comp != 0 ) + return comp; + } + } + + comp = detail::ci_compare_encoded( + encoded_host(), + other.encoded_host()); + if ( comp != 0 ) + return comp; + + comp = has_port() - other.has_port(); + if ( comp != 0 ) + return comp; + + if (has_port()) + { + comp = detail::compare( + port(), + other.port()); + if ( comp != 0 ) + return comp; + } + + return 0; +} + + } // urls } // boost diff --git a/include/boost/url/impl/url.ipp b/include/boost/url/impl/url.ipp index 76c2a7dbd..c58da7cd1 100644 --- a/include/boost/url/impl/url.ipp +++ b/include/boost/url/impl/url.ipp @@ -137,6 +137,253 @@ reserve_impl( u_.cs_ = s_; } +result +relative( + url_view_base const& base, + url_view_base const& href, + url_base& dest) +{ + // AFREITAS: + // - filesystem functions use + // (const path&, const path& base) + // - this function name is probably still bad + // - the function in URI.js behaves slightly + // differently from their own examples + // see https://medialize.github.io/URI.js/docs.html#relativeto + // - we have a bug where u.set_path("") or + // u.segments() = {} crash, so this function + // uses u.segments() = {"."}, which gives + // us slightly different results. + // - The basic exception guarantee is not + // satisfied in case of allocation errors. + + BOOST_ASSERT(&dest != &base); + BOOST_ASSERT(&dest != &href); + + // Validate input + if (!href.is_path_absolute()) + { + // href is already relative + return error::not_a_base; + } + if (!base.is_path_absolute()) + { + // cannot calculate a URI relative to another relative URI + return error::not_a_base; + } + + // Resolve scheme + if (href.scheme() == base.scheme() || + !href.has_scheme()) + dest.remove_scheme(); + else + dest.set_scheme(href.scheme()); + + // Resolve authority + if (dest.has_scheme() || + href.has_authority() != base.has_authority() || + href.authority() != base.authority() || + href.has_userinfo() || + href.has_password()) + { + // Otherwise, copy all but scheme from href + if (href.has_authority()) + dest.set_encoded_authority(href.encoded_authority()); + else + dest.remove_authority(); + dest.set_encoded_path(href.encoded_path()); + dest.normalize_path(); + if (href.has_query()) + dest.set_encoded_query(href.encoded_query()); + else + dest.remove_query(); + if (href.has_fragment()) + dest.set_encoded_fragment(href.encoded_fragment()); + else + dest.remove_fragment(); + return {}; + } + dest.remove_authority(); + + // Resolve path + // 0. Get segments + auto segs0 = base.segments(); + auto segs1 = href.segments(); + auto begin0 = segs0.begin(); + auto it0 = begin0; + auto end0 = segs0.end(); + auto last0 = begin0 != end0 ? std::prev(end0) : end0; + auto begin1 = segs1.begin(); + auto it1 = begin1; + auto end1 = segs1.end(); + auto last1 = begin0 != end1 ? std::prev(end1) : end1; + pct_encoded_view const dotdot(".."); + pct_encoded_view const dot("."); + + // 1. Find the longest common path + while ( + it0 != last0 && + it1 != last1) + { + if (*it0 == *it1) + { + ++it0; + ++it1; + } + else if (*it0 == dot) + { + ++it0; + } + else if (*it1 == dot) + { + ++it1; + } + else if (*it0 == dotdot) + { + ++it0; + if (it1 != begin1) + --it1; + } + else if (*it1 == dotdot) + { + if (it0 != begin0) + --it0; + ++it1; + } + else + { + // Check if *it0 will be consumed by a dotdot + auto it2 = std::next(it0); + std::size_t l = 1; + while (it2 != last0) + { + if (*it2 == dotdot) + { + if (--l == 0) + { + ++it2; + it0 = it2; + break; + } + } + else if (*it2 != dot) + { + ++l; + } + ++it2; + } + if (it0 == it2) + continue; + + // Check if *it1 will be consumed by a dotdot + auto it3 = std::next(it1); + l = 1; + while (it3 != last1) + { + if (*it3 == dotdot) + { + if (--l == 0) + { + ++it3; + it1 = it3; + break; + } + } + else if (*it3 != dot) + { + ++l; + } + ++it3; + } + if (it1 == it3) + continue; + + break; + } + } + + // 1.b Check if paths are the same + if (it0 == last0 && + it1 == last1 && + it0 != end0 && + it1 != end1 && + *it0 == *it1) + { + // Return empty path + dest.segments() = {dot.encoded()}; + if (href.has_query()) + dest.set_encoded_query(href.encoded_query()); + else + dest.remove_query(); + if (href.has_fragment()) + dest.set_encoded_fragment(href.encoded_fragment()); + else + dest.remove_fragment(); + return {}; + } + + // 2. replace each path component in the + // base path with ../ + segments_encoded segs = dest.encoded_segments(); + segs = {dot.encoded()}; + if (it0 != end0) + { + dest.set_path_absolute(false); + auto last0 = std::prev(end0); + while (it0 != last0) + { + if (*it0 == dotdot) + { + if (segs.size() > 1) + segs.pop_back(); + else + segs = {dot.encoded()}; + } + else if (*it0 != dot) + { + if (dest.path() == dot) + segs = {dotdot.encoded()}; + else + segs.push_back(dotdot.encoded()); + } + ++it0; + } + } + + // 3. Append the reference path + while (it1 != end1) + { + if (*it1 == dotdot) + { + if (segs.size() > 1) + segs.pop_back(); + else + segs = {dot.encoded()}; + } + else if (*it1 != dot) + { + string_view v = (*it1).encoded(); + if (dest.path() == dot) + segs = {v}; + else + segs.push_back(v); + } + ++it1; + } + if (href.has_query()) + dest.set_encoded_query(href.encoded_query()); + else + dest.remove_query(); + + if (href.has_fragment()) + dest.set_encoded_fragment(href.encoded_fragment()); + else + dest.remove_fragment(); + + return {}; +} + + } // urls } // boost diff --git a/include/boost/url/impl/url_view_base.ipp b/include/boost/url/impl/url_view_base.ipp index 4ef4519ff..a76883dac 100644 --- a/include/boost/url/impl/url_view_base.ipp +++ b/include/boost/url/impl/url_view_base.ipp @@ -548,35 +548,29 @@ int url_view_base:: compare(const url_view_base& other) const noexcept { - int comp = detail::ci_compare( - scheme(), - other.scheme()); + int comp = has_scheme() - other.has_scheme(); if ( comp != 0 ) return comp; - comp = detail::compare_encoded( - encoded_user(), - other.encoded_user()); - if ( comp != 0 ) - return comp; - - comp = detail::compare_encoded( - encoded_password(), - other.encoded_password()); - if ( comp != 0 ) - return comp; + if (has_scheme()) + { + comp = detail::ci_compare( + scheme(), + other.scheme()); + if ( comp != 0 ) + return comp; + } - comp = detail::ci_compare_encoded( - encoded_host(), - other.encoded_host()); + comp = has_authority() - other.has_authority(); if ( comp != 0 ) return comp; - comp = detail::compare( - port(), - other.port()); - if ( comp != 0 ) - return comp; + if (has_authority()) + { + comp = authority().compare(other.authority()); + if ( comp != 0 ) + return comp; + } comp = detail::normalized_path_compare( encoded_path(), @@ -586,18 +580,32 @@ compare(const url_view_base& other) const noexcept if ( comp != 0 ) return comp; - comp = detail::compare_encoded( - encoded_query(), - other.encoded_query()); + comp = has_query() - other.has_query(); if ( comp != 0 ) return comp; - comp = detail::compare_encoded( - encoded_fragment(), - other.encoded_fragment()); + if (has_query()) + { + comp = detail::compare_encoded( + encoded_query(), + other.encoded_query()); + if ( comp != 0 ) + return comp; + } + + comp = has_fragment() - other.has_fragment(); if ( comp != 0 ) return comp; + if (has_fragment()) + { + comp = detail::compare_encoded( + encoded_fragment(), + other.encoded_fragment()); + if ( comp != 0 ) + return comp; + } + return 0; } diff --git a/include/boost/url/url.hpp b/include/boost/url/url.hpp index 53145248e..64cb49bcd 100644 --- a/include/boost/url/url.hpp +++ b/include/boost/url/url.hpp @@ -322,6 +322,74 @@ resolve( return dest.resolve_impl(base, ref); } +/** Compares two absolute paths and make one relative to the other + + This function compares the absolute paths in + two urls. It returns a new url with a relative + path that references the target path relative + to the base path. + + Unlike @ref resolve, this function takes + two absolute paths to create a relative + path. + + If the input URLs contain schemes and + authorities, these are resolved. If the + schemes and authorities are the same, they + are removed before the relative path is + calculated. If they are different, only + the relative path of the reference URL + is returned. + + Given the input base URL, this function + resolves the reference URL as if performing + the following steps: + + @li Normalize both URLs + @li Remove the longest common path from both paths + @li Replace each segment in the base path with ".." + @li Append the reference path + + This function places the result of the + resolution into `dest`, which can be + any of the url containers that inherit + from @ref url_base. + + If an error occurs, the contents of + `dest` is unspecified and `ec` is set. + + @par Example + @code + url dest; + error_code ec; + relative("/relative/sub/foo/sub/file", "/relative/path", dest, ec); + assert( dest.str() == "../../../path" ); + @endcode + + @par Exception Safety + Basic guarantee. + Calls to allocate may throw. + + @param base The base URL to resolve against. + + @param href The target URL the relative URL should point to + + @param dest The container where the relative result + is written, upon success. + + @param ec Set to the error if any occurred. + + @see + @ref url, + @ref url_view. +*/ +BOOST_URL_DECL +result +relative( + url_view_base const& base, + url_view_base const& href, + url_base& dest); + } // urls } // boost diff --git a/include/boost/url/url_view_base.hpp b/include/boost/url/url_view_base.hpp index aeedb3fbc..225920c6c 100644 --- a/include/boost/url/url_view_base.hpp +++ b/include/boost/url/url_view_base.hpp @@ -1436,10 +1436,7 @@ class BOOST_SYMBOL_VISIBLE { pct_decode_opts opt; opt.plus_to_space = false; - string_view s = encoded_path(); - return - detail::access::construct( - s, u_.decoded_[id_path], opt); + return pct_encoded_view(encoded_path()); } /** Return the path segments @@ -1876,8 +1873,8 @@ class BOOST_SYMBOL_VISIBLE /** Return the result of comparing two URLs - The URLs are compared character - by character as if they were first + The URLs are compared component by + component as if they were first normalized. @par Effects @@ -1902,8 +1899,8 @@ class BOOST_SYMBOL_VISIBLE /** Return the result of comparing two URLs - The URLs are compared character - by character as if they were first + The URLs are compared component by + component as if they were first normalized. @par Effects @@ -1928,8 +1925,8 @@ class BOOST_SYMBOL_VISIBLE /** Return the result of comparing two URLs - The URLs are compared character - by character as if they were first + The URLs are compared component by + component as if they were first normalized. @par Effects @@ -1954,8 +1951,8 @@ class BOOST_SYMBOL_VISIBLE /** Return the result of comparing two URLs - The URLs are compared character - by character as if they were first + The URLs are compared component by + component as if they were first normalized. @par Effects @@ -1980,8 +1977,8 @@ class BOOST_SYMBOL_VISIBLE /** Return the result of comparing two URLs - The URLs are compared character - by character as if they were first + The URLs are compared component by + component as if they were first normalized. @par Effects @@ -2006,8 +2003,8 @@ class BOOST_SYMBOL_VISIBLE /** Return the result of comparing two URLs - The URLs are compared character - by character as if they were first + The URLs are compared component by + component as if they were first normalized. @par Effects diff --git a/test/unit/url.cpp b/test/unit/url.cpp index 74df13b51..17e12012e 100644 --- a/test/unit/url.cpp +++ b/test/unit/url.cpp @@ -2066,6 +2066,111 @@ class url_test check("g#s/../x" , "http://a/b/c/g#s/../x"); } + void + testRelative() + { + auto const check = []( + string_view b, + string_view r, + string_view e) + { + auto ub = + parse_uri_reference(b).value(); + auto ur = + parse_uri_reference(r).value(); + url dest = parse_uri_reference("x/y" ).value(); + auto rv = relative(ub, ur, dest); + BOOST_TEST(!rv.has_error()); + BOOST_TEST_EQ(dest.string(), e); + }; + + // relative URL / absolute paths + check("/a/path/to/somewhere/else", "/a/path/to/a", "../a"); + check("/relative/sub/foo/sub/file", "/relative/path", "../../../path"); + check("http://google.com/baz", "http://example.org/world.html", "//example.org/world.html"); + check("http://google.com/baz", "http:/world.html", "/world.html"); + // AFREITAS: The paths below should be "" but set_path("") and segments() = {} crash + // "." is still equivalent + check("http://www.example.com:8080/dir/file", "http://www.example.com:8080/dir/file", "."); + check("http://www.example.com:8080/dir/file", "http://www.example.com:8080/dir/file?foo=bar#abcd", ".?foo=bar#abcd"); + // "." should be ignored + check("/a/path/././to/./somewhere/else", "/a/./path/./to/a", "../a"); + // ".." should be normalized + check("/a/path/x/../to/y/../somewhere/else", "/b/../a/path/to/a", "../a"); + // same parent + check("/relative/file?some=query#hash", "/relative/path?blubber=1#hash1", "path?blubber=1#hash1"); + // direct parent + check("/relative/sub/file?some=query#hash", "/relative/path?blubber=1#hash1", "../path?blubber=1#hash1"); + // second parent + check("/relative/sub/sub/file?some=query#hash", "/relative/path?blubber=1#hash1", "../../path?blubber=1#hash1"); + // third parent + check("/relative/sub/foo/sub/file?some=query#hash", "/relative/path?blubber=1#hash1", "../../../path?blubber=1#hash1"); + // parent top level + check("/path/to/file?some=query#hash", "/relative/path?blubber=1#hash1", "../../relative/path?blubber=1#hash1"); + // descendant + check("/base/path/top.html", "/base/path/with/subdir/inner.html", "with/subdir/inner.html"); + // same directory + check("/path/top.html", "/path/", "./"); + // absolute / + check("http://example.org/", "http://example.org/foo/bar/bat", "foo/bar/bat"); + // absolute /foo + check("http://example.org/foo", "http://example.org/foo/bar/bat", "foo/bar/bat"); + // absolute /foo/ + check("http://example.org/foo/", "http://example.org/foo/bar/bat", "bar/bat"); + // same scheme + check("http://example.com/foo/", "http://example.org/foo/bar/bat", "//example.org/foo/bar/bat"); + // different scheme + check("https://example.org/foo/", "http://example.org/foo/bar", "http://example.org/foo/bar"); + // base with no scheme or host + check("/foo/", "http://example.org/foo/bar", "http://example.org/foo/bar"); + // base with no scheme + check("//example.org/foo/bar", "http://example.org/foo/bar", "http://example.org/foo/bar"); + // denormalized base + check("/foo/./bar/", "/foo/bar/bat", "bat"); + // denormalized url + check("/foo/bar/", "/foo//bar/bat", "..//bar/bat"); + // credentials + check("http://example.org/foo/", "http://user:pass@example.org/foo/bar", "//user:pass@example.org/foo/bar"); + // base credentials + check("http://user:pass@example.org/foo/bar", "http://example.org/foo/bar", "//example.org/foo/bar"); + // same credentials different host + check("http://user:pass@example.com/foo/bar", "http://user:pass@example.org/foo/bar", "//user:pass@example.org/foo/bar"); + // different port 1 + check("http://example.org:8080/foo/bar", "http://example.org/foo/bar", "//example.org/foo/bar"); + // different port 2 + check("http://example.org:8080/foo/bar", "http://example.org:8081/foo/bar", "//example.org:8081/foo/bar"); + // different port 3 + check("http://example.org/foo/bar", "http://example.org:8081/foo/bar", "//example.org:8081/foo/bar"); + // same path - fragment + check("http://www.example.com:8080/dir/file", "http://www.example.com:8080/dir/file#abcd", ".#abcd"); + // same path - query + check("http://www.example.com:8080/dir/file", "http://www.example.com:8080/dir/file?abcd=123", ".?abcd=123"); + // same path - query and fragment + check("http://www.example.com:8080/dir/file", "http://www.example.com:8080/dir/file?abcd=123#alpha", ".?abcd=123#alpha"); + + auto const fail = []( + string_view b, + string_view r, + error_code e) + { + auto ub = + parse_uri_reference(b).value(); + auto ur = + parse_uri_reference(r).value(); + url dest = parse_uri_reference("x/y" ).value(); + auto rv = relative(ub, ur, dest); + BOOST_TEST(rv.has_error()); + BOOST_TEST_EQ(rv.error(), e); + }; + + // already relative + fail("/foo/", "foo/bar", error::not_a_base); + + // relative base + fail("foo/", "/foo/bar", error::not_a_base); + + } + //-------------------------------------------- void @@ -2255,6 +2360,7 @@ class url_test testFragment(); testSegments(); testResolution(); + testRelative(); testOstream(); testNormalize(); }