Skip to content

Commit

Permalink
Fix bug in _SimpleUri.resolve.
Browse files Browse the repository at this point in the history
In some cases it didn't follow our non-RFC behavior for resolving a relative
path on top of another relative path.

Fixes issue #27447
BUG= http://dartbug.com/27447
R=eernst@google.com

Review URL: https://codereview.chromium.org/2374253004 .
  • Loading branch information
lrhn committed Sep 29, 2016
1 parent 2ca7ae5 commit 9bd4406
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 56 deletions.
42 changes: 26 additions & 16 deletions sdk/lib/core/uri.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4323,10 +4323,6 @@ class _SimpleUri implements Uri {
base._schemeCache);
}
// Merge paths.
if (base._uri.startsWith("../", base._pathStart)) {
// Complex rare case, go slow.
return _toNonSimple().resolveUri(ref);
}

// The RFC 3986 algorithm merges the base path without its final segment
// (anything after the final "/", or everything if the base path doesn't
Expand All @@ -4341,39 +4337,53 @@ class _SimpleUri implements Uri {
String refUri = ref._uri;
int baseStart = base._pathStart;
int baseEnd = base._queryStart;
while (baseUri.startsWith("../", baseStart)) baseStart += 3;
int refStart = ref._pathStart;
int refEnd = ref._queryStart;
int backCount = 1;

int slashCount = 0;

// Count leading ".." segments in reference path.
/// Count of leading ".." segments in reference path.
/// The count is decremented when the segment is matched with a
/// segment of the base path, and both are then omitted from the result.
int backCount = 0;
/// Count "../" segments and advance `refStart` to after the segments.
while (refStart + 3 <= refEnd && refUri.startsWith("../", refStart)) {
refStart += 3;
backCount += 1;
}

// Extra slash inserted between base and reference path parts if
// the base path contains any slashes.
// the base path contains any slashes, or empty string if none.
// (We could use a slash from the base path in most cases, but not if
// we remove the entire base path).
String insert = "";

/// Remove segments from the base path.
/// Start with the segment trailing the last slash,
/// then remove segments for each leading "../" segment
/// from the reference path, or as many of them as are available.
while (baseEnd > baseStart) {
baseEnd--;
int char = baseUri.codeUnitAt(baseEnd);
if (char == _SLASH) {
insert = "/";
backCount--;
if (backCount == 0) break;
backCount--;
}
}
// If the base URI has no scheme or authority (`_pathStart == 0`)
// and a relative path, and we reached the beginning of the path,
// we have a special case.
if (baseEnd == 0 && !base.hasAbsolutePath) {
// Non-RFC 3986 behavior when resolving a purely relative path on top of
// another relative path: Don't make the result absolute.

if (baseEnd == baseStart && !base.hasScheme && !base.hasAbsolutePath) {
// If the base is *just* a relative path (no scheme or authority),
// then merging with another relative path doesn't follow the
// RFC-3986 behavior.
// Don't need to check `base.hasAuthority` since the base path is
// non-empty - if there is an authority, a non-empty path is absolute.

// We reached the start of the base path, and want to stay relative,
// so don't insert a slash.
insert = "";
// If we reached the start of the base path with more "../" left over
// in the reference path, include those segments in the result.
refStart -= backCount * 3;
}

var delta = baseEnd - refStart + insert.length;
Expand Down
125 changes: 85 additions & 40 deletions tests/corelib/uri_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,42 @@ testEncodeDecodeQueryComponent(String orig,
}

testUriPerRFCs() {
final urisSample = "http://a/b/c/d;p?q";
Uri base = Uri.parse(urisSample);
// Convert a Uri to a guaranteed "non simple" URI with the same content.
toComplex(Uri uri) {
Uri complex = new Uri(
scheme: uri.scheme,
userInfo: uri.hasAuthority ? uri.userInfo : null,
host: uri.hasAuthority ? uri.host : null,
port: uri.hasAuthority ? uri.port : null,
path: uri.path,
query: uri.hasQuery ? uri.query : null,
fragment: uri.hasFragment ? uri.fragment : null,
);
assert(complex.toString() == uri.toString());
return complex;
}

Uri base;
Uri complexBase;
// Sets the [base] and [complexBase] to the parse of the URI and a
// guaranteed non-simple version of the same URI.
setBase(String uri) {
base = Uri.parse(uri);
complexBase = toComplex(base);
}

testResolve(expect, relative) {
String name = "$base << $relative";
Expect.stringEquals(expect, base.resolve(relative).toString(), name);

Expect.stringEquals(expect, complexBase.resolve(relative).toString(),
name + " (complex base)");
}

// From RFC 3986.
final urisSample = "http://a/b/c/d;p?q";
setBase(urisSample);

testResolve("g:h", "g:h");
testResolve("http://a/b/c/g", "g");
testResolve("http://a/b/c/g", "./g");
Expand Down Expand Up @@ -139,47 +167,66 @@ testUriPerRFCs() {
// Additional tests (not from RFC 3986).
testResolve("http://a/b/g;p/h;s", "../g;p/h;s");

base = Uri.parse("s:a/b");
setBase("s:a/b");
testResolve("s:a/c", "c");
testResolve("s:/c", "../c");

base = Uri.parse("S:a/b");
setBase("S:a/b");
testResolve("s:a/c", "c");
testResolve("s:/c", "../c");

base = Uri.parse("s:foo");
setBase("s:foo");
testResolve("s:bar", "bar");
testResolve("s:bar", "../bar");

base = Uri.parse("S:foo");
setBase("S:foo");
testResolve("s:bar", "bar");
testResolve("s:bar", "../bar");

// Special-case (deliberate non-RFC behavior).
base = Uri.parse("foo/bar");
setBase("foo/bar");
testResolve("foo/baz", "baz");
testResolve("baz", "../baz");

base = Uri.parse("s:/foo");
setBase("s:/foo");
testResolve("s:/bar", "bar");
testResolve("s:/bar", "../bar");

base = Uri.parse("S:/foo");
setBase("S:/foo");
testResolve("s:/bar", "bar");
testResolve("s:/bar", "../bar");

// Test non-URI base (no scheme, no authority, relative path).
base = Uri.parse("a/b/c?_#_");
setBase("a/b/c?_#_");
testResolve("a/b/g?q#f", "g?q#f");
testResolve("./", "../..");
testResolve("../", "../../..");
testResolve("a/b/", ".");
testResolve("c", "../../c"); // Deliberate non-RFC behavior.
base = Uri.parse("../../a/b/c?_#_"); // Initial ".." in base url.
setBase("../../a/b/c?_#_"); // Initial ".." in base url.
testResolve("../../a/d", "../d");
testResolve("../../d", "../../d");
testResolve("../../../d", "../../../d");

base = Uri.parse("s://h/p?q#f"); // A simple base.
setBase("../../a/b");
testResolve("../../a/d", "d");
testResolve("../../d", "../d");
testResolve("../../../d", "../../d");
setBase("../../a");
testResolve("../../d", "d");
testResolve("../../../d", "../d");
testResolve("../../../../d", "../../d");

// Absoluyte path, not scheme or authority.
setBase("/a");
testResolve("/b", "b");
testResolve("/b", "../b");
testResolve("/b", "../../b");
setBase("/a/b");
testResolve("/a/c", "c");
testResolve("/c", "../c");
testResolve("/c", "../../c");

setBase("s://h/p?q#f"); // A simple base.
// Simple references:
testResolve("s2://h2/P?Q#F", "s2://h2/P?Q#F");
testResolve("s://h2/P?Q#F", "//h2/P?Q#F");
Expand All @@ -195,7 +242,7 @@ testUriPerRFCs() {
testResolve("s://h/p?Q#F%20", "?Q#F%20");
testResolve("s://h/p?q#F%20", "#F%20");

base = Uri.parse("s://h/p1/p2/p3"); // A simple base with a path.
setBase("s://h/p1/p2/p3"); // A simple base with a path.
testResolve("s://h/p1/p2/", ".");
testResolve("s://h/p1/p2/", "./");
testResolve("s://h/p1/", "..");
Expand All @@ -206,7 +253,7 @@ testUriPerRFCs() {
testResolve("s://h/", "../../../..");
testResolve("s://h/", "../../../../");

base = Uri.parse("s://h/p?q#f%20"); // A non-simpe base.
setBase("s://h/p?q#f%20"); // A non-simpe base.
// Simple references:
testResolve("s2://h2/P?Q#F", "s2://h2/P?Q#F");
testResolve("s://h2/P?Q#F", "//h2/P?Q#F");
Expand All @@ -222,7 +269,7 @@ testUriPerRFCs() {
testResolve("s://h/p?Q#F%20", "?Q#F%20");
testResolve("s://h/p?q#F%20", "#F%20");

base = Uri.parse("S://h/p1/p2/p3"); // A non-simple base with a path.
setBase("S://h/p1/p2/p3"); // A non-simple base with a path.
testResolve("s://h/p1/p2/", ".");
testResolve("s://h/p1/p2/", "./");
testResolve("s://h/p1/", "..");
Expand All @@ -233,7 +280,7 @@ testUriPerRFCs() {
testResolve("s://h/", "../../../..");
testResolve("s://h/", "../../../../");

base = Uri.parse("../../../"); // A simple relative path.
setBase("../../../"); // A simple relative path.
testResolve("../../../a", "a");
testResolve("../../../../a", "../a");
testResolve("../../../a%20", "a%20");
Expand All @@ -242,8 +289,7 @@ testUriPerRFCs() {
// Tests covering the branches of the merge algorithm in RFC 3986
// with both simple and complex base URIs.
for (var b in ["s://a/pa/pb?q#f", "s://a/pa/pb?q#f%20"]) {
var origBase = Uri.parse(b);
base = origBase;
setBase(b);

// if defined(R.scheme) then ...
testResolve("s2://a2/p2?q2#f2", "s2://a2/p2?q2#f2");
Expand Down Expand Up @@ -271,40 +317,40 @@ testUriPerRFCs() {
// (Cover the merge function and the remove-dot-fragments functions too).

// If base has authority and empty path ...
var emptyPathBase = Uri.parse(b.replaceFirst("/pa/pb", ""));
base = emptyPathBase;
var emptyPathBase = b.replaceFirst("/pa/pb", "");
setBase(emptyPathBase);
testResolve("s://a/p2?q2#f2", "p2?q2#f2");
testResolve("s://a/p2#f2", "p2#f2");
testResolve("s://a/p2", "p2");

base = origBase;
setBase(b);
// otherwise
// (Cover both no authority and non-empty path and both).
var noAuthEmptyPathBase = Uri.parse(b.replaceFirst("//a/pa/pb", ""));
var noAuthAbsPathBase = Uri.parse(b.replaceFirst("//a", ""));
var noAuthRelPathBase = Uri.parse(b.replaceFirst("//a/", ""));
var noAuthRelSinglePathBase = Uri.parse(b.replaceFirst("//a/pa/", ""));
var noAuthEmptyPathBase = b.replaceFirst("//a/pa/pb", "");
var noAuthAbsPathBase = b.replaceFirst("//a", "");
var noAuthRelPathBase = b.replaceFirst("//a/", "");
var noAuthRelSinglePathBase = b.replaceFirst("//a/pa/", "");

testResolve("s://a/pa/p2?q2#f2", "p2?q2#f2");
testResolve("s://a/pa/p2#f2", "p2#f2");
testResolve("s://a/pa/p2", "p2");

base = noAuthEmptyPathBase;
setBase(noAuthEmptyPathBase);
testResolve("s:p2?q2#f2", "p2?q2#f2");
testResolve("s:p2#f2", "p2#f2");
testResolve("s:p2", "p2");

base = noAuthAbsPathBase;
setBase(noAuthAbsPathBase);
testResolve("s:/pa/p2?q2#f2", "p2?q2#f2");
testResolve("s:/pa/p2#f2", "p2#f2");
testResolve("s:/pa/p2", "p2");

base = noAuthRelPathBase;
setBase(noAuthRelPathBase);
testResolve("s:pa/p2?q2#f2", "p2?q2#f2");
testResolve("s:pa/p2#f2", "p2#f2");
testResolve("s:pa/p2", "p2");

base = noAuthRelSinglePathBase;
setBase(noAuthRelSinglePathBase);
testResolve("s:p2?q2#f2", "p2?q2#f2");
testResolve("s:p2#f2", "p2#f2");
testResolve("s:p2", "p2");
Expand All @@ -314,7 +360,7 @@ testUriPerRFCs() {
// A. if input buffer starts with "../" or "./".
// This only happens if base has only a single (may be empty) segment and
// no slash.
base = emptyPathBase;
setBase(emptyPathBase);
testResolve("s://a/p2", "../p2");
testResolve("s://a/", "../");
testResolve("s://a/", "..");
Expand All @@ -324,7 +370,7 @@ testUriPerRFCs() {
testResolve("s://a/p2", "../../p2");
testResolve("s://a/p2", "../../././p2");

base = noAuthRelSinglePathBase;
setBase(noAuthRelSinglePathBase);
testResolve("s:p2", "../p2");
testResolve("s:", "../");
testResolve("s:", "..");
Expand All @@ -337,31 +383,30 @@ testUriPerRFCs() {
// B. if input buffer starts with "/./" or is "/.". replace with "/".
// (The URI implementation removes the "." path segments when parsing,
// so this case isn't handled by merge).
base = origBase;
setBase(b);
testResolve("s://a/pa/p2", "./p2");

// C. if input buffer starts with "/../" or is "/..", replace with "/"
// and remove preceeding segment.
testResolve("s://a/p2", "../p2");
var longPathBase = Uri.parse(b.replaceFirst("/pb", "/pb/pc/pd"));
base = longPathBase;
var longPathBase = b.replaceFirst("/pb", "/pb/pc/pd");
setBase(longPathBase);
testResolve("s://a/pa/pb/p2", "../p2");
testResolve("s://a/pa/p2", "../../p2");
testResolve("s://a/p2", "../../../p2");
testResolve("s://a/p2", "../../../../p2");
var noAuthRelLongPathBase =
Uri.parse(b.replaceFirst("//a/pa/pb", "pa/pb/pc/pd"));
base = noAuthRelLongPathBase;
var noAuthRelLongPathBase = b.replaceFirst("//a/pa/pb", "pa/pb/pc/pd");
setBase(noAuthRelLongPathBase);
testResolve("s:pa/pb/p2", "../p2");
testResolve("s:pa/p2", "../../p2");
testResolve("s:/p2", "../../../p2");
testResolve("s:/p2", "../../../../p2");

// D. if the input buffer contains only ".." or ".", remove it.
base = noAuthEmptyPathBase;
setBase(noAuthEmptyPathBase);
testResolve("s:", "..");
testResolve("s:", ".");
base = noAuthRelSinglePathBase;
setBase(noAuthRelSinglePathBase);
testResolve("s:", "..");
testResolve("s:", ".");
}
Expand Down

0 comments on commit 9bd4406

Please sign in to comment.