Index: sdk/lib/core/uri.dart |
diff --git a/sdk/lib/core/uri.dart b/sdk/lib/core/uri.dart |
index 045449102e5d4d636a3b7e1c00bd5a33104ca3b4..232d5525c4e72773e2f2156c25a7a35f3d6d91e9 100644 |
--- a/sdk/lib/core/uri.dart |
+++ b/sdk/lib/core/uri.dart |
@@ -7,10 +7,12 @@ part of dart.core; |
// Frequently used character codes. |
const int _SPACE = 0x20; |
const int _PERCENT = 0x25; |
+const int _AMPERSAND = 0x26; |
const int _PLUS = 0x2B; |
const int _DOT = 0x2E; |
const int _SLASH = 0x2F; |
const int _COLON = 0x3A; |
+const int _EQUALS = 0x3d; |
const int _UPPER_CASE_A = 0x41; |
const int _UPPER_CASE_Z = 0x5A; |
const int _LEFT_BRACKET = 0x5B; |
@@ -1369,7 +1371,7 @@ class _Uri implements Uri { |
* |
* Always non-null. |
*/ |
- String _path; |
+ final String path; |
// The query content, or null if there is no query. |
final String _query; |
@@ -1401,11 +1403,21 @@ class _Uri implements Uri { |
Map<String, List<String>> _queryParameterLists; |
/// Internal non-verifying constructor. Only call with validated arguments. |
+ /// |
+ /// The components must be properly normalized. |
+ /// |
+ /// Use `null` for [_host] if there is no authority. In that case, always |
+ /// pass `null` for [_port] and [_userInfo] as well. |
+ /// |
+ /// Use `null` for [_port], [_userInfo], [_query] and [_fragment] if there is |
+ /// component of that type. |
+ /// |
+ /// The [path] and [scheme] are never empty. |
_Uri._internal(this.scheme, |
this._userInfo, |
this._host, |
this._port, |
- this._path, |
+ this.path, |
this._query, |
this._fragment); |
@@ -1543,8 +1555,6 @@ class _Uri implements Uri { |
return 0; |
} |
- String get path => _path; |
- |
String get query => _query ?? ""; |
String get fragment => _fragment ?? ""; |
@@ -1847,7 +1857,7 @@ class _Uri implements Uri { |
path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
scheme, hasAuthority); |
} else { |
- path = this._path; |
+ path = this.path; |
if ((isFile || (hasAuthority && !path.isEmpty)) && |
!path.startsWith('/')) { |
path = "/" + path; |
@@ -1873,7 +1883,7 @@ class _Uri implements Uri { |
Uri removeFragment() { |
if (!this.hasFragment) return this; |
return new _Uri._internal(scheme, _userInfo, _host, _port, |
- _path, _query, null); |
+ path, _query, null); |
} |
List<String> get pathSegments { |
@@ -1914,8 +1924,8 @@ class _Uri implements Uri { |
} |
Uri normalizePath() { |
- String path = _normalizePath(_path, scheme, hasAuthority); |
- if (identical(path, _path)) return this; |
+ String path = _normalizePath(this.path, scheme, hasAuthority); |
+ if (identical(path, this.path)) return this; |
return this.replace(path: path); |
} |
@@ -2082,7 +2092,7 @@ class _Uri implements Uri { |
static String _makeUserInfo(String userInfo, int start, int end) { |
if (userInfo == null) return ""; |
- return _normalize(userInfo, start, end, _userinfoTable); |
+ return _normalizeOrSubstring(userInfo, start, end, _userinfoTable); |
} |
static String _makePath(String path, int start, int end, |
@@ -2097,7 +2107,7 @@ class _Uri implements Uri { |
} |
var result; |
if (path != null) { |
- result = _normalize(path, start, end, _pathCharOrSlashTable); |
+ result = _normalizeOrSubstring(path, start, end, _pathCharOrSlashTable); |
} else { |
result = pathSegments.map((s) => |
_uriEncode(_pathCharTable, s, UTF8, false)).join("/"); |
@@ -2130,7 +2140,7 @@ class _Uri implements Uri { |
if (queryParameters != null) { |
throw new ArgumentError('Both query and queryParameters specified'); |
} |
- return _normalize(query, start, end, _queryCharTable); |
+ return _normalizeOrSubstring(query, start, end, _queryCharTable); |
} |
if (queryParameters == null) return null; |
@@ -2162,7 +2172,7 @@ class _Uri implements Uri { |
static String _makeFragment(String fragment, int start, int end) { |
if (fragment == null) return null; |
- return _normalize(fragment, start, end, _queryCharTable); |
+ return _normalizeOrSubstring(fragment, start, end, _queryCharTable); |
} |
/** |
@@ -2185,8 +2195,8 @@ class _Uri implements Uri { |
} |
int firstDigit = source.codeUnitAt(index + 1); |
int secondDigit = source.codeUnitAt(index + 2); |
- int firstDigitValue = _parseHexDigit(firstDigit); |
- int secondDigitValue = _parseHexDigit(secondDigit); |
+ int firstDigitValue = hexDigitValue(firstDigit); |
+ int secondDigitValue = hexDigitValue(secondDigit); |
if (firstDigitValue < 0 || secondDigitValue < 0) { |
return "%"; // Marks the escape as invalid. |
} |
@@ -2206,19 +2216,6 @@ class _Uri implements Uri { |
return null; |
} |
- // Converts a UTF-16 code-unit to its value as a hex digit. |
- // Returns -1 for non-hex digits. |
- static int _parseHexDigit(int char) { |
- const int zeroDigit = 0x30; |
- int digit = char ^ zeroDigit; |
- if (digit <= 9) return digit; |
- int lowerCase = char | 0x20; |
- if (_LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_F) { |
- return lowerCase - (_LOWER_CASE_A - 10); |
- } |
- return -1; |
- } |
- |
static String _escapeChar(int char) { |
assert(char <= 0x10ffff); // It's a valid unicode code point. |
List<int> codeUnits; |
@@ -2255,6 +2252,18 @@ class _Uri implements Uri { |
} |
/** |
+ * Normalizes using [_normalize] or returns substring of original. |
+ * |
+ * If [_normalize] returns `null` (original content is already normalized), |
+ * this methods returns the substring if [component] from [start] to [end]. |
+ */ |
+ static String _normalizeOrSubstring(String component, int start, int end, |
+ List<int> charTable) { |
+ return _normalize(component, start, end, charTable) ?? |
+ component.substring(start, end); |
+ } |
+ |
+ /** |
* Runs through component checking that each character is valid and |
* normalize percent escapes. |
* |
@@ -2262,9 +2271,12 @@ class _Uri implements Uri { |
* Each `%` character must be followed by two hex digits. |
* If the hex-digits are lower case letters, they are converted to |
* upper case. |
+ * |
+ * Returns `null` if the original content was already normalized. |
*/ |
static String _normalize(String component, int start, int end, |
- List<int> charTable) { |
+ List<int> charTable, |
+ {bool escapeDelimiters = false}) { |
StringBuffer buffer; |
int sectionStart = start; |
int index = start; |
@@ -2290,7 +2302,7 @@ class _Uri implements Uri { |
} else { |
sourceLength = 3; |
} |
- } else if (_isGeneralDelimiter(char)) { |
+ } else if (!escapeDelimiters && _isGeneralDelimiter(char)) { |
_fail(component, index, "Invalid character"); |
} else { |
sourceLength = 1; |
@@ -2315,8 +2327,7 @@ class _Uri implements Uri { |
} |
} |
if (buffer == null) { |
- // Makes no copy if start == 0 and end == component.length. |
- return component.substring(start, end); |
+ return null; |
} |
if (sectionStart < end) { |
buffer.write(component.substring(sectionStart, end)); |
@@ -2509,7 +2520,7 @@ class _Uri implements Uri { |
targetHost = this._host; |
targetPort = this._port; |
if (reference.path == "") { |
- targetPath = this._path; |
+ targetPath = this.path; |
if (reference.hasQuery) { |
targetQuery = reference.query; |
} else { |
@@ -2535,7 +2546,7 @@ class _Uri implements Uri { |
targetPath = _removeDotSegments("/" + reference.path); |
} |
} else { |
- var mergedPath = _mergePaths(this._path, reference.path); |
+ var mergedPath = _mergePaths(this.path, reference.path); |
if (this.hasScheme || this.hasAuthority || this.hasAbsolutePath) { |
targetPath = _removeDotSegments(mergedPath); |
} else { |
@@ -2572,9 +2583,9 @@ class _Uri implements Uri { |
bool get hasFragment => _fragment != null; |
- bool get hasEmptyPath => _path.isEmpty; |
+ bool get hasEmptyPath => path.isEmpty; |
- bool get hasAbsolutePath => _path.startsWith('/'); |
+ bool get hasAbsolutePath => path.startsWith('/'); |
String get origin { |
if (scheme == "") { |
@@ -2652,7 +2663,7 @@ class _Uri implements Uri { |
} |
bool get _isPathAbsolute { |
- return _path != null && _path.startsWith('/'); |
+ return path != null && path.startsWith('/'); |
} |
void _writeAuthority(StringSink ss) { |
@@ -2742,13 +2753,11 @@ class _Uri implements Uri { |
result.putIfAbsent(key, _createList).add(value); |
} |
- const int _equals = 0x3d; |
- const int _ampersand = 0x26; |
while (i < query.length) { |
int char = query.codeUnitAt(i); |
- if (char == _equals) { |
+ if (char == _EQUALS) { |
if (equalsIndex < 0) equalsIndex = i; |
- } else if (char == _ampersand) { |
+ } else if (char == _AMPERSAND) { |
parsePair(start, equalsIndex, i); |
start = i + 1; |
equalsIndex = -1; |
@@ -3149,10 +3158,10 @@ class UriData { |
* If there is a single separator left, it ends the "base64" marker. |
* |
* So the following separators are found for a text: |
- * |
- * data:text/plain;foo=bar;base64,ARGLEBARGLE= |
- * ^ ^ ^ ^ ^ |
- * |
+ * ``` |
+ * data:text/plain;foo=bar;base64,ARGLEBARGLE= |
+ * ^ ^ ^ ^ ^ |
+ * ``` |
*/ |
final List<int> _separatorIndices; |
@@ -3353,12 +3362,15 @@ class UriData { |
* percent-escapes for non-ASCII byte values that need an interpretation |
* to be converted to the corresponding string. |
* |
- * Parsing doesn't check the validity of any part, it just checks that the |
- * input has the correct structure with the correct sequence of `/`, `;`, `=` |
- * and `,` delimiters. |
+ * Parsing checks that Base64 encoded data is valid, and it normalizes it |
+ * to use the default Base64 alphabet and to use padding. |
+ * Non-Base64 data is escaped using percent-escapes as necessary to make |
+ * it valid, and existing escapes are case normalized. |
* |
* Accessing the individual parts may fail later if they turn out to have |
- * content that can't be decoded successfully as a string. |
+ * content that can't be decoded successfully as a string, for example if |
+ * existing percent escapes represent bytes that cannot be decoded |
+ * by the chosen [Encoding] (see [contentAsString]). |
*/ |
static UriData parse(String uri) { |
if (uri.length >= 5) { |
@@ -3388,17 +3400,14 @@ class UriData { |
String query = null; |
int colonIndex = _separatorIndices[0]; |
int queryIndex = _text.indexOf('?', colonIndex + 1); |
- int end = null; |
+ int end = _text.length; |
if (queryIndex >= 0) { |
- query = _text.substring(queryIndex + 1); |
+ query = _Uri._normalizeOrSubstring(_text, queryIndex + 1, end, _Uri._queryCharTable); |
end = queryIndex; |
} |
- path = _text.substring(colonIndex + 1, end); |
- // TODO(lrn): This can generate a URI that isn't path normalized. |
- // That's perfectly reasonable - data URIs are not hierarchical, |
- // but it may make some consumers stumble. |
- // Should we at least do escape normalization? |
- _uriCache = new _Uri._internal("data", "", null, null, path, query, null); |
+ path = _Uri._normalizeOrSubstring(_text, colonIndex + 1, end, |
+ _Uri._pathCharOrSlashTable); |
+ _uriCache = new _DataUri(this, path, query); |
return _uriCache; |
} |
@@ -3408,6 +3417,9 @@ class UriData { |
* A data URI consists of a "media type" followed by data. |
* The media type starts with a MIME type and can be followed by |
* extra parameters. |
+ * If the MIME type representation in the URI text contains URI escapes, |
+ * they are unescaped in the returned string. |
+ * If the value contain non-ASCII percent escapes, they are decoded as UTF-8. |
* |
* Example: |
* |
@@ -3432,6 +3444,10 @@ class UriData { |
* If the parameters of the media type contains a `charset` parameter |
* then this returns its value, otherwise it returns `US-ASCII`, |
* which is the default charset for data URIs. |
+ * If the value contain non-ASCII percent escapes, they are decoded as UTF-8. |
+ * |
+ * If the MIME type representation in the URI text contains URI escapes, |
+ * they are unescaped in the returned string. |
*/ |
String get charset { |
int parameterStart = 1; |
@@ -3503,10 +3519,8 @@ class UriData { |
result[index++] = codeUnit; |
} else { |
if (i + 2 < text.length) { |
- var digit1 = _Uri._parseHexDigit(text.codeUnitAt(i + 1)); |
- var digit2 = _Uri._parseHexDigit(text.codeUnitAt(i + 2)); |
- if (digit1 >= 0 && digit2 >= 0) { |
- int byte = digit1 * 16 + digit2; |
+ int byte = parseHexByte(text, i + 1); |
+ if (byte >= 0) { |
result[index++] = byte; |
i += 2; |
continue; |
@@ -3561,8 +3575,8 @@ class UriData { |
* in the URI, which is reflected by the [charset] getter. This means that |
* [charset] may return a value even if `parameters["charset"]` is `null`. |
* |
- * If the values contain non-ASCII values or percent escapes, they default |
- * to being decoded as UTF-8. |
+ * If the values contain non-ASCII values or percent escapes, |
+ * they are decoded as UTF-8. |
*/ |
Map<String, String> get parameters { |
var result = <String, String>{}; |
@@ -3633,6 +3647,19 @@ class UriData { |
} |
} |
indices.add(i); |
+ bool isBase64 = indices.length.isOdd; |
+ if (isBase64) { |
+ text = BASE64.normalize(text, i + 1, text.length); |
+ } else { |
+ // Validate "data" part, must only contain RFC 2396 'uric' characters |
+ // (reserved, unreserved, or escape sequences). |
+ // Normalize to this (throws on a fragment separator). |
+ var data = _Uri._normalize(text, i + 1, text.length, _uricTable, |
+ escapeDelimiters: true); |
+ if (data != null) { |
+ text = text.replaceRange(i + 1, text.length, data); |
+ } |
+ } |
return new UriData._(text, indices, sourceUri); |
} |
@@ -3706,6 +3733,26 @@ class UriData { |
// |
// This is the same characters as in a URI query (which is URI pchar plus '?') |
static const _uricTable = _Uri._queryCharTable; |
+ |
+ // Characters allowed in base-64 encoding (alphanumeric, '/', '+' and '='). |
+ static const _base64Table = const [ |
+ // LSB MSB |
+ // | | |
+ 0x0000, // 0x00 - 0x0f 00000000 00000000 |
+ 0x0000, // 0x10 - 0x1f 00000000 00000000 |
+ // + / |
+ 0x8800, // 0x20 - 0x2f 00000000 00010001 |
+ // 01234567 89 |
+ 0x03ff, // 0x30 - 0x3f 11111111 11000000 |
+ // ABCDEFG HIJKLMNO |
+ 0xfffe, // 0x40 - 0x4f 01111111 11111111 |
+ // PQRSTUVW XYZ |
+ 0x07ff, // 0x50 - 0x5f 11111111 11100000 |
+ // abcdefg hijklmno |
+ 0xfffe, // 0x60 - 0x6f 01111111 11111111 |
+ // pqrstuvw xyz |
+ 0x07ff, // 0x70 - 0x7f 11111111 11100000 |
+ ]; |
} |
// -------------------------------------------------------------------- |
@@ -4555,6 +4602,16 @@ class _SimpleUri implements Uri { |
String toString() => _uri; |
} |
+/// Special [_Uri] created from an existing [UriData]. |
+class _DataUri extends _Uri { |
+ final UriData _data; |
+ |
+ _DataUri(this._data, String path, String query) |
+ : super._internal("data", null, null, null, path, query, null); |
+ |
+ UriData get data => _data; |
+} |
+ |
/// Checks whether [text] starts with "data:" at position [start]. |
/// |
/// The text must be long enough to allow reading five characters |