Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(60)

Unified Diff: sdk/lib/core/uri.dart

Issue 2086613003: Add fast-mode Uri class. (Closed) Base URL: https://github.com/dart-lang/sdk.git@master
Patch Set: Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « pkg/analyzer/lib/src/util/fast_uri.dart ('k') | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: sdk/lib/core/uri.dart
diff --git a/sdk/lib/core/uri.dart b/sdk/lib/core/uri.dart
index 5718ca80b83b870dd578011592707b4c7d99cdc4..a89f8d4b445abc4870142572e78b459e8e293a91 100644
--- a/sdk/lib/core/uri.dart
+++ b/sdk/lib/core/uri.dart
@@ -73,6 +73,11 @@ class Uri {
List<String> _pathSegments;
/**
+ * Cache of the full normalized text representation of the URI.
+ */
+ String _text;
+
+ /**
* Cache the computed return value of [queryParameters].
*/
Map<String, String> _queryParameters;
@@ -158,8 +163,8 @@ class Uri {
* general delimiters, are escaped if necessary.
* If `fragment` is omitted or `null`, the URI has no fragment part.
*/
- factory Uri({String scheme : "",
- String userInfo : "",
+ factory Uri({String scheme,
+ String userInfo,
String host,
int port,
String path,
@@ -394,199 +399,192 @@ class Uri {
// query = *( pchar / "/" / "?" )
//
// fragment = *( pchar / "/" / "?" )
- const int EOI = -1;
+ end ??= uri.length;
- String scheme = "";
- String userinfo = "";
- String host = null;
- int port = null;
- String path = null;
- String query = null;
- String fragment = null;
- if (end == null) end = uri.length;
-
- int index = start;
- int pathStart = start;
- // End of input-marker.
- int char = EOI;
-
- void parseAuth() {
- if (index == end) {
- char = EOI;
- return;
- }
- int authStart = index;
- int lastColon = -1;
- int lastAt = -1;
- char = uri.codeUnitAt(index);
- while (index < end) {
- char = uri.codeUnitAt(index);
- if (char == _SLASH || char == _QUESTION || char == _NUMBER_SIGN) {
- break;
- }
- if (char == _AT_SIGN) {
- lastAt = index;
- lastColon = -1;
- } else if (char == _COLON) {
- lastColon = index;
- } else if (char == _LEFT_BRACKET) {
- lastColon = -1;
- int endBracket = uri.indexOf(']', index + 1);
- if (endBracket == -1) {
- index = end;
- char = EOI;
- break;
- } else {
- index = endBracket;
- }
- }
- index++;
- char = EOI;
- }
- int hostStart = authStart;
- int hostEnd = index;
- if (lastAt >= 0) {
- userinfo = _makeUserInfo(uri, authStart, lastAt);
- hostStart = lastAt + 1;
- }
- if (lastColon >= 0) {
- int portNumber;
- if (lastColon + 1 < index) {
- portNumber = 0;
- for (int i = lastColon + 1; i < index; i++) {
- int digit = uri.codeUnitAt(i);
- if (_ZERO > digit || _NINE < digit) {
- _fail(uri, i, "Invalid port number");
- }
- portNumber = portNumber * 10 + (digit - _ZERO);
- }
- }
- port = _makePort(portNumber, scheme);
- hostEnd = lastColon;
- }
- host = _makeHost(uri, hostStart, hostEnd, true);
- if (index < end) {
- char = uri.codeUnitAt(index);
+ // Special case data:URIs. Ignore case when testing.
+ if (end >= start + 5 &&
+ uri.codeUnitAt(start + 4) == _COLON &&
+ uri.codeUnitAt(start) | 0x20 == 0x64 /*d*/ &&
+ uri.codeUnitAt(start + 1) | 0x20 == 0x61 /*a*/ &&
+ uri.codeUnitAt(start + 2) | 0x20 == 0x74 /*t*/ &&
+ uri.codeUnitAt(start + 3) | 0x20 == 0x61 /*a*/) {
+ // Data-URI.
+ if (uri.startsWith("data", start)) {
+ // The case is right.
+ if (start > 0 || end < uri.length) uri = uri.substring(start, end);
+ return UriData._parse(uri, 5, null).uri;
}
+ return Uriata._parse(uri.substring(start + 5, end), 0, null).uri;
floitsch 2016/06/28 00:28:03 UriData I'm guessing you didn't run the tests?
Lasse Reichstein Nielsen 2016/06/28 13:00:51 I did. Turns out we never tested a data URI starti
}
- // When reaching path parsing, the current character is known to not
- // be part of the path.
- const int NOT_IN_PATH = 0;
- // When reaching path parsing, the current character is part
- // of the a non-empty path.
- const int IN_PATH = 1;
- // When reaching authority parsing, authority is possible.
- // This is only true at start or right after scheme.
- const int ALLOW_AUTH = 2;
+ var indices = _scanUri(uri, start, end);
floitsch 2016/06/28 00:28:03 Provide example of what the indices could look lik
Lasse Reichstein Nielsen 2016/06/28 13:00:52 Done.
- // Current state.
- // Initialized to the default value that is used when exiting the
- // scheme loop by reaching the end of input.
- // All other breaks set their own state.
- int state = NOT_IN_PATH;
- int i = index; // Temporary alias for index to avoid bug 19550 in dart2js.
- while (i < end) {
- char = uri.codeUnitAt(i);
- if (char == _QUESTION || char == _NUMBER_SIGN) {
- state = NOT_IN_PATH;
- break;
- }
- if (char == _SLASH) {
- state = (i == start) ? ALLOW_AUTH : IN_PATH;
- break;
+ int schemeEnd = indices[_schemeEndIndex]; // >0 if has scheme
floitsch 2016/06/28 00:28:04 Finish with ".".
Lasse Reichstein Nielsen 2016/06/28 13:00:52 Just removed, it's documented in the _scanUri docs
+ int hostStart = indices[_hostStartIndex] + 1; // >0 if has authority.
+ int portStart = indices[_portStartIndex];
+ int pathStart = indices[_pathStartIndex];
+ int queryStart = indices[_queryStartIndex];
+ int fragmentStart = indices[_fragmentStartIndex];
+
+ // We may discover scheme while handling special cases.
+ String scheme;
+
+ // Derive some indices that weren't set.
+ // If fragment but no query, set query to start at fragment.
+ if (fragmentStart < queryStart) queryStart = fragmentStart;
floitsch 2016/06/28 00:28:03 It's not clear why this wouldn't be done by the _s
Lasse Reichstein Nielsen 2016/06/28 13:00:52 It absolutely could. More to the point, the _scanU
+ // If scheme but no authority, the pathStart isn't set.
+ if (schemeEnd >= start && hostStart <= start) pathStart = schemeEnd + 1;
+ // If scheme or authority but pathStart isn't set.
+ if (pathStart == start && (schemeEnd >= start || hostStart > start)) {
+ pathStart = queryStart;
+ }
+ // If authority and no port.
+ // (including when user-info contains : and portStart >= 0).
+ if (portStart < hostStart) portStart = pathStart;
+
+ bool isSimple = indices[_notSimpleIndex] < start;
+ if (isSimple) {
+ // Check/do normalizations that weren't detected by the scanner.
+ // This includes removal of empty port or userInfo,
+ // or scheme specific port and path normalizations.
+ if (hostStart > schemeEnd + 3) {
+ // Always be non-simple if URI contains user-info.
+ isSimple = false;
}
floitsch 2016/06/28 00:28:03 Is it necessary to enter the 'else' below? if not
Lasse Reichstein Nielsen 2016/06/28 13:00:52 Done.
- if (char == _COLON) {
- if (i == start) _fail(uri, start, "Invalid empty scheme");
- scheme = _makeScheme(uri, start, i);
- i++;
- if (scheme == "data") {
- // This generates a URI that is (potentially) not path normalized.
- // Applying part normalization to a non-hierarchial URI isn't
- // meaningful.
- return UriData._parse(uri, i, null).uri;
- }
- pathStart = i;
- if (i == end) {
- char = EOI;
- state = NOT_IN_PATH;
- } else {
- char = uri.codeUnitAt(i);
- if (char == _QUESTION || char == _NUMBER_SIGN) {
- state = NOT_IN_PATH;
- } else if (char == _SLASH) {
- state = ALLOW_AUTH;
- } else {
- state = IN_PATH;
+ if (portStart > start && portStart + 1 == pathStart) {
+ // If the port is empty, it should be omitted.
+ // Path case, don't bother correcting it.
+ isSimple = false;
+ } else if (hostStart == schemeEnd + 4) {
+ // If the userInfo is empty, it should be omitted.
+ // (4 is length of "://@").
+ // Pathological case, don't bother correcting it.
+ isSimple = false;
+ } else {
+ // There are a few scheme-based normalizations that
+ // the scanner couldn't check.
+ // That means that the input is very close to simple, so just do
+ // the normalizations.
+ if (schemeEnd == start + 4) {
+ // Do scheme based normalizations for file, http.
+ if (uri.startsWith("file", start)) {
+ scheme = "file";
+ if (hostStart <= 0) {
+ // File URIs should have an authority.
+ // Paths after an authority should be absolute.
+ String insert = "//";
+ int delta = 2;
+ if (!uri.startsWith("/", pathStart)) {
+ insert = "///";
+ delta = 3;
+ }
+ uri = "${uri.substring(start, schemeEnd + 1)}$insert"
+ "${uri.substring(pathStart, end)}";
+ schemeEnd -= start;
+ pathStart += 2 - start;
+ hostStart = schemeEnd + 3;
+ portStart = pathStart;
+ queryStart += delta;
+ fragmentStart += delta;
+ start = 0;
+ end = uri.length;
+ } else if (pathStart == queryStart) {
+ uri = "${uri.substring(start, pathStart)}/"
+ "${uri.substring(pathStart, end)}";
+ queryStart += 1;
+ fragmentStart += 1;
+ }
+ } else if (uri.startsWith("http", start)) {
+ scheme = "http";
+ // Http URIs should not have an explicit port of 80
floitsch 2016/06/28 00:28:03 Finish with ".".
Lasse Reichstein Nielsen 2016/06/28 13:00:51 Done.
+ if (portStart > start && portStart + 3 == pathStart &&
+ uri.startsWith("80", portStart + 1)) {
+ uri = uri.substring(start, portStart) +
+ uri.substring(pathStart, end);
+ schemeEnd -= start;
+ hostStart -= start;
+ portStart -= start;
+ pathStart -= 3 + start;
+ queryStart -= 3 + start;
+ fragmentStart -= 3 + start;
+ start = 0;
+ end = uri.length;
+ }
+ }
+ } else if (schemeEnd == start + 5 && uri.startsWith("https", start)) {
+ scheme = "https";
+ // Https URIs should not have an explicit port of 443
+ if (portStart > start && portStart + 4 == pathStart &&
+ uri.startsWith("443", portStart + 1)) {
+ uri = uri.substring(start, portStart) +
+ uri.substring(pathStart, end);
+ schemeEnd -= start;
+ hostStart -= start;
+ portStart -= start;
+ pathStart -= 4 + start;
+ queryStart -= 4 + start;
+ fragmentStart -= 4 + start;
+ start = 0;
+ end = uri.length;
}
}
- break;
}
- i++;
- char = EOI;
}
- index = i; // Remove alias when bug is fixed.
- if (state == ALLOW_AUTH) {
- assert(char == _SLASH);
- // Have seen one slash either at start or right after scheme.
- // If two slashes, it's an authority, otherwise it's just the path.
- index++;
- if (index == end) {
- char = EOI;
- state = NOT_IN_PATH;
- } else {
- char = uri.codeUnitAt(index);
- if (char == _SLASH) {
- index++;
- parseAuth();
- pathStart = index;
- }
- if (char == _QUESTION || char == _NUMBER_SIGN || char == EOI) {
- state = NOT_IN_PATH;
- } else {
- state = IN_PATH;
+ if (isSimple) {
+ if (start > 0 || end < uri.length) {
+ uri = uri.substring(start, end);
+ if (schemeEnd >= 0) schemeEnd -= start;
+ if (hostStart > 0) {
+ hostStart -= start;
+ portStart -= start;
}
+ pathStart -= start;
+ queryStart -= start;
+ fragmentStart -= start;
}
+ return new _SimpleUri(uri, schemeEnd, hostStart, portStart, pathStart,
+ queryStart, fragmentStart, scheme);
+
}
- assert(state == IN_PATH || state == NOT_IN_PATH);
- if (state == IN_PATH) {
- // Characters from pathStart to index (inclusive) are known
- // to be part of the path.
- while (++index < end) {
- char = uri.codeUnitAt(index);
- if (char == _QUESTION || char == _NUMBER_SIGN) {
- break;
- }
- char = EOI;
+ if (scheme == null) {
+ scheme = "";
+ if (schemeEnd > start) {
+ scheme = _makeScheme(uri, start, schemeEnd);
+ } else if (schemeEnd == start) {
+ _fail(uri, start, "Invalid empty scheme");
}
- state = NOT_IN_PATH;
}
-
- assert(state == NOT_IN_PATH);
- bool hasAuthority = (host != null);
- path = _makePath(uri, pathStart, index, null, scheme, hasAuthority);
-
- if (char == _QUESTION) {
- int numberSignIndex = -1;
- for (int i = index + 1; i < end; i++) {
- if (uri.codeUnitAt(i) == _NUMBER_SIGN) {
- numberSignIndex = i;
- break;
- }
+ String userInfo = "";
+ String host;
+ int port;
+ if (hostStart > start) {
+ int userInfoStart = schemeEnd + 3;
+ if (userInfoStart < hostStart) {
+ userInfo = _makeUserInfo(uri, userInfoStart, hostStart - 1);
}
- if (numberSignIndex < 0) {
- query = _makeQuery(uri, index + 1, end, null);
- } else {
- query = _makeQuery(uri, index + 1, numberSignIndex, null);
- fragment = _makeFragment(uri, numberSignIndex + 1, end);
+ host = _makeHost(uri, hostStart, portStart, false);
+ if (portStart + 1 < pathStart) {
+ // Should throw because invalid.
+ port = int.parse(uri.substring(portStart + 1, pathStart), onError: (_) {
+ throw new FormatException("Invalid port", uri, portStart + 1);
+ });
+ port = _makePort(port, scheme);
}
- } else if (char == _NUMBER_SIGN) {
- fragment = _makeFragment(uri, index + 1, end);
+ }
+ String path = _makePath(uri, pathStart, queryStart, null,
+ scheme, host != null);
+ String query;
+ if (queryStart < fragmentStart) {
+ query = _makeQuery(uri, queryStart + 1, fragmentStart, null);
+ }
+ String fragment;
+ if (fragmentStart < end) {
+ fragment = _makeFragment(uri, fragmentStart + 1, end);
}
return new Uri._internal(scheme,
- userinfo,
+ userInfo,
host,
port,
path,
@@ -1333,6 +1331,17 @@ class Uri {
}
scheme = scheme.substring(start, end);
if (containsUpperCase) scheme = scheme.toLowerCase();
+ return _canonicalizeScheme(scheme);
+ }
+
+ // Canonicalize a few often-used scheme strings.
+ //
+ // This improves memory usage and makes comparison faster.
+ static String _canonicalizeScheme(String scheme) {
+ if (scheme == "http") return "http";
+ if (scheme == "file") return "file";
+ if (scheme == "https") return "https";
+ if (scheme == "package") return "package";
return scheme;
}
@@ -1932,6 +1941,8 @@ class Uri {
* If the URI cannot be converted to a file path calling this throws
* [UnsupportedError].
*/
+ // TODO(lrn): Deprecate and move functionality to File class or similar.
+ // The core libraries should not worry about the platform.
String toFilePath({bool windows}) {
if (scheme != "" && scheme != "file") {
throw new UnsupportedError(
@@ -1946,25 +1957,27 @@ class Uri {
"Cannot extract a file path from a URI with a fragment component");
}
if (windows == null) windows = _isWindows;
- return windows ? _toWindowsFilePath() : _toFilePath();
+ return windows ? _toWindowsFilePath(this) : _toFilePath();
}
String _toFilePath() {
- if (host != "") {
+ if (hasAuthority && host != "") {
throw new UnsupportedError(
"Cannot extract a non-Windows file path from a file URI "
"with an authority");
}
+ // Use path segments to have any escapes unescaped.
+ var pathSegments = this.pathSegments;
_checkNonWindowsPathReservedCharacters(pathSegments, false);
var result = new StringBuffer();
- if (_isPathAbsolute) result.write("/");
+ if (hasAbsolutePath) result.write("/");
result.writeAll(pathSegments, "/");
return result.toString();
}
- String _toWindowsFilePath() {
+ static String _toWindowsFilePath(Uri uri) {
bool hasDriveLetter = false;
- var segments = pathSegments;
+ var segments = uri.pathSegments;
if (segments.length > 0 &&
segments[0].length == 2 &&
segments[0].codeUnitAt(1) == _COLON) {
@@ -1972,23 +1985,25 @@ class Uri {
_checkWindowsPathReservedCharacters(segments, false, 1);
hasDriveLetter = true;
} else {
- _checkWindowsPathReservedCharacters(segments, false);
+ _checkWindowsPathReservedCharacters(segments, false, 0);
}
var result = new StringBuffer();
- if (_isPathAbsolute && !hasDriveLetter) result.write("\\");
- if (host != "") {
- result.write("\\");
- result.write(host);
- result.write("\\");
+ if (uri.hasAbsolutePath && !hasDriveLetter) result.write(r"\");
+ if (uri.hasAuthority) {
+ var host = uri.host;
+ if (host.isNotEmpty) {
+ result.write(r"\");
+ result.write(host);
+ result.write(r"\");
+ }
}
- result.writeAll(segments, "\\");
- if (hasDriveLetter && segments.length == 1) result.write("\\");
+ result.writeAll(segments, r"\");
+ if (hasDriveLetter && segments.length == 1) result.write(r"\");
return result.toString();
}
bool get _isPathAbsolute {
- if (path == null || path.isEmpty) return false;
- return path.startsWith('/');
+ return _path != null && _path.startsWith('/');
}
void _writeAuthority(StringSink ss) {
@@ -2014,8 +2029,9 @@ class Uri {
UriData get data => (scheme == "data") ? new UriData.fromUri(this) : null;
String toString() {
+ if (_text != null) return _text;
StringBuffer sb = new StringBuffer();
- _addIfNonEmpty(sb, scheme, scheme, ':');
+ if (scheme.isNotEmpty) sb..write(scheme)..write(":");
if (hasAuthority || path.startsWith("//") || (scheme == "file")) {
// File URIS always have the authority, even if it is empty.
// The empty URI means "localhost".
@@ -2023,9 +2039,10 @@ class Uri {
_writeAuthority(sb);
}
sb.write(path);
- if (_query != null) { sb..write("?")..write(_query); }
- if (_fragment != null) { sb..write("#")..write(_fragment); }
- return sb.toString();
+ if (_query != null) sb..write("?")..write(_query);
+ if (_fragment != null) sb..write("#")..write(_fragment);
+ _text = sb.toString();
+ return _text;
}
bool operator==(other) {
@@ -2044,20 +2061,7 @@ class Uri {
}
int get hashCode {
- int combine(part, current) {
- // The sum is truncated to 30 bits to make sure it fits into a Smi.
- return (current * 31 + part.hashCode) & 0x3FFFFFFF;
- }
- return combine(scheme, combine(userInfo, combine(host, combine(port,
- combine(path, combine(query, combine(fragment, 1)))))));
- }
-
- static void _addIfNonEmpty(StringBuffer sb, String test,
- String first, String second) {
- if ("" != test) {
- sb.write(first);
- sb.write(second);
- }
+ return (_text ?? toString()).hashCode;
}
/**
@@ -3359,3 +3363,713 @@ class UriData {
// This is the same characters as in a URI query (which is URI pchar plus '?')
static const _uricTable = Uri._queryCharTable;
}
+
+// --------------------------------------------------------------------
+// Constants used to read the scanner result.
+
+const int _nopIndex = 0;
+const int _schemeEndIndex = 1;
+const int _hostStartIndex = 2;
+const int _portStartIndex = 3;
+const int _pathStartIndex = 4;
+const int _queryStartIndex = 5;
+const int _fragmentStartIndex = 6;
+const int _notSimpleIndex = 7;
+
+// Initial state for scanner.
+const int _start = 00;
+
+final _scannerTables = _createTables();
+
+// ----------------------------------------------------------------------
+// Code to create the URI scanner table.
+
+const int _schemeOrPath = 01;
+const int _authOrPath = 02;
+const int _authOrPathSlash = 03;
+const int _uinfoOrHost0 = 04;
+const int _uinfoOrHost = 05;
+const int _uinfoOrPort0 = 06;
+const int _uinfoOrPort = 07;
+const int _ipv6Host = 08;
+const int _relPathSeg = 09;
+const int _pathSeg = 10;
+const int _path = 11;
+const int _query = 12;
+const int _fragment = 13;
+const int _schemeOrPathDot = 14;
+const int _schemeOrPathDot2 = 15;
+const int _relPathSegDot = 16;
+const int _relPathSegDot2 = 17;
+const int _pathSegDot = 18;
+const int _pathSegDot2 = 19;
+
+const int _scheme0 = 20;
+const int _scheme = 21;
+
+const int _stateCount = 22;
+const int _nonSimpleEndStates = 14;
floitsch 2016/06/28 00:28:04 This needs a comment and should be separated from
Lasse Reichstein Nielsen 2016/06/28 13:00:52 Commented and moved. Most of the states have been
+
+const int _nop = _nopIndex << 5;
+const int _schemeEnd = _schemeEndIndex << 5;
+const int _hostStart = _hostStartIndex << 5;
+const int _portStart = _portStartIndex << 5;
+const int _pathStart = _pathStartIndex << 5;
+const int _queryStart = _queryStartIndex << 5;
+const int _fragmentStart = _fragmentStartIndex << 5;
+const int _notSimple = _notSimpleIndex << 5;
+
+
+void _setChars(Uint8List target, String chars, int value) {
floitsch 2016/06/28 00:28:03 Needs dartdocs.
Lasse Reichstein Nielsen 2016/06/28 13:00:51 Done.
+ for (int i = 0; i < chars.length; i++) {
+ var char = chars.codeUnitAt(i);
+ target[char ^ 0x60] = value;
floitsch 2016/06/28 00:28:04 Needs explanation what the ^ 0x60 does.
Lasse Reichstein Nielsen 2016/06/28 13:00:52 Now described on the _scannerTables declaration.
+ }
+}
+
+void _setRange(Uint8List target, String range, int value) {
floitsch 2016/06/28 00:28:03 Needs dartdocs.
Lasse Reichstein Nielsen 2016/06/28 13:00:51 Done.
+ for (int i = range.codeUnitAt(0), n = range.codeUnitAt(1); i <= n; i++) {
+ target[i ^ 0x60] = value;
+ }
+}
+
+List<Uint8List> _createTables() {
floitsch 2016/06/28 00:28:04 Needs documentation.
Lasse Reichstein Nielsen 2016/06/28 13:00:52 Done.
+ // TODO(lrn): Use a precomputed table.
+ var tables = new List<Uint8List>.generate(_stateCount,
+ (_) => new Uint8List(96));
+
+ const unreserved =
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~" ;
+ const subDelims = r"!$&'()*+,;=";
+ const pchar = "$unreserved$subDelims";
floitsch 2016/06/28 00:28:03 what does the "p" stand for?
Lasse Reichstein Nielsen 2016/06/28 13:00:52 This refers to the "pchar" character group in RFC
+
+ build(index, defaultValue) =>
+ tables[index]..fillRange(0, 96, defaultValue);
+
+ var b;
+ // Validate as path, if it is a scheme, we handle it later.
+ b = build(_start, _schemeOrPath | _notSimple);
+ _setChars(b, pchar, _schemeOrPath);
+ _setChars(b, ".", _schemeOrPathDot);
+ _setChars(b, "%", _schemeOrPath | _notSimple);
+ _setChars(b, ":", _authOrPath | _schemeEnd); // Handle later.
+ _setChars(b, "/", _authOrPathSlash);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_schemeOrPathDot, _schemeOrPath | _notSimple);
+ _setChars(b, pchar, _schemeOrPath);
+ _setChars(b, ".", _schemeOrPathDot2);
+ _setChars(b, "%", _schemeOrPath | _notSimple);
+ _setChars(b, ':', _authOrPath | _schemeEnd);
+ _setChars(b, "/", _pathSeg | _notSimple);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_schemeOrPathDot2, _schemeOrPath | _notSimple);
+ _setChars(b, pchar, _schemeOrPath);
+ _setChars(b, "%", _schemeOrPath | _notSimple);
+ _setChars(b, ':', _authOrPath | _schemeEnd);
+ _setChars(b, "/", _relPathSeg);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_schemeOrPath, _schemeOrPath | _notSimple);
+ _setChars(b, pchar, _schemeOrPath);
+ _setChars(b, ':', _authOrPath | _schemeEnd);
+ _setChars(b, "/", _pathSeg);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_authOrPath, _path | _notSimple);
+ _setChars(b, pchar, _path);
+ _setChars(b, "/", _authOrPathSlash);
+ _setChars(b, ".", _pathSegDot);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_authOrPathSlash, _path | _notSimple);
+ _setChars(b, pchar, _path);
+ _setChars(b, "/", _uinfoOrHost0 | _hostStart);
+ _setChars(b, ".", _pathSegDot);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_uinfoOrHost0, _uinfoOrHost | _notSimple);
+ _setChars(b, pchar, _uinfoOrHost);
+ _setRange(b, "AZ", _uinfoOrHost | _notSimple);
+ _setChars(b, ":", _uinfoOrPort0 | _portStart);
+ _setChars(b, "@", _uinfoOrHost0 | _hostStart);
+ _setChars(b, "[", _ipv6Host | _notSimple);
+ _setChars(b, "/", _pathSeg | _pathStart);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_uinfoOrHost, _uinfoOrHost | _notSimple);
+ _setChars(b, pchar, _uinfoOrHost);
+ _setRange(b, "AZ", _uinfoOrHost | _notSimple);
+ _setChars(b, ":", _uinfoOrPort0 | _portStart);
+ _setChars(b, "@", _uinfoOrHost0 | _hostStart);
+ _setChars(b, "/", _pathSeg | _pathStart);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_uinfoOrPort0, _uinfoOrPort | _notSimple);
+ _setChars(b, "0", _uinfoOrPort | _notSimple);
+ _setRange(b, "19", _uinfoOrPort);
+ _setChars(b, "@", _uinfoOrHost0 | _hostStart);
+ _setChars(b, "/", _pathSeg | _pathStart);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_uinfoOrPort, _uinfoOrPort | _notSimple);
+ _setRange(b, "09", _uinfoOrPort);
+ _setChars(b, "@", _uinfoOrHost0 | _hostStart);
+ _setChars(b, "/", _pathSeg | _pathStart);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_ipv6Host, _ipv6Host);
+ _setChars(b, "]", _uinfoOrHost);
+
+ b = build(_relPathSeg, _path | _notSimple);
+ _setChars(b, pchar, _path);
+ _setChars(b, ".", _relPathSegDot);
+ _setChars(b, "/", _pathSeg | _notSimple);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_relPathSegDot, _path | _notSimple);
+ _setChars(b, pchar, _path);
+ _setChars(b, ".", _relPathSegDot2);
+ _setChars(b, "/", _pathSeg | _notSimple);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_relPathSegDot2, _path | _notSimple);
+ _setChars(b, pchar, _path);
+ _setChars(b, ".", _path);
+ _setChars(b, "/", _relPathSeg);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_pathSeg, _path | _notSimple);
+ _setChars(b, pchar, _path);
+ _setChars(b, ".", _pathSegDot);
+ _setChars(b, "/", _pathSeg | _notSimple);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_pathSegDot, _path | _notSimple);
+ _setChars(b, pchar, _path);
+ _setChars(b, ".", _pathSegDot2);
+ _setChars(b, "/", _pathSeg | _notSimple);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_pathSegDot2, _path | _notSimple);
+ _setChars(b, pchar, _path);
+ _setChars(b, ".", _path);
+ _setChars(b, "/", _pathSeg | _notSimple);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_path, _path | _notSimple);
+ _setChars(b, pchar, _path);
+ _setChars(b, "/", _pathSeg);
+ _setChars(b, "?", _query | _queryStart);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_query, _query | _notSimple);
+ _setChars(b, pchar, _query);
+ _setChars(b, "?", _query);
+ _setChars(b, "#", _fragment | _fragmentStart);
+
+ b = build(_fragment, _fragment | _notSimple);
+ _setChars(b, pchar, _fragment);
+ _setChars(b, "?", _fragment);
+
+ // A separate two-state validator for scheme names.
+ // Only accepts lower-case letters.
+ b = build(_scheme0, _scheme | _notSimple);
+ _setRange(b, "az", _scheme);
+
+ b = build(_scheme, _scheme | _notSimple);
+ _setRange(b, "az", _scheme);
+ _setRange(b, "09", _scheme);
+ _setChars(b, "+-.", _scheme);
+
+ return tables;
+}
+
+// --------------------------------------------------------------------
+// Code that uses the URI scanner table.
+
+int _scan(String uri, int start, int end, int state, List<int> indices) {
+ var tables = _scannerTables;
+ assert(end <= uri.length);
+ for (int i = start; i < end; i++) {
+ var table = tables[state];
+ // xor 0x60 to move range 0x20-0x7f into 0x00-0x5f
+ int char = uri.codeUnitAt(i) ^ 0x60;
+ // use 0x1f (nee 0x7f) to represent all unhandled characters.
floitsch 2016/06/28 00:28:03 Use
Lasse Reichstein Nielsen 2016/06/28 13:00:52 Done.
+ if (char > 0x5f) char = 0x1f; // TODO: check if negating test is better.
floitsch 2016/06/28 00:28:03 TODO(ldap)
Lasse Reichstein Nielsen 2016/06/28 13:00:52 Or check it. It isn't faster.
+ int transition = table[char];
+ state = transition & 0x1f;
+ indices[transition >> 5] = i;
+ }
+ return state;
+}
+
+List<int> _scanUri(String uri, int start, int end) {
+ var indices = new List<int>.filled(8, start - 1);
+ indices[_portStartIndex] = start; // equals effective pathStart if no auth.
floitsch 2016/06/28 00:28:04 "Equals"
Lasse Reichstein Nielsen 2016/06/28 13:00:51 Done.
+ indices[_pathStartIndex] = start;
+ indices[_queryStartIndex] = end;
+ indices[_fragmentStartIndex] = end;
+ var state = _scan(uri, start, end, _start, indices);
+ // Some states that should be non-simple, but the URI ended early.
+ if (state >= _nonSimpleEndStates) {
+ indices[_notSimpleIndex] = end;
+ }
+ int schemeEnd = indices[_schemeEndIndex];
+ if (schemeEnd >= start) {
+ // Rescan the scheme part now that we know it's not a path.
+ state = _scan(uri, start, schemeEnd, _scheme0, indices);
+ if (state == _scheme0) {
+ // Empty scheme.
+ indices[_notSimpleIndex] = schemeEnd;
+ }
+ }
+ return indices;
+}
+
+bool _isUpperCase(int char) {
floitsch 2016/06/28 00:28:04 unused?
Lasse Reichstein Nielsen 2016/06/28 13:00:52 Indeed. Gone.
+ return 0x41 <= char && char <= 0x5b;
+}
+
+class _SimpleUri implements Uri {
+ final String _uri;
+ final int _schemeEnd;
+ final int _hostStart;
+ final int _portStart;
+ final int _pathStart;
+ final int _queryStart;
+ final int _fragmentStart;
+ /// The scheme is often used to distinguish URIs.
+ /// To make comparisons more efficient, we cache the value, and
+ /// canonicalize a few known types.
+ String _schemeCache;
+
+ _SimpleUri(
+ this._uri,
+ this._schemeEnd,
+ this._hostStart,
+ this._portStart,
+ this._pathStart,
+ this._queryStart,
+ this._fragmentStart,
+ this._schemeCache);
+
+ bool get hasScheme => _schemeEnd > 0;
+ bool get hasAuthority => _hostStart > 0;
+ bool get hasUserInfo => _hostStart > _schemeEnd + 4;
+ bool get hasPort => _hostStart > 0 && _portStart + 1 < _pathStart;
+ bool get hasQuery => _queryStart < _fragmentStart;
+ bool get hasFragment => _fragmentStart < _uri.length;
+
+ bool get _isFile => _schemeEnd == 4 && _uri.startsWith("file");
+ bool get _isHttp => _schemeEnd == 4 && _uri.startsWith("http");
+ bool get _isHttps => _schemeEnd == 5 && _uri.startsWith("https");
+ bool get _isPackage => _schemeEnd == 7 && _uri.startsWith("package");
+ bool _isScheme(String scheme) =>
+ _schemeEnd == scheme.length && _uri.startsWith(scheme);
+
+ bool get hasAbsolutePath => _uri.startsWith("/", _pathStart);
+ bool get hasEmptyPath => _pathStart == _queryStart;
+
+ bool get isAbsolute => hasScheme && !hasFragment;
+
+ String get scheme {
+ if (_schemeEnd <= 0) return "";
+ if (_schemeCache != null) return _schemeCache;
+ if (_isHttp) {
+ _schemeCache = "http";
+ } else if (_isHttps) {
+ _schemeCache = "https";
+ } else if (_isFile) {
+ _schemeCache = "file";
+ } else if (_isPackage) {
+ _schemeCache = "package";
+ } else {
+ _schemeCache = _uri.substring(0, _schemeEnd);
+ }
+ return _schemeCache;
+ }
+ String get authority => _hostStart > 0 ?
+ _uri.substring(_schemeEnd + 3, _pathStart) : "";
+ String get userInfo => (_hostStart > _schemeEnd + 3) ?
+ _uri.substring(_schemeEnd + 3, _hostStart - 1) : "";
+ String get host =>
+ _hostStart > 0 ? _uri.substring(_hostStart, _portStart) : "";
+ int get port {
+ if (hasPort) return int.parse(_uri.substring(_portStart + 1, _pathStart));
+ if (_isHttp) return 80;
+ if (_isHttps) return 443;
+ return 0;
+ }
+ String get path =>_uri.substring(_pathStart, _queryStart);
+ String get query => (_queryStart < _fragmentStart) ?
+ _uri.substring(_queryStart + 1, _fragmentStart) : "";
+ String get fragment => (_fragmentStart < _uri.length) ?
+ _uri.substring(_fragmentStart + 1) : "";
+
+ String get origin {
+ // Check original behavior - W3C spec is wonky!
+ bool isHttp = _isHttp;
+ if (_schemeEnd < 0 || _hostStart == _portStart) {
+ throw new StateError("Cannot use origin without a scheme: $this");
+ }
+ if (!isHttp && !_isHttps) {
+ throw new StateError(
+ "Origin is only applicable schemes http and https: $this");
+ }
+ if (_hostStart == _schemeEnd + 3) {
+ return _uri.substring(0, _pathStart);
+ }
+ // Need to drop anon-empty userInfo.
+ return _uri.substring(0, _schemeEnd + 3) +
+ _uri.substring(_hostStart, _pathStart);
+ }
+
+ List<String> get pathSegments {
+ int start = _pathStart;
+ int end = _queryStart;
+ if (_uri.startsWith("/", start)) start++;
+ if (start == end) return const <String>[];
+ List<String> parts = [];
+ for (int i = start; i < end; i++) {
+ var char = _uri.codeUnitAt(i);
+ if (char == Uri._SLASH) {
+ parts.add(_uri.substring(start, i));
+ start = i + 1;
+ }
+ }
+ parts.add(_uri.substring(start, end));
+ return new List<String>.unmodifiable(parts);
+ }
+
+ Map<String, String> get queryParameters {
+ if (!hasQuery) return const <String, String>{};
+ return new UnmodifiableMapView<String, String>(
+ Uri.splitQueryString(query));
+ }
+
+ Map<String, List<String>> get queryParametersAll {
+ if (!hasQuery) return const <String, List<String>>{};
+ Map queryParameterLists = Uri._splitQueryStringAll(query);
+ for (var key in queryParameterLists.keys) {
+ queryParameterLists[key] =
+ new List<String>.unmodifiable(queryParameterLists[key]);
+ }
+ return new Map<String, List<String>>.unmodifiable(queryParameterLists);
+ }
+
+ bool _isPort(String port) {
+ int portDigitStart = _portStart + 1;
+ return portDigitStart + port.length == _pathStart &&
+ _uri.startsWith(port, portDigitStart);
+ }
+
+ Uri normalizePath() => this;
+
+ Uri removeFragment() {
+ if (!hasFragment) return this;
+ return new _SimpleUri(
+ _uri.substring(0, _fragmentStart),
+ _schemeEnd, _hostStart, _portStart,
+ _pathStart, _queryStart, _fragmentStart, _schemeCache);
+ }
+
+ Uri replace({String scheme,
+ String userInfo,
+ String host,
+ int port,
+ String path,
+ Iterable<String> pathSegments,
+ String query,
+ Map<String, dynamic/*String|Iterable<String>*/> queryParameters,
+ String fragment}) {
+ bool schemeChanged = false;
+ if (scheme != null) {
+ scheme = Uri._makeScheme(scheme, 0, scheme.length);
+ schemeChanged = !_isScheme(scheme);
+ } else {
+ scheme = this.scheme;
+ }
+ bool isFile = (scheme == "file");
+ if (userInfo != null) {
+ userInfo = Uri._makeUserInfo(userInfo, 0, userInfo.length);
+ } else if (_hostStart > 0) {
+ userInfo = _uri.substring(_schemeEnd + 3, _hostStart);
+ } else {
+ userInfo = "";
+ }
+ if (port != null) {
+ port = Uri._makePort(port, scheme);
+ } else {
+ port = this.hasPort ? this.port : null;
+ if (schemeChanged) {
+ // The default port might have changed.
+ port = Uri._makePort(port, scheme);
+ }
+ }
+ if (host != null) {
+ host = Uri._makeHost(host, 0, host.length, false);
+ } else if (_hostStart > 0) {
+ host = _uri.substring(_hostStart, _portStart);
+ } else if (userInfo.isNotEmpty || port != null || isFile) {
+ host = "";
+ }
+
+ bool hasAuthority = host != null;
+ if (path != null || pathSegments != null) {
+ path = Uri._makePath(path, 0, _stringOrNullLength(path), pathSegments,
+ scheme, hasAuthority);
+ } else {
+ path = _uri.substring(_pathStart, _queryStart);
+ if ((isFile || (hasAuthority && !path.isEmpty)) &&
+ !path.startsWith('/')) {
+ path = "/" + path;
+ }
+ }
+
+ if (query != null || queryParameters != null) {
+ query =
+ Uri._makeQuery(query, 0, _stringOrNullLength(query), queryParameters);
+ } else if (_queryStart < _fragmentStart) {
+ query = _uri.substring(_queryStart, _fragmentStart);
+ }
+
+ if (fragment != null) {
+ fragment = Uri._makeFragment(fragment, 0, fragment.length);
+ } else if (_fragmentStart < _uri.length) {
+ fragment = _uri.substring(_fragmentStart);
+ }
+
+ return new Uri._internal(
+ scheme, userInfo, host, port, path, query, fragment);
+ }
+
+ Uri resolve(String reference) {
+ return resolveUri(Uri.parse(reference));
+ }
+
+ Uri resolveUri(Uri reference) {
+ if (reference is _SimpleUri) {
+ return _simpleMerge(this, reference);
+ }
+ return _toNonSimple().resolveUri(reference);
+ }
+
+ // Merge two simple URIs. This should always result in a prefix of
+ // one concatentated with a suffix of the other, which is again simple.
+ // In a few cases, there might be a need for extra normalization, when
+ // resolving on top of a known scheme.
+ Uri _simpleMerge(_SimpleUri base, _SimpleUri ref) {
+ if (ref.hasScheme) return ref;
+ if (ref.hasAuthority) {
+ if (!base.hasScheme) return ref;
+ bool isSimple = true;
+ if (base._isFile) {
+ isSimple = !ref.hasEmptyPath;
+ } else if (base._isHttp) {
+ isSimple = !ref._isPort("80");
+ } else if (base._isHttps) {
+ isSimple = !ref._isPort("443");
+ }
+ if (isSimple) {
+ var delta = base._schemeEnd + 1;
+ var newUri = base._uri.substring(0, base._schemeEnd + 1) +
+ ref._uri.substring(ref._schemeEnd + 1);
+ return new _SimpleUri(newUri,
+ base._schemeEnd,
+ ref._hostStart + delta,
+ ref._portStart + delta,
+ ref._pathStart + delta,
+ ref._queryStart + delta,
+ ref._fragmentStart + delta,
+ base._schemeCache);
+ } else {
+ // Slowcase.
+ return _toNonSimple().resolveUri(ref);
+ }
+ }
+ if (ref.hasEmptyPath) {
+ if (ref.hasQuery) {
+ int delta = base._queryStart - ref._queryStart;
+ var newUri = base._uri.substring(0, base._queryStart) +
+ ref._uri.substring(ref._queryStart);
+ return new _SimpleUri(newUri,
+ base._schemeEnd,
+ base._hostStart,
+ base._portStart,
+ base._pathStart,
+ ref._queryStart + delta,
+ ref._fragmentStart + delta,
+ base._schemeCache);
+ }
+ if (ref.hasFragment) {
+ int delta = base._fragmentStart - ref._fragmentStart;
+ var newUri = base._uri.substring(0, base._fragmentStart) +
+ ref._uri.substring(ref._fragmentStart);
+ return new _SimpleUri(newUri,
+ base._schemeEnd,
+ base._hostStart,
+ base._portStart,
+ base._pathStart,
+ base._queryStart,
+ ref._fragmentStart + delta,
+ base._schemeCache);
+ }
+ return base.removeFragment();
+ }
+ if (ref.hasAbsolutePath) {
+ var delta = base._pathStart - ref._pathStart;
+ var newUri = base._uri.substring(0, base._pathStart) +
+ ref._uri.substring(ref._pathStart);
+ return new _SimpleUri(newUri,
+ base._schemeEnd,
+ base._hostStart,
+ base._portStart,
+ base._pathStart,
+ ref._queryStart + delta,
+ ref._fragmentStart + delta,
+ base._schemeCache);
+ }
+ if (base.hasEmptyPath && base.hasAuthority) {
+ // ref has relative non-empty path.
+ var delta = base._pathStart - ref._pathStart + 1;
+ var newUri = "${base._uri.substring(0, base._pathStart)}/"
+ "${ref._uri.substring(ref._pathStart)}";
+ return new _SimpleUri(newUri,
+ base._schemeEnd,
+ base._hostStart,
+ base._portStart,
+ base._pathStart,
+ ref._queryStart + delta,
+ ref._fragmentStart + delta,
+ base._schemeCache);
+ }
+ // Merge paths.
+ if (base._uri.startsWith("../", base._pathStart)) {
+ // Complex rare case, go slow.
+ return _toNonSimple().resolveUri(ref);
+ }
+ String baseUri = base._uri;
+ String refUri = ref._uri;
+ int baseStart = base._pathStart;
+ int baseEnd = base._queryStart;
+ int refStart = ref._pathStart;
+ int refEnd = ref._queryStart;
+ int backCount = 1;
+ while (refStart + 3 <= refEnd && refUri.startsWith("../", refStart)) {
+ refStart += 3;
+ backCount += 1;
+ }
+ if (refStart + 2 == refEnd && refUri.startsWith("..", refStart)) {
+ refStart += 2;
+ backCount += 1;
+ }
+
+ const slash = 0x2f;
+ while (baseEnd > baseStart) {
+ baseEnd--;
+ int char = baseUri.codeUnitAt(baseEnd);
+ if (char == slash) {
+ backCount--;
+ if (backCount == 0) break;
+ }
+ }
+ var delta;
+ var newUri;
+ if (baseEnd != baseStart || base._uri.startsWith("/", baseStart) ||
+ base.hasScheme || base.hasAuthority) {
+ delta = baseEnd - refStart + 1;
+ newUri = "${base._uri.substring(0, baseEnd)}/"
+ "${ref._uri.substring(refStart)}";
+ } else {
+ // We exactly removed all of a relative path.
+ // Example: foo:bar/baz resolve vs. ../../qux -> foo:qux
+ delta = baseEnd - refStart;
+ newUri = "${base._uri.substring(0, baseStart)}"
+ "${ref._uri.substring(refStart)}";
+ }
+
+ return new _SimpleUri(newUri,
+ base._schemeEnd,
+ base._hostStart,
+ base._portStart,
+ base._pathStart,
+ ref._queryStart + delta,
+ ref._fragmentStart + delta,
+ base._schemeCache);
+ }
+
+ // TODO: Deprecate Remove Kill Crush Destroy!
floitsch 2016/06/28 00:28:03 TODO(ldap). Also, make this more informative.
Lasse Reichstein Nielsen 2016/06/28 13:00:52 Done. Whoops. :)
+ // Reason: requires knowing whether we are on Windows.
+ // That's a PLATFORM SPECIFIC THING.
+ // This should be File.pathFromUri, since File is platform specific already.
+ String toFilePath({bool windows}) {
+ if (_schemeEnd >= 0 && !_isFile) {
+ throw new UnsupportedError(
+ "Cannot extract a file path from a $scheme URI");
+ }
+ if (_queryStart < _uri.length) {
+ if (_queryStart < _fragmentStart) {
+ throw new UnsupportedError(
+ "Cannot extract a file path from a URI with a query component");
+ }
+ throw new UnsupportedError(
+ "Cannot extract a file path from a URI with a fragment component");
+ }
+ if (windows == null) windows = Uri._isWindows;
+ return windows ? Uri._toWindowsFilePath(this) : _toFilePath();
+ }
+
+ String _toFilePath() {
+ if (_hostStart < _portStart) {
+ // Has authority and non-empty host.
+ throw new UnsupportedError(
+ "Cannot extract a non-Windows file path from a file URI "
+ "with an authority");
+ }
+ return this.path;
+ }
+
+ UriData get data {
+ assert(scheme != "data");
+ return null;
+ }
+
+ int get hashCode => _uri.hashCode;
+
+ bool operator==(Object other) {
+ if (other is! Uri) return false;
+ return _uri == other.toString();
+ }
+
+ Uri _toNonSimple() {
+ return new Uri._internal(
+ this.scheme,
+ this.userInfo,
+ this.hasAuthority ? this.host: null,
+ this.hasPort ? this.port : null,
+ this.path,
+ this.hasQuery ? this.query : null,
+ this.hasFragment ? this.fragment : null
+ );
+ }
+
+ String toString() => _uri;
+}
« no previous file with comments | « pkg/analyzer/lib/src/util/fast_uri.dart ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698