Index: sdk/lib/core/uri.dart |
diff --git a/sdk/lib/core/uri.dart b/sdk/lib/core/uri.dart |
index 5cf49b07ff44958a6877187ffa03a5ede3e163d3..3eb81a47b1376db26fec5fb89132465db23255cd 100644 |
--- a/sdk/lib/core/uri.dart |
+++ b/sdk/lib/core/uri.dart |
@@ -381,9 +381,8 @@ class Uri { |
} |
assert(state == NOT_IN_PATH); |
- bool isFile = (scheme == "file"); |
- bool ensureLeadingSlash = host != null; |
- path = _makePath(uri, pathStart, index, null, ensureLeadingSlash, isFile); |
+ bool hasAuthority = (host != null); |
+ path = _makePath(uri, pathStart, index, null, scheme, hasAuthority); |
if (char == _QUESTION) { |
int numberSignIndex = -1; |
@@ -511,9 +510,14 @@ class Uri { |
(userInfo.isNotEmpty || port != null || isFile)) { |
host = ""; |
} |
- bool ensureLeadingSlash = host != null; |
+ bool hasAuthority = (host != null); |
path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
- ensureLeadingSlash, isFile); |
+ scheme, hasAuthority); |
+ if (scheme.isEmpty && host == null && !path.startsWith('/')) { |
+ path = _normalizeRelativePath(path); |
+ } else { |
+ path = _removeDotSegments(path); |
+ } |
return new Uri._internal(scheme, userInfo, host, port, |
path, query, fragment); |
} |
@@ -953,15 +957,15 @@ class Uri { |
host = ""; |
} |
- bool ensureLeadingSlash = (host != null); |
+ bool hasAuthority = host != null; |
if (path != null || pathSegments != null) { |
path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
- ensureLeadingSlash, isFile); |
+ scheme, hasAuthority); |
} else { |
path = this.path; |
- if ((isFile || (ensureLeadingSlash && !path.isEmpty)) && |
+ if ((isFile || (hasAuthority && !path.isEmpty)) && |
!path.startsWith('/')) { |
- path = "/$path"; |
+ path = "/" + path; |
} |
} |
@@ -1025,20 +1029,23 @@ class Uri { |
} |
/** |
- * Returns an URI where the path has been normalized. |
+ * Returns a URI where the path has been normalized. |
* |
* A normalized path does not contain `.` segments or non-leading `..` |
* segments. |
- * Only a relative path may contain leading `..` segments, |
+ * Only a relative path with no scheme or authority may contain |
+ * leading `..` segments, |
* a path that starts with `/` will also drop any leading `..` segments. |
* |
- * This uses the same normalization strategy as [resolveUri], as specified by |
- * RFC 3986. |
+ * This uses the same normalization strategy as `new Uri().resolve(this)`. |
* |
* Does not change any part of the URI except the path. |
+ * |
+ * The default implementation of `Uri` always normalizes paths, so calling |
+ * this function has no effect. |
*/ |
Uri normalizePath() { |
- String path = _removeDotSegments(_path); |
+ String path = _normalizePath(_path, scheme, hasAuthority); |
if (identical(path, _path)) return this; |
return this.replace(path: path); |
} |
@@ -1178,18 +1185,18 @@ class Uri { |
if (!_isAlphabeticCharacter(firstCodeUnit)) { |
_fail(scheme, start, "Scheme not starting with alphabetic character"); |
} |
- bool allLowercase = firstCodeUnit >= _LOWER_CASE_A; |
+ bool containsUpperCase = false; |
for (int i = start; i < end; i++) { |
final int codeUnit = scheme.codeUnitAt(i); |
if (!_isSchemeCharacter(codeUnit)) { |
_fail(scheme, i, "Illegal scheme character"); |
} |
- if (codeUnit < _LOWER_CASE_A || codeUnit > _LOWER_CASE_Z) { |
- allLowercase = false; |
+ if (_UPPER_CASE_A <= codeUnit && codeUnit <= _UPPER_CASE_Z) { |
+ containsUpperCase = true; |
} |
} |
scheme = scheme.substring(start, end); |
- if (!allLowercase) scheme = scheme.toLowerCase(); |
+ if (containsUpperCase) scheme = scheme.toLowerCase(); |
return scheme; |
} |
@@ -1200,8 +1207,10 @@ class Uri { |
static String _makePath(String path, int start, int end, |
Iterable<String> pathSegments, |
- bool ensureLeadingSlash, |
- bool isFile) { |
+ String scheme, |
+ bool hasAuthority) { |
+ bool isFile = (scheme == "file"); |
+ bool ensureLeadingSlash = isFile || hasAuthority; |
if (path == null && pathSegments == null) return isFile ? "/" : ""; |
if (path != null && pathSegments != null) { |
throw new ArgumentError('Both path and pathSegments specified'); |
@@ -1214,13 +1223,25 @@ class Uri { |
} |
if (result.isEmpty) { |
if (isFile) return "/"; |
- } else if ((isFile || ensureLeadingSlash) && |
- result.codeUnitAt(0) != _SLASH) { |
- return "/$result"; |
+ } else if (ensureLeadingSlash && !result.startsWith('/')) { |
+ result = "/" + result; |
} |
+ result = _normalizePath(result, scheme, hasAuthority); |
return result; |
} |
+ /// Performs path normalization (remove dot segments) on a path. |
+ /// |
+ /// If the URI has neither scheme nor authority, it's considered a |
+ /// "pure path" and normalization won't remove leading ".." segments. |
+ /// Otherwise it follows the RFC 3986 "remove dot segments" algorithm. |
+ static String _normalizePath(String path, String scheme, bool hasAuthority) { |
+ if (scheme.isEmpty && !hasAuthority && !path.startsWith('/')) { |
+ return _normalizeRelativePath(path); |
+ } |
+ return _removeDotSegments(path); |
+ } |
+ |
static String _makeQuery(String query, int start, int end, |
Map<String, String> queryParameters) { |
if (query == null && queryParameters == null) return null; |
@@ -1429,8 +1450,7 @@ class Uri { |
*/ |
bool get isAbsolute => scheme != "" && fragment == ""; |
- String _merge(String base, String reference) { |
- if (base.isEmpty) return "/$reference"; |
+ String _mergePaths(String base, String reference) { |
// Optimize for the case: absolute base, reference beginning with "../". |
int backCount = 0; |
int refStart = 0; |
@@ -1463,21 +1483,36 @@ class Uri { |
reference.substring(refStart - 3 * backCount)); |
} |
- bool _hasDotSegments(String path) { |
- if (path.length > 0 && path.codeUnitAt(0) == _DOT) return true; |
+ /// Make a guess at whether a path contains a `..` or `.` segment. |
+ /// |
+ /// This is a primitive test that can cause false positives. |
+ /// It's only used to avoid a more expensive operation in the case where |
+ /// it's not necessary. |
+ static bool _mayContainDotSegments(String path) { |
+ if (path.startsWith('.')) return true; |
int index = path.indexOf("/."); |
return index != -1; |
} |
- String _removeDotSegments(String path) { |
- if (!_hasDotSegments(path)) return path; |
+ /// Removes '.' and '..' segments from a path. |
+ /// |
+ /// Follows the RFC 2986 "remove dot segments" algorithm. |
+ /// This algorithm is only used on paths of URIs with a scheme, |
+ /// and it treats the path as if it is absolute (leading '..' are removed). |
+ static String _removeDotSegments(String path) { |
+ if (!_mayContainDotSegments(path)) return path; |
+ assert(path.isNotEmpty); // An empty path would not have dot segments. |
List<String> output = []; |
bool appendSlash = false; |
for (String segment in path.split("/")) { |
appendSlash = false; |
if (segment == "..") { |
- if (!output.isEmpty && |
- ((output.length != 1) || (output[0] != ""))) output.removeLast(); |
+ if (output.isNotEmpty) { |
+ output.removeLast(); |
+ if (output.isEmpty) { |
+ output.add(""); |
+ } |
+ } |
appendSlash = true; |
} else if ("." == segment) { |
appendSlash = true; |
@@ -1489,6 +1524,42 @@ class Uri { |
return output.join("/"); |
} |
+ /// Removes all `.` segments and any non-leading `..` segments. |
+ /// |
+ /// Removing the ".." from a "bar/foo/.." sequence results in "bar/" |
+ /// (trailing "/"). If the entire path is removed (because it contains as |
+ /// many ".." segments as real segments), the result is "./". |
+ /// This is different from an empty string, which represents "no path", |
+ /// when you resolve it against a base URI with a path with a non-empty |
+ /// final segment. |
+ static String _normalizeRelativePath(String path) { |
+ assert(!path.startsWith('/')); // Only get called for relative paths. |
+ if (!_mayContainDotSegments(path)) return path; |
+ assert(path.isNotEmpty); // An empty path would not have dot segments. |
+ List<String> output = []; |
+ bool appendSlash = false; |
+ for (String segment in path.split("/")) { |
+ appendSlash = false; |
+ if (".." == segment) { |
+ if (!output.isEmpty && output.last != "..") { |
+ output.removeLast(); |
+ appendSlash = true; |
+ } else { |
+ output.add(".."); |
+ } |
+ } else if ("." == segment) { |
+ appendSlash = true; |
+ } else { |
+ output.add(segment); |
+ } |
+ } |
+ if (output.isEmpty || (output.length == 1 && output[0].isEmpty)) { |
+ return "./"; |
+ } |
+ if (appendSlash || output.last == '..') output.add(""); |
+ return output.join("/"); |
+ } |
+ |
/** |
* Resolve [reference] as an URI relative to `this`. |
* |
@@ -1508,9 +1579,15 @@ class Uri { |
* |
* Returns the resolved URI. |
* |
- * The algorithm for resolving a reference is described in |
- * [RFC-3986 Section 5] |
+ * The algorithm "Transform Reference" for resolving a reference is |
+ * described in [RFC-3986 Section 5] |
* (http://tools.ietf.org/html/rfc3986#section-5 "RFC-1123"). |
+ * |
+ * Updated to handle the case where the base URI is just a relative path - |
+ * that is: when it has no scheme or authority and the path does not start |
+ * with a slash. |
+ * In that case, the paths are combined without removing leading "..", and |
+ * an empty path is not converted to "/". |
*/ |
Uri resolveUri(Uri reference) { |
// From RFC 3986. |
@@ -1541,6 +1618,9 @@ class Uri { |
targetPath = _removeDotSegments(reference.path); |
if (reference.hasQuery) targetQuery = reference.query; |
} else { |
+ targetUserInfo = this._userInfo; |
+ targetHost = this._host; |
+ targetPort = this._port; |
if (reference.path == "") { |
targetPath = this._path; |
if (reference.hasQuery) { |
@@ -1549,16 +1629,32 @@ class Uri { |
targetQuery = this._query; |
} |
} else { |
- if (reference.path.startsWith("/")) { |
+ if (reference.hasAbsolutePath) { |
targetPath = _removeDotSegments(reference.path); |
} else { |
- targetPath = _removeDotSegments(_merge(this._path, reference.path)); |
+ // This is the RFC 3986 behavior for merging. |
+ if (this.hasEmptyPath) { |
+ if (!this.hasScheme && !this.hasAuthority) { |
+ // Keep the path relative if no scheme or authority. |
+ targetPath = reference.path; |
+ } else { |
+ // Add path normalization on top of RFC algorithm. |
+ targetPath = _removeDotSegments("/" + reference.path); |
+ } |
+ } else { |
+ var mergedPath = _mergePaths(this._path, reference.path); |
+ if (this.hasScheme || this.hasAuthority || this.hasAbsolutePath) { |
+ targetPath = _removeDotSegments(mergedPath); |
+ } else { |
+ // Non-RFC 3986 beavior. If both base and reference are relative |
+ // path, allow the merged path to start with "..". |
+ // The RFC only specifies the case where the base has a scheme. |
+ targetPath = _normalizeRelativePath(mergedPath); |
+ } |
+ } |
} |
if (reference.hasQuery) targetQuery = reference.query; |
} |
- targetUserInfo = this._userInfo; |
- targetHost = this._host; |
- targetPort = this._port; |
} |
} |
String fragment = reference.hasFragment ? reference.fragment : null; |
@@ -1572,6 +1668,11 @@ class Uri { |
} |
/** |
+ * Returns whether the URI has a [scheme] component. |
+ */ |
+ bool get hasScheme => scheme.isNotEmpty; |
+ |
+ /** |
* Returns whether the URI has an [authority] component. |
*/ |
bool get hasAuthority => _host != null; |
@@ -1597,6 +1698,16 @@ class Uri { |
bool get hasFragment => _fragment != null; |
/** |
+ * Returns whether the URI has an empty path. |
+ */ |
+ bool get hasEmptyPath => _path.isEmpty; |
+ |
+ /** |
+ * Returns whether the URI has an absolute path (starting with '/'). |
+ */ |
+ bool get hasAbsolutePath => _path.startsWith('/'); |
+ |
+ /** |
* Returns the origin of the URI in the form scheme://host:port for the |
* schemes http and https. |
* |