-
-
Notifications
You must be signed in to change notification settings - Fork 16
Make URI::relative_to the strict inverse of URI::resolve_from
#2719
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8ad4042
96d1f2a
0799653
4076d73
8dc66ab
6032c11
50a90ab
10b8b4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,6 +98,10 @@ auto URI::resolve_from(const URI &base) -> URI & { | |
| this->userinfo_ = base.userinfo_; | ||
| this->host_ = base.host_; | ||
| this->port_ = base.port_; | ||
| // RFC 3986 Section 5.2.2 inherits the whole authority, and whether the host | ||
| // is an IP literal is part of it, as Section 3.2.2 only writes the enclosing | ||
| // brackets for that form | ||
| this->ip_literal_ = base.ip_literal_; | ||
|
|
||
| // Reference has empty path | ||
| if (!this->path_.has_value() || this->path_.value().empty()) { | ||
|
|
@@ -137,29 +141,23 @@ auto URI::relative_to(const URI &base) -> URI & { | |
| } | ||
|
|
||
| // The full authority must match (but components can be null for URNs) | ||
| if (this->userinfo_ != base.userinfo_) { | ||
| return *this; | ||
| } | ||
|
|
||
| if (this->host_ != base.host_) { | ||
| return *this; | ||
| } | ||
|
|
||
| if (this->port_ != base.port_) { | ||
| if (!this->has_same_authority(base)) { | ||
| return *this; | ||
| } | ||
|
|
||
| // Special case: both URIs are exactly the same | ||
| if (this->path_ == base.path_ && this->query_ == base.query_ && | ||
| this->fragment_ == base.fragment_) { | ||
| // Clear all components to make it empty relative URI | ||
| // Clear every component the base supplies back on resolution, which is | ||
| // everything but the fragment. RFC 3986 Section 5.2.2 always takes the | ||
| // fragment from the reference, as "T.fragment = R.fragment", so an empty | ||
| // reference names the base without one and has to keep it here | ||
| this->scheme_.reset(); | ||
| this->userinfo_.reset(); | ||
| this->host_.reset(); | ||
| this->port_.reset(); | ||
| this->path_.reset(); | ||
| this->query_.reset(); | ||
| this->fragment_.reset(); | ||
| return *this; | ||
| } | ||
|
|
||
|
|
@@ -227,43 +225,29 @@ auto URI::relative_to(const URI &base) -> URI & { | |
| return *this; | ||
| } | ||
|
|
||
| // Case 2: Check if this_path starts with base_path followed by "/" | ||
| // This handles: base="/foo" and this="/foo/bar" = "bar" | ||
| // But NOT: base="/spec" and this="/spec/" (different resources) | ||
| // RFC 3986 Section 5.2.2 uses a reference path that starts with a slash as | ||
| // is, so a remainder that begins with one would drop the base prefix rather | ||
| // than name something below it | ||
| const std::string base_with_slash = | ||
| base_path.ends_with('/') ? base_path : base_path + "/"; | ||
| if (this_path.starts_with(base_with_slash) && | ||
| this_path.length() > base_with_slash.length() && | ||
| this_path[base_with_slash.length()] != '/') { | ||
| auto relative_path = this_path.substr(base_with_slash.length()); | ||
|
|
||
| this->scheme_.reset(); | ||
| this->userinfo_.reset(); | ||
| this->host_.reset(); | ||
| this->port_.reset(); | ||
| this->path_ = relative_path.empty() | ||
| ? std::nullopt | ||
| : std::optional<std::string>{relative_path}; | ||
|
|
||
| return *this; | ||
| } | ||
|
|
||
| // Find last slash positions (needed for multiple cases below) | ||
| const auto base_last_slash = base_path.rfind('/'); | ||
| const auto this_last_slash = this_path.rfind('/'); | ||
|
|
||
| // Case 3: Check if both paths share the same parent directory (siblings) | ||
| // RFC 3986 Section 5.2.3 merges against the base path with everything after | ||
| // its right-most slash excluded, and Section 5.2.2 removes dot segments only | ||
| // once that merge has happened, so what a reference is really measured | ||
| // against is that prefix after normalisation. Normalising the whole base | ||
| // path instead would be wrong, as a dot segment sitting after the last slash | ||
| // is dropped by the merge rather than applied | ||
| const auto base_anchor = | ||
| base_last_slash != std::string::npos | ||
| ? remove_dot_segments(base_path.substr(0, base_last_slash + 1)) | ||
| : std::string{}; | ||
|
|
||
| // Case 2: Check if both paths share the same parent directory (siblings) | ||
| // This handles: base="/test/bar.json" and this="/test/foo.json" = | ||
| // "foo.json" | ||
| if (base_last_slash != std::string::npos && | ||
| this_last_slash != std::string::npos) { | ||
| const auto base_parent = base_path.substr(0, base_last_slash + 1); | ||
| const auto this_parent = this_path.substr(0, this_last_slash + 1); | ||
|
|
||
| if (base_parent == this_parent) { | ||
| if (base_anchor == this_parent) { | ||
| auto relative_path = this_path.substr(this_last_slash + 1); | ||
|
|
||
| this->scheme_.reset(); | ||
|
|
@@ -280,17 +264,13 @@ auto URI::relative_to(const URI &base) -> URI & { | |
| } | ||
| } | ||
|
|
||
| // Case 4: General case - compute relative path using .. segments | ||
| // Case 3: General case - compute relative path using .. segments | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For rootless absolute bases, this branch still violates the new inverse contract: with base Severity: medium 🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
| // This handles cases like: base="/schemas/foo.json" and this="/bundling/bar" | ||
| // Result should be "../bundling/bar" | ||
| // Note: We don't make URIs relative if the target is just a shallow path | ||
| // like "/foo" (only one level deep) as that's not meaningfully navigable | ||
| const auto base_parent = base_last_slash != std::string::npos | ||
| ? base_path.substr(0, base_last_slash + 1) | ||
| : base_path; | ||
|
|
||
| std::string relative_path; | ||
| std::string current_base_parent{base_parent}; | ||
| std::string current_base_parent{base_anchor}; | ||
|
|
||
| while (!current_base_parent.empty() && current_base_parent != "/") { | ||
| if (this_path.starts_with(current_base_parent)) { | ||
|
|
@@ -361,16 +341,14 @@ auto merge_new_base_path(std::optional<std::string> &target_path, | |
| if (new_base_path.has_value() && saved_path.has_value()) { | ||
| auto merged{std::move(new_base_path.value())}; | ||
| const auto &relative_path = saved_path.value(); | ||
| const auto base_ends_with_slash = merged.ends_with('/'); | ||
| const auto relative_starts_with_slash = relative_path.starts_with('/'); | ||
| if (base_ends_with_slash && relative_starts_with_slash) { | ||
| merged.append(relative_path, 1); | ||
| } else if (!base_ends_with_slash && !relative_starts_with_slash) { | ||
| // The suffix is what lies below the old base with the separating slash | ||
| // already removed, so a slash it does start with opens an empty segment | ||
| // and must not be mistaken for that separator | ||
| if (!merged.empty() && !merged.ends_with('/')) { | ||
| merged += '/'; | ||
| merged += relative_path; | ||
| } else { | ||
| merged += relative_path; | ||
| } | ||
|
|
||
| merged += relative_path; | ||
| target_path = std::move(merged); | ||
| } else if (new_base_path.has_value()) { | ||
| target_path = std::move(new_base_path); | ||
|
|
@@ -379,59 +357,69 @@ auto merge_new_base_path(std::optional<std::string> &target_path, | |
| } | ||
| } | ||
|
|
||
| // The portion of a path that lies below a base, or no value when the URI is | ||
| // neither the base nor under it. Component boundaries are respected, so a path | ||
| // of "/foobar" does not lie under "/foo" | ||
| auto path_under(const URI &uri, const URI &base) -> std::optional<std::string> { | ||
| if (uri.scheme() != base.scheme() || !uri.has_same_authority(base)) { | ||
| return std::nullopt; | ||
| } | ||
|
|
||
| return URI::strip_path_prefix(uri.path().value_or(""), | ||
| base.path().value_or("")); | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| auto URI::rebase(const URI &base, const URI &new_base) -> URI & { | ||
| this->relative_to(base); | ||
| if (!this->is_relative()) { | ||
| auto suffix{path_under(*this, base)}; | ||
| if (!suffix.has_value()) { | ||
| return *this; | ||
| } | ||
|
|
||
| auto saved_path = std::move(this->path_); | ||
| auto saved_fragment = std::move(this->fragment_); | ||
| auto saved_query = std::move(this->query_); | ||
| std::optional<std::string> relative_path; | ||
| if (!suffix.value().empty()) { | ||
| relative_path = std::move(suffix.value()); | ||
| } | ||
|
|
||
| this->scheme_ = new_base.scheme_; | ||
| this->userinfo_ = new_base.userinfo_; | ||
| this->host_ = new_base.host_; | ||
| this->port_ = new_base.port_; | ||
| this->ip_literal_ = new_base.ip_literal_; | ||
| // The new components come from the new base, so the result is an IRI if the | ||
| // new base is one | ||
| this->iri_ = this->iri_ || new_base.iri_; | ||
|
|
||
| std::optional<std::string> new_base_path_copy{new_base.path_}; | ||
| merge_new_base_path(this->path_, std::move(new_base_path_copy), | ||
| std::move(saved_path)); | ||
|
|
||
| this->fragment_ = std::move(saved_fragment); | ||
| this->query_ = std::move(saved_query); | ||
| std::move(relative_path)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An empty path segment below the old base is lost here: rebasing Severity: medium Other Locations
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage. |
||
|
|
||
| return *this; | ||
| } | ||
|
|
||
| auto URI::rebase(const URI &base, URI &&new_base) -> URI & { | ||
| this->relative_to(base); | ||
| if (!this->is_relative()) { | ||
| auto suffix{path_under(*this, base)}; | ||
| if (!suffix.has_value()) { | ||
| return *this; | ||
| } | ||
|
|
||
| auto saved_path = std::move(this->path_); | ||
| auto saved_fragment = std::move(this->fragment_); | ||
| auto saved_query = std::move(this->query_); | ||
| std::optional<std::string> relative_path; | ||
| if (!suffix.value().empty()) { | ||
| relative_path = std::move(suffix.value()); | ||
| } | ||
|
|
||
| this->scheme_ = std::move(new_base.scheme_); | ||
| this->userinfo_ = std::move(new_base.userinfo_); | ||
| this->host_ = std::move(new_base.host_); | ||
| this->port_ = new_base.port_; | ||
| this->ip_literal_ = new_base.ip_literal_; | ||
| // The new components come from the new base, so the result is an IRI if the | ||
| // new base is one | ||
| this->iri_ = this->iri_ || new_base.iri_; | ||
|
|
||
| merge_new_base_path(this->path_, std::move(new_base.path_), | ||
| std::move(saved_path)); | ||
|
|
||
| this->fragment_ = std::move(saved_fragment); | ||
| this->query_ = std::move(saved_query); | ||
| std::move(relative_path)); | ||
|
|
||
| return *this; | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.