Chromium Code Reviews| Index: sdk/lib/core/uri.dart |
| diff --git a/sdk/lib/core/uri.dart b/sdk/lib/core/uri.dart |
| index 5718ca80b83b870dd578011592707b4c7d99cdc4..533f4ab339ca78573e598345196f09c8a3bebd85 100644 |
| --- a/sdk/lib/core/uri.dart |
| +++ b/sdk/lib/core/uri.dart |
| @@ -4,6 +4,24 @@ |
| part of dart.core; |
| +// Frequently used character codes. |
| +const int _SPACE = 0x20; |
| +const int _PERCENT = 0x25; |
| +const int _PLUS = 0x2B; |
| +const int _DOT = 0x2E; |
| +const int _SLASH = 0x2F; |
| +const int _COLON = 0x3A; |
| +const int _UPPER_CASE_A = 0x41; |
| +const int _UPPER_CASE_Z = 0x5A; |
| +const int _LEFT_BRACKET = 0x5B; |
| +const int _BACKSLASH = 0x5C; |
| +const int _RIGHT_BRACKET = 0x5D; |
| +const int _LOWER_CASE_A = 0x61; |
| +const int _LOWER_CASE_F = 0x66; |
| +const int _LOWER_CASE_Z = 0x7A; |
| + |
| +const String _hexDigits = "0123456789ABCDEF"; |
| + |
| /** |
| * A parsed URI, such as a URL. |
| * |
| @@ -15,77 +33,17 @@ part of dart.core; |
| * [uris]: https://www.dartlang.org/docs/dart-up-and-running/ch03.html#uris |
| * [libtour]: https://www.dartlang.org/docs/dart-up-and-running/contents/ch03.html |
| */ |
| -class Uri { |
| +abstract class Uri { |
| /** |
| - * The scheme component of the URI. |
| + * Returns the natural base URI for the current platform. |
| * |
| - * Returns the empty string if there is no scheme component. |
| + * When running in a browser this is the current URL of the current page |
| + * (from `window.location.href`). |
| * |
| - * A URI scheme is case insensitive. |
| - * The returned scheme is canonicalized to lowercase letters. |
| + * When not running in a browser this is the file URI referencing |
| + * the current working directory. |
| */ |
| - // We represent the missing scheme as an empty string. |
| - // A valid scheme cannot be empty. |
| - final String scheme; |
| - |
| - /** |
| - * The user-info part of the authority. |
| - * |
| - * Does not distinguish between an empty user-info and an absent one. |
| - * The value is always non-null. |
| - * Is considered absent if [_host] is `null`. |
| - */ |
| - final String _userInfo; |
| - |
| - /** |
| - * The host name of the URI. |
| - * |
| - * Set to `null` if there is no authority in the URI. |
| - * The host name is the only mandatory part of an authority, so we use |
| - * it to mark whether an authority part was present or not. |
| - */ |
| - final String _host; |
| - |
| - /** |
| - * The port number part of the authority. |
| - * |
| - * The port. Set to null if there is no port. Normalized to null if |
| - * the port is the default port for the scheme. |
| - */ |
| - int _port; |
| - |
| - /** |
| - * The path of the URI. |
| - * |
| - * Always non-null. |
| - */ |
| - String _path; |
| - |
| - // The query content, or null if there is no query. |
| - final String _query; |
| - |
| - // The fragment content, or null if there is no fragment. |
| - final String _fragment; |
| - |
| - /** |
| - * Cache the computed return value of [pathSegements]. |
| - */ |
| - List<String> _pathSegments; |
| - |
| - /** |
| - * Cache the computed return value of [queryParameters]. |
| - */ |
| - Map<String, String> _queryParameters; |
| - Map<String, List<String>> _queryParameterLists; |
| - |
| - /// Internal non-verifying constructor. Only call with validated arguments. |
| - Uri._internal(this.scheme, |
| - this._userInfo, |
| - this._host, |
| - this._port, |
| - this._path, |
| - this._query, |
| - this._fragment); |
| + external static Uri get base; |
| /** |
| * Creates a new URI from its components. |
| @@ -158,39 +116,15 @@ 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, |
| Iterable<String> pathSegments, |
| String query, |
| Map<String, dynamic/*String|Iterable<String>*/> queryParameters, |
| - String fragment}) { |
| - scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme)); |
| - userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); |
| - host = _makeHost(host, 0, _stringOrNullLength(host), false); |
| - // Special case this constructor for backwards compatibility. |
| - if (query == "") query = null; |
| - query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); |
| - fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); |
| - port = _makePort(port, scheme); |
| - bool isFile = (scheme == "file"); |
| - if (host == null && |
| - (userInfo.isNotEmpty || port != null || isFile)) { |
| - host = ""; |
| - } |
| - bool hasAuthority = (host != null); |
| - path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
| - 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); |
| - } |
| + String fragment}) = _Uri; |
| /** |
| * Creates a new `http` URI from authority, path and query. |
| @@ -227,9 +161,7 @@ class Uri { |
| */ |
| factory Uri.http(String authority, |
| String unencodedPath, |
| - [Map<String, String> queryParameters]) { |
| - return _makeHttpUri("http", authority, unencodedPath, queryParameters); |
| - } |
| + [Map<String, String> queryParameters]) = _Uri.http; |
| /** |
| * Creates a new `https` URI from authority, path and query. |
| @@ -239,11 +171,179 @@ class Uri { |
| */ |
| factory Uri.https(String authority, |
| String unencodedPath, |
| - [Map<String, String> queryParameters]) { |
| - return _makeHttpUri("https", authority, unencodedPath, queryParameters); |
| + [Map<String, String> queryParameters]) = _Uri.https; |
| + |
| + /** |
| + * Creates a new file URI from an absolute or relative file path. |
| + * |
| + * The file path is passed in [path]. |
| + * |
| + * This path is interpreted using either Windows or non-Windows |
| + * semantics. |
| + * |
| + * With non-Windows semantics the slash ("/") is used to separate |
| + * path segments. |
| + * |
| + * With Windows semantics, backslash ("\") and forward-slash ("/") |
| + * are used to separate path segments, except if the path starts |
| + * with "\\?\" in which case, only backslash ("\") separates path |
| + * segments. |
| + * |
| + * If the path starts with a path separator an absolute URI is |
| + * created. Otherwise a relative URI is created. One exception from |
| + * this rule is that when Windows semantics is used and the path |
| + * starts with a drive letter followed by a colon (":") and a |
| + * path separator then an absolute URI is created. |
| + * |
| + * The default for whether to use Windows or non-Windows semantics |
| + * determined from the platform Dart is running on. When running in |
| + * the standalone VM this is detected by the VM based on the |
| + * operating system. When running in a browser non-Windows semantics |
| + * is always used. |
| + * |
| + * To override the automatic detection of which semantics to use pass |
| + * a value for [windows]. Passing `true` will use Windows |
| + * semantics and passing `false` will use non-Windows semantics. |
| + * |
| + * Examples using non-Windows semantics: |
| + * |
| + * ``` |
| + * // xxx/yyy |
| + * new Uri.file("xxx/yyy", windows: false); |
| + * |
| + * // xxx/yyy/ |
| + * new Uri.file("xxx/yyy/", windows: false); |
| + * |
| + * // file:///xxx/yyy |
| + * new Uri.file("/xxx/yyy", windows: false); |
| + * |
| + * // file:///xxx/yyy/ |
| + * new Uri.file("/xxx/yyy/", windows: false); |
| + * |
| + * // C: |
| + * new Uri.file("C:", windows: false); |
| + * ``` |
| + * |
| + * Examples using Windows semantics: |
| + * |
| + * ``` |
| + * // xxx/yyy |
| + * new Uri.file(r"xxx\yyy", windows: true); |
| + * |
| + * // xxx/yyy/ |
| + * new Uri.file(r"xxx\yyy\", windows: true); |
| + * |
| + * file:///xxx/yyy |
| + * new Uri.file(r"\xxx\yyy", windows: true); |
| + * |
| + * file:///xxx/yyy/ |
| + * new Uri.file(r"\xxx\yyy/", windows: true); |
| + * |
| + * // file:///C:/xxx/yyy |
| + * new Uri.file(r"C:\xxx\yyy", windows: true); |
| + * |
| + * // This throws an error. A path with a drive letter is not absolute. |
| + * new Uri.file(r"C:", windows: true); |
| + * |
| + * // This throws an error. A path with a drive letter is not absolute. |
| + * new Uri.file(r"C:xxx\yyy", windows: true); |
| + * |
| + * // file://server/share/file |
| + * new Uri.file(r"\\server\share\file", windows: true); |
| + * ``` |
| + * |
| + * If the path passed is not a legal file path [ArgumentError] is thrown. |
| + */ |
| + factory Uri.file(String path, {bool windows}) = _Uri.file; |
| + |
| + /** |
| + * Like [Uri.file] except that a non-empty URI path ends in a slash. |
| + * |
| + * If [path] is not empty, and it doesn't end in a directory separator, |
| + * then a slash is added to the returned URI's path. |
| + * In all other cases, the result is the same as returned by `Uri.file`. |
| + */ |
| + factory Uri.directory(String path, {bool windows}) = _Uri.directory; |
| + |
| + /** |
| + * Creates a `data:` URI containing the [content] string. |
| + * |
| + * Converts the content to a bytes using [encoding] or the charset specified |
| + * in [parameters] (defaulting to US-ASCII if not specified or unrecognized), |
| + * then encodes the bytes into the resulting data URI. |
| + * |
| + * Defaults to encoding using percent-encoding (any non-ASCII or non-URI-valid |
| + * bytes is replaced by a percent encoding). If [base64] is true, the bytes |
| + * are instead encoded using [BASE64]. |
| + * |
| + * If [encoding] is not provided and [parameters] has a `charset` entry, |
| + * that name is looked up using [Encoding.getByName], |
| + * and if the lookup returns an encoding, that encoding is used to convert |
| + * [content] to bytes. |
| + * If providing both an [encoding] and a charset [parameter], they should |
| + * agree, otherwise decoding won't be able to use the charset parameter |
| + * to determine the encoding. |
| + * |
| + * If [mimeType] and/or [parameters] are supplied, they are added to the |
| + * created URI. If any of these contain characters that are not allowed |
| + * in the data URI, the character is percent-escaped. If the character is |
| + * non-ASCII, it is first UTF-8 encoded and then the bytes are percent |
| + * encoded. An omitted [mimeType] in a data URI means `text/plain`, just |
| + * as an omitted `charset` parameter defaults to meaning `US-ASCII`. |
| + * |
| + * To read the content back, use [UriData.contentAsString]. |
| + */ |
| + factory Uri.dataFromString(String content, |
| + {String mimeType, |
| + Encoding encoding, |
| + Map<String, String> parameters, |
| + bool base64: false}) { |
| + UriData data = new UriData.fromString(content, |
| + mimeType: mimeType, |
| + encoding: encoding, |
| + parameters: parameters, |
| + base64: base64); |
| + return data.uri; |
| } |
| /** |
| + * Creates a `data:` URI containing an encoding of [bytes]. |
| + * |
| + * Defaults to Base64 encoding the bytes, but if [percentEncoded] |
| + * is `true`, the bytes will instead be percent encoded (any non-ASCII |
| + * or non-valid-ASCII-character byte is replaced by a percent encoding). |
| + * |
| + * To read the bytes back, use [UriData.contentAsBytes]. |
| + * |
| + * It defaults to having the mime-type `application/octet-stream`. |
| + * The [mimeType] and [parameters] are added to the created URI. |
| + * If any of these contain characters that are not allowed |
| + * in the data URI, the character is percent-escaped. If the character is |
| + * non-ASCII, it is first UTF-8 encoded and then the bytes are percent |
| + * encoded. |
| + */ |
| + factory Uri.dataFromBytes(List<int> bytes, |
| + {mimeType: "application/octet-stream", |
| + Map<String, String> parameters, |
| + percentEncoded: false}) { |
| + UriData data = new UriData.fromBytes(bytes, |
| + mimeType: mimeType, |
| + parameters: parameters, |
| + percentEncoded: percentEncoded); |
| + return data.uri; |
| + } |
| + |
| + /** |
| + * The scheme component of the URI. |
| + * |
| + * Returns the empty string if there is no scheme component. |
| + * |
| + * A URI scheme is case insensitive. |
| + * The returned scheme is canonicalized to lowercase letters. |
| + */ |
| + String get scheme; |
| + |
| + /** |
| * Returns the authority component. |
| * |
| * The authority is formatted from the [userInfo], [host] and [port] |
| @@ -251,12 +351,7 @@ class Uri { |
| * |
| * Returns the empty string if there is no authority component. |
| */ |
| - String get authority { |
| - if (!hasAuthority) return ""; |
| - var sb = new StringBuffer(); |
| - _writeAuthority(sb); |
| - return sb.toString(); |
| - } |
| + String get authority; |
| /** |
| * Returns the user info part of the authority component. |
| @@ -264,7 +359,7 @@ class Uri { |
| * Returns the empty string if there is no user info in the |
| * authority component. |
| */ |
| - String get userInfo => _userInfo; |
| + String get userInfo; |
| /** |
| * Returns the host part of the authority component. |
| @@ -279,13 +374,7 @@ class Uri { |
| * The returned host name is canonicalized to lower-case |
| * with upper-case percent-escapes. |
| */ |
| - String get host { |
| - if (_host == null) return ""; |
| - if (_host.startsWith('[')) { |
| - return _host.substring(1, _host.length - 1); |
| - } |
| - return _host; |
| - } |
| + String get host; |
| /** |
| * Returns the port part of the authority component. |
| @@ -293,17 +382,7 @@ class Uri { |
| * Returns the defualt port if there is no port number in the authority |
| * component. That's 80 for http, 443 for https, and 0 for everything else. |
| */ |
| - int get port { |
| - if (_port == null) return _defaultPort(scheme); |
| - return _port; |
| - } |
| - |
| - // The default port for the scheme of this Uri.. |
| - static int _defaultPort(String scheme) { |
| - if (scheme == "http") return 80; |
| - if (scheme == "https") return 443; |
| - return 0; |
| - } |
| + int get port; |
| /** |
| * Returns the path component. |
| @@ -313,7 +392,7 @@ class Uri { |
| * |
| * Returns the empty string if there is no path component. |
| */ |
| - String get path => _path; |
| + String get path; |
| /** |
| * Returns the query component. The returned query is encoded. To get |
| @@ -321,7 +400,7 @@ class Uri { |
| * |
| * Returns the empty string if there is no query component. |
| */ |
| - String get query => (_query == null) ? "" : _query; |
| + String get query; |
| /** |
| * Returns the fragment identifier component. |
| @@ -329,7 +408,299 @@ class Uri { |
| * Returns the empty string if there is no fragment identifier |
| * component. |
| */ |
| - String get fragment => (_fragment == null) ? "" : _fragment; |
| + String get fragment; |
| + |
| + /** |
| + * Returns the URI path split into its segments. Each of the segments in the |
| + * returned list have been decoded. If the path is empty the empty list will |
| + * be returned. A leading slash `/` does not affect the segments returned. |
| + * |
| + * The returned list is unmodifiable and will throw [UnsupportedError] on any |
| + * calls that would mutate it. |
| + */ |
| + List<String> get pathSegments; |
| + |
| + /** |
| + * Returns the URI query split into a map according to the rules |
| + * specified for FORM post in the [HTML 4.01 specification section |
| + * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). |
| + * Each key and value in the returned map has been decoded. |
| + * If there is no query the empty map is returned. |
| + * |
| + * Keys in the query string that have no value are mapped to the |
| + * empty string. |
| + * If a key occurs more than once in the query string, it is mapped to |
| + * an arbitrary choice of possible value. |
| + * The [queryParametersAll] getter can provide a map |
| + * that maps keys to all of their values. |
| + * |
| + * The returned map is unmodifiable. |
| + */ |
| + Map<String, String> get queryParameters; |
| + |
| + /** |
| + * Returns the URI query split into a map according to the rules |
| + * specified for FORM post in the [HTML 4.01 specification section |
| + * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). |
| + * Each key and value in the returned map has been decoded. If there is no |
| + * query the empty map is returned. |
| + * |
| + * Keys are mapped to lists of their values. If a key occurs only once, |
| + * its value is a singleton list. If a key occurs with no value, the |
| + * empty string is used as the value for that occurrence. |
| + * |
| + * The returned map and the lists it contains are unmodifiable. |
| + */ |
| + Map<String, List<String>> get queryParametersAll; |
| + |
| + /** |
| + * Returns whether the URI is absolute. |
| + * |
| + * A URI is an absolute URI in the sense of RFC 3986 if it has a scheme |
| + * and no fragment. |
| + */ |
| + bool get isAbsolute; |
| + |
| + /** |
| + * Returns whether the URI has a [scheme] component. |
| + */ |
| + bool get hasScheme => scheme.isNotEmpty; |
| + |
| + /** |
| + * Returns whether the URI has an [authority] component. |
| + */ |
| + bool get hasAuthority; |
| + |
| + /** |
| + * Returns whether the URI has an explicit port. |
| + * |
| + * If the port number is the default port number |
| + * (zero for unrecognized schemes, with http (80) and https (443) being |
| + * recognized), |
| + * then the port is made implicit and omitted from the URI. |
| + */ |
| + bool get hasPort; |
| + |
| + /** |
| + * Returns whether the URI has a query part. |
| + */ |
| + bool get hasQuery; |
| + |
| + /** |
| + * Returns whether the URI has a fragment part. |
| + */ |
| + bool get hasFragment; |
| + |
| + /** |
| + * Returns whether the URI has an empty path. |
| + */ |
| + bool get hasEmptyPath; |
| + |
| + /** |
| + * Returns whether the URI has an absolute path (starting with '/'). |
| + */ |
| + bool get hasAbsolutePath; |
| + |
| + /** |
| + * Returns the origin of the URI in the form scheme://host:port for the |
| + * schemes http and https. |
| + * |
| + * It is an error if the scheme is not "http" or "https". |
| + * |
| + * See: http://www.w3.org/TR/2011/WD-html5-20110405/origin-0.html#origin |
| + */ |
| + String get origin; |
| + |
| + /** |
| + * Returns the file path from a file URI. |
| + * |
| + * The returned path has either Windows or non-Windows |
| + * semantics. |
| + * |
| + * For non-Windows semantics the slash ("/") is used to separate |
| + * path segments. |
| + * |
| + * For Windows semantics the backslash ("\") separator is used to |
| + * separate path segments. |
| + * |
| + * If the URI is absolute the path starts with a path separator |
| + * unless Windows semantics is used and the first path segment is a |
| + * drive letter. When Windows semantics is used a host component in |
| + * the uri in interpreted as a file server and a UNC path is |
| + * returned. |
| + * |
| + * The default for whether to use Windows or non-Windows semantics |
| + * determined from the platform Dart is running on. When running in |
| + * the standalone VM this is detected by the VM based on the |
| + * operating system. When running in a browser non-Windows semantics |
| + * is always used. |
| + * |
| + * To override the automatic detection of which semantics to use pass |
| + * a value for [windows]. Passing `true` will use Windows |
| + * semantics and passing `false` will use non-Windows semantics. |
| + * |
| + * If the URI ends with a slash (i.e. the last path component is |
| + * empty) the returned file path will also end with a slash. |
| + * |
| + * With Windows semantics URIs starting with a drive letter cannot |
| + * be relative to the current drive on the designated drive. That is |
| + * for the URI `file:///c:abc` calling `toFilePath` will throw as a |
| + * path segment cannot contain colon on Windows. |
| + * |
| + * Examples using non-Windows semantics (resulting of calling |
| + * toFilePath in comment): |
| + * |
| + * Uri.parse("xxx/yyy"); // xxx/yyy |
| + * Uri.parse("xxx/yyy/"); // xxx/yyy/ |
| + * Uri.parse("file:///xxx/yyy"); // /xxx/yyy |
| + * Uri.parse("file:///xxx/yyy/"); // /xxx/yyy/ |
| + * Uri.parse("file:///C:"); // /C: |
| + * Uri.parse("file:///C:a"); // /C:a |
| + * |
| + * Examples using Windows semantics (resulting URI in comment): |
| + * |
| + * Uri.parse("xxx/yyy"); // xxx\yyy |
| + * Uri.parse("xxx/yyy/"); // xxx\yyy\ |
| + * Uri.parse("file:///xxx/yyy"); // \xxx\yyy |
| + * Uri.parse("file:///xxx/yyy/"); // \xxx\yyy/ |
| + * Uri.parse("file:///C:/xxx/yyy"); // C:\xxx\yyy |
| + * Uri.parse("file:C:xxx/yyy"); // Throws as a path segment |
| + * // cannot contain colon on Windows. |
| + * Uri.parse("file://server/share/file"); // \\server\share\file |
| + * |
| + * If the URI is not a file URI calling this throws |
| + * [UnsupportedError]. |
| + * |
| + * 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}); |
| + |
| + /** |
| + * Access the structure of a `data:` URI. |
| + * |
| + * Returns a [UriData] object for `data:` URIs and `null` for all other |
| + * URIs. |
| + * The [UriData] object can be used to access the media type and data |
| + * of a `data:` URI. |
| + */ |
| + UriData get data; |
| + |
| + /// Returns a hash code computed as `toString().hashCode`. |
| + /// |
| + /// This guarantees that URIs with the same normalized |
| + int get hashCode; |
| + |
| + /// A URI is equal to another URI with the same normalized representation. |
| + bool operator==(Object other); |
| + |
| + /// Returns the normalized string representation of the URI. |
| + String toString(); |
| + |
| + /** |
| + * Returns a new `Uri` based on this one, but with some parts replaced. |
| + * |
| + * This method takes the same parameters as the [new Uri] constructor, |
| + * and they have the same meaning. |
| + * |
| + * At most one of [path] and [pathSegments] must be provided. |
| + * Likewise, at most one of [query] and [queryParameters] must be provided. |
| + * |
| + * Each part that is not provided will default to the corresponding |
| + * value from this `Uri` instead. |
| + * |
| + * This method is different from [Uri.resolve] which overrides in a |
| + * hierarchial manner, |
| + * and can instead replace each part of a `Uri` individually. |
| + * |
| + * Example: |
| + * |
| + * Uri uri1 = Uri.parse("a://b@c:4/d/e?f#g"); |
| + * Uri uri2 = uri1.replace(scheme: "A", path: "D/E/E", fragment: "G"); |
| + * print(uri2); // prints "A://b@c:4/D/E/E/?f#G" |
| + * |
| + * This method acts similarly to using the `new Uri` constructor with |
| + * some of the arguments taken from this `Uri` . Example: |
| + * |
| + * Uri uri3 = new Uri( |
| + * scheme: "A", |
| + * userInfo: uri1.userInfo, |
| + * host: uri1.host, |
| + * port: uri1.port, |
| + * path: "D/E/E", |
| + * query: uri1.query, |
| + * fragment: "G"); |
| + * print(uri3); // prints "A://b@c:4/D/E/E/?f#G" |
| + * print(uri2 == uri3); // prints true. |
| + * |
| + * Using this method can be seen as a shorthand for the `Uri` constructor |
| + * call above, but may also be slightly faster because the parts taken |
| + * from this `Uri` need not be checked for validity again. |
| + */ |
| + 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}); |
| + |
| + /** |
| + * Returns a `Uri` that differs from this only in not having a fragment. |
| + * |
| + * If this `Uri` does not have a fragment, it is itself returned. |
| + */ |
| + Uri removeFragment(); |
| + |
| + /** |
| + * Resolve [reference] as an URI relative to `this`. |
| + * |
| + * First turn [reference] into a URI using [Uri.parse]. Then resolve the |
| + * resulting URI relative to `this`. |
| + * |
| + * Returns the resolved URI. |
| + * |
| + * See [resolveUri] for details. |
| + */ |
| + Uri resolve(String reference); |
| + |
| + /** |
| + * Resolve [reference] as an URI relative to `this`. |
| + * |
| + * Returns the resolved URI. |
| + * |
| + * 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); |
| + |
| + /** |
| + * Returns a URI where the path has been normalized. |
| + * |
| + * A normalized path does not contain `.` segments or non-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 `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(); |
| /** |
| * Creates a new `Uri` object by parsing a URI string. |
| @@ -394,206 +765,683 @@ class Uri { |
| // query = *( pchar / "/" / "?" ) |
| // |
| // fragment = *( pchar / "/" / "?" ) |
| - const int EOI = -1; |
| - |
| - 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; |
| + end ??= uri.length; |
| + |
| + // Special case data:URIs. Ignore case when testing. |
| + if (end >= start + 5) { |
| + int dataDelta = _startsWithData(uri, start); |
| + if (dataDelta == 0) { |
| + // The case is right. |
| + if (start > 0 || end < uri.length) uri = uri.substring(start, end); |
| + return UriData._parse(uri, 5, null).uri; |
| + } else if (dataDelta == 0x20) { |
| + return UriData._parse(uri.substring(start + 5, end), 0, null).uri; |
| } |
| - 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; |
| + // Otherwise the URI doesn't start with "data:" or any case variant of it. |
| + } |
| + |
| + // TODO(lrn): Consider inlining _scanUri here. |
| + // The following index-normalization belongs with the scanning, but is |
| + // easier to do here because we already have extracted variables from the |
|
floitsch
2016/06/29 23:41:47
I think you should do the normalizations in _scanU
Lasse Reichstein Nielsen
2016/06/30 10:27:31
It probably shouldn't but in practice I see a ~10%
|
| + // indices list. |
| + var indices = _scanUri(uri, start, end); |
| + |
| + int schemeEnd = indices[_schemeEndIndex]; |
| + int hostStart = indices[_hostStartIndex] + 1; |
| + 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 to normalize the indices. |
| + // If fragment but no query, set query to start at fragment. |
| + if (fragmentStart < queryStart) queryStart = fragmentStart; |
| + // 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; |
| + |
| + assert(hostStart == start || schemeEnd <= hostStart); |
| + assert(hostStart <= portStart); |
| + assert(schemeEnd <= pathStart); |
| + assert(portStart <= pathStart); |
| + assert(pathStart <= queryStart); |
| + assert(queryStart <= fragmentStart); |
| + |
| + 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. |
| + // The scanner doesn't set the not-simple position in this case because |
| + // it's setting the host-start position instead. |
| + isSimple = false; |
| + } else if (portStart > start && portStart + 1 == pathStart) { |
| + // If the port is empty, it should be omitted. |
| + // Pathological 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 if (queryStart < end && |
| + (queryStart == pathStart + 2 && |
| + uri.startsWith("..", pathStart)) || |
| + (queryStart > pathStart + 2 && |
| + uri.startsWith("/..", queryStart - 3))) { |
| + // The path ends in a ".." segment. This should be normalized to "../". |
| + // We didn't detect this while scanning because a query or fragment was |
| + // detected at the same time (which is why we only need to check this |
| + // if there is something after the path). |
| + 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 <= start) { |
| + // File URIs should have an authority. |
| + // Paths after an authority should be absolute. |
| + String schemeAuth = "file://"; |
| + int delta = 2; |
| + if (!uri.startsWith("/", pathStart)) { |
| + schemeAuth = "file:///"; |
| + delta = 3; |
| + } |
| + uri = schemeAuth + uri.substring(pathStart, end); |
| + schemeEnd -= start; |
| + hostStart = 7; |
| + portStart = 7; |
| + pathStart = 7; |
| + queryStart += delta - start; |
| + fragmentStart += delta - start; |
| + start = 0; |
| + end = uri.length; |
| + } else if (pathStart == queryStart) { |
| + // Uri has authority and empty path. Add "/" as path. |
| + uri = "${uri.substring(start, pathStart)}/" |
| + "${uri.substring(queryStart, end)}"; |
| + schemeEnd -= start; |
| + hostStart -= start; |
| + portStart -= start; |
| + pathStart -= start; |
| + queryStart += 1 - start; |
| + fragmentStart += 1 - start; |
| + start = 0; |
| + end = uri.length; |
| + } |
| + } else if (uri.startsWith("http", start)) { |
| + scheme = "http"; |
| + // HTTP URIs should not have an explicit port of 80. |
| + 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; |
| } |
| } |
| - index++; |
| - char = EOI; |
| } |
| - int hostStart = authStart; |
| - int hostEnd = index; |
| - if (lastAt >= 0) { |
| - userinfo = _makeUserInfo(uri, authStart, lastAt); |
| - hostStart = lastAt + 1; |
| + } |
| + |
| + 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; |
| } |
| - 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); |
| - } |
| + return new _SimpleUri(uri, schemeEnd, hostStart, portStart, pathStart, |
| + queryStart, fragmentStart, scheme); |
| + |
| + } |
| + |
| + return new _Uri.notSimple(uri, start, end, schemeEnd, hostStart, portStart, |
| + pathStart, queryStart, fragmentStart, scheme); |
| + } |
| + |
| + /** |
| + * Encode the string [component] using percent-encoding to make it |
| + * safe for literal use as a URI component. |
| + * |
| + * All characters except uppercase and lowercase letters, digits and |
| + * the characters `-_.!~*'()` are percent-encoded. This is the |
| + * set of characters specified in RFC 2396 and the which is |
| + * specified for the encodeUriComponent in ECMA-262 version 5.1. |
| + * |
| + * When manually encoding path segments or query components remember |
| + * to encode each part separately before building the path or query |
| + * string. |
| + * |
| + * For encoding the query part consider using |
| + * [encodeQueryComponent]. |
| + * |
| + * To avoid the need for explicitly encoding use the [pathSegments] |
| + * and [queryParameters] optional named arguments when constructing |
| + * a [Uri]. |
| + */ |
| + static String encodeComponent(String component) { |
| + return _Uri._uriEncode(_Uri._unreserved2396Table, component, UTF8, false); |
| + } |
| + |
| + /** |
| + * Encode the string [component] according to the HTML 4.01 rules |
| + * for encoding the posting of a HTML form as a query string |
| + * component. |
| + * |
| + * Encode the string [component] according to the HTML 4.01 rules |
| + * for encoding the posting of a HTML form as a query string |
| + * component. |
| + |
| + * The component is first encoded to bytes using [encoding]. |
| + * The default is to use [UTF8] encoding, which preserves all |
| + * the characters that don't need encoding. |
| + |
| + * Then the resulting bytes are "percent-encoded". This transforms |
| + * spaces (U+0020) to a plus sign ('+') and all bytes that are not |
| + * the ASCII decimal digits, letters or one of '-._~' are written as |
| + * a percent sign '%' followed by the two-digit hexadecimal |
| + * representation of the byte. |
| + |
| + * Note that the set of characters which are percent-encoded is a |
| + * superset of what HTML 4.01 requires, since it refers to RFC 1738 |
| + * for reserved characters. |
| + * |
| + * When manually encoding query components remember to encode each |
| + * part separately before building the query string. |
| + * |
| + * To avoid the need for explicitly encoding the query use the |
| + * [queryParameters] optional named arguments when constructing a |
| + * [Uri]. |
| + * |
| + * See http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 for more |
| + * details. |
| + */ |
| + static String encodeQueryComponent(String component, |
| + {Encoding encoding: UTF8}) { |
| + return _Uri._uriEncode(_Uri._unreservedTable, component, encoding, true); |
| + } |
| + |
| + /** |
| + * Decodes the percent-encoding in [encodedComponent]. |
| + * |
| + * Note that decoding a URI component might change its meaning as |
| + * some of the decoded characters could be characters with are |
| + * delimiters for a given URI componene type. Always split a URI |
| + * component using the delimiters for the component before decoding |
| + * the individual parts. |
| + * |
| + * For handling the [path] and [query] components consider using |
| + * [pathSegments] and [queryParameters] to get the separated and |
| + * decoded component. |
| + */ |
| + static String decodeComponent(String encodedComponent) { |
| + return _Uri._uriDecode(encodedComponent, 0, encodedComponent.length, |
| + UTF8, false); |
| + } |
| + |
| + /** |
| + * Decodes the percent-encoding in [encodedComponent], converting |
| + * pluses to spaces. |
| + * |
| + * It will create a byte-list of the decoded characters, and then use |
| + * [encoding] to decode the byte-list to a String. The default encoding is |
| + * UTF-8. |
| + */ |
| + static String decodeQueryComponent( |
| + String encodedComponent, |
| + {Encoding encoding: UTF8}) { |
| + return _Uri._uriDecode(encodedComponent, 0, encodedComponent.length, |
| + encoding, true); |
| + } |
| + |
| + /** |
| + * Encode the string [uri] using percent-encoding to make it |
| + * safe for literal use as a full URI. |
| + * |
| + * All characters except uppercase and lowercase letters, digits and |
| + * the characters `!#$&'()*+,-./:;=?@_~` are percent-encoded. This |
| + * is the set of characters specified in in ECMA-262 version 5.1 for |
| + * the encodeURI function . |
| + */ |
| + static String encodeFull(String uri) { |
| + return _Uri._uriEncode(_Uri._encodeFullTable, uri, UTF8, false); |
| + } |
| + |
| + /** |
| + * Decodes the percent-encoding in [uri]. |
| + * |
| + * Note that decoding a full URI might change its meaning as some of |
| + * the decoded characters could be reserved characters. In most |
| + * cases an encoded URI should be parsed into components using |
| + * [Uri.parse] before decoding the separate components. |
| + */ |
| + static String decodeFull(String uri) { |
| + return _Uri._uriDecode(uri, 0, uri.length, UTF8, false); |
| + } |
| + |
| + /** |
| + * Returns the [query] split into a map according to the rules |
| + * specified for FORM post in the [HTML 4.01 specification section |
| + * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). |
| + * Each key and value in the returned map has been decoded. If the [query] |
| + * is the empty string an empty map is returned. |
| + * |
| + * Keys in the query string that have no value are mapped to the |
| + * empty string. |
| + * |
| + * Each query component will be decoded using [encoding]. The default encoding |
| + * is UTF-8. |
| + */ |
| + static Map<String, String> splitQueryString(String query, |
| + {Encoding encoding: UTF8}) { |
| + return query.split("&").fold({}, (map, element) { |
| + int index = element.indexOf("="); |
| + if (index == -1) { |
| + if (element != "") { |
| + map[decodeQueryComponent(element, encoding: encoding)] = ""; |
| } |
| - port = _makePort(portNumber, scheme); |
| - hostEnd = lastColon; |
| + } else if (index != 0) { |
| + var key = element.substring(0, index); |
| + var value = element.substring(index + 1); |
| + map[decodeQueryComponent(key, encoding: encoding)] = |
| + decodeQueryComponent(value, encoding: encoding); |
| } |
| - host = _makeHost(uri, hostStart, hostEnd, true); |
| - if (index < end) { |
| - char = uri.codeUnitAt(index); |
| + return map; |
| + }); |
| + } |
| + |
| + |
| + /** |
| + * Parse the [host] as an IP version 4 (IPv4) address, returning the address |
| + * as a list of 4 bytes in network byte order (big endian). |
| + * |
| + * Throws a [FormatException] if [host] is not a valid IPv4 address |
| + * representation. |
| + */ |
| + static List<int> parseIPv4Address(String host) { |
| + void error(String msg) { |
| + throw new FormatException('Illegal IPv4 address, $msg', host); |
| + } |
| + var bytes = host.split('.'); |
| + if (bytes.length != 4) { |
| + error('IPv4 address should contain exactly 4 parts'); |
| + } |
| + var result = new Uint8List(4); |
| + for (int i = 0; i < 4; i++) { |
| + var byteString = bytes[i]; |
| + int byte = int.parse(byteString); |
| + if (byte < 0 || byte > 255) { |
| + error('each part must be in the range of `0..255`'); |
| } |
| + result[i] = byte; |
| } |
| + return result; |
| + } |
| - // 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; |
| - |
| - // 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; |
| + /** |
| + * Parse the [host] as an IP version 6 (IPv6) address, returning the address |
| + * as a list of 16 bytes in network byte order (big endian). |
| + * |
| + * Throws a [FormatException] if [host] is not a valid IPv6 address |
| + * representation. |
| + * |
| + * Acts on the substring from [start] to [end]. If [end] is omitted, it |
| + * defaults ot the end of the string. |
| + * |
| + * Some examples of IPv6 addresses: |
| + * * ::1 |
| + * * FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 |
| + * * 3ffe:2a00:100:7031::1 |
| + * * ::FFFF:129.144.52.38 |
| + * * 2010:836B:4179::836B:4179 |
| + */ |
| + static List<int> parseIPv6Address(String host, [int start = 0, int end]) { |
| + if (end == null) end = host.length; |
| + // An IPv6 address consists of exactly 8 parts of 1-4 hex digits, seperated |
| + // by `:`'s, with the following exceptions: |
| + // |
| + // - One (and only one) wildcard (`::`) may be present, representing a fill |
| + // of 0's. The IPv6 `::` is thus 16 bytes of `0`. |
| + // - The last two parts may be replaced by an IPv4 address. |
| + void error(String msg, [position]) { |
| + throw new FormatException('Illegal IPv6 address, $msg', host, position); |
| + } |
| + int parseHex(int start, int end) { |
|
floitsch
2016/06/29 23:41:47
newline before and after nested functions.
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Done.
|
| + if (end - start > 4) { |
| + error('an IPv6 part can only contain a maximum of 4 hex digits', start); |
| } |
| - if (char == _SLASH) { |
| - state = (i == start) ? ALLOW_AUTH : IN_PATH; |
| - break; |
| + int value = int.parse(host.substring(start, end), radix: 16); |
| + if (value < 0 || value > (1 << 16) - 1) { |
| + error('each part must be in the range of `0x0..0xFFFF`', start); |
| } |
| - 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; |
| + return value; |
| + } |
| + if (host.length < 2) error('address is too short'); |
| + List<int> parts = []; |
| + bool wildcardSeen = false; |
| + int partStart = start; |
| + // Parse all parts, except a potential last one. |
| + for (int i = start; i < end; i++) { |
| + if (host.codeUnitAt(i) == _COLON) { |
| + if (i == start) { |
| + // If we see a `:` in the beginning, expect wildcard. |
| + i++; |
| + if (host.codeUnitAt(i) != _COLON) { |
| + error('invalid start colon.', i); |
| + } |
| + partStart = i; |
| } |
| - 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 (i == partStart) { |
| + // Wildcard. We only allow one. |
| + if (wildcardSeen) { |
| + error('only one wildcard `::` is allowed', i); |
| } |
| + wildcardSeen = true; |
| + parts.add(-1); |
| + } else { |
| + // Found a single colon. Parse [partStart..i] as a hex entry. |
| + parts.add(parseHex(partStart, i)); |
| } |
| - break; |
| + partStart = i + 1; |
| } |
| - 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 (parts.length == 0) error('too few parts'); |
| + bool atEnd = (partStart == end); |
| + bool isLastWildcard = (parts.last == -1); |
| + if (atEnd && !isLastWildcard) { |
| + error('expected a part after last `:`', end); |
| + } |
| + if (!atEnd) { |
| + try { |
|
floitsch
2016/06/29 23:41:47
I would assume that the try/catch costs performanc
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Rewritten to not use try/catch.
This means that th
|
| + parts.add(parseHex(partStart, end)); |
| + } catch (e) { |
| + // Failed to parse the last chunk as hex. Try IPv4. |
| + try { |
| + List<int> last = parseIPv4Address(host.substring(partStart, end)); |
| + parts.add(last[0] << 8 | last[1]); |
| + parts.add(last[2] << 8 | last[3]); |
| + } catch (e) { |
| + error('invalid end of IPv6 address.', partStart); |
| } |
| - if (char == _QUESTION || char == _NUMBER_SIGN || char == EOI) { |
| - state = NOT_IN_PATH; |
| - } else { |
| - state = IN_PATH; |
| + } |
| + } |
| + if (wildcardSeen) { |
| + if (parts.length > 7) { |
| + error('an address with a wildcard must have less than 7 parts'); |
| + } |
| + } else if (parts.length != 8) { |
| + error('an address without a wildcard must contain exactly 8 parts'); |
| + } |
| + List<int> bytes = new Uint8List(16); |
| + for (int i = 0, index = 0; i < parts.length; i++) { |
| + int value = parts[i]; |
| + if (value == -1) { |
| + int wildCardLength = 9 - parts.length; |
| + for (int j = 0; j < wildCardLength; j++) { |
| + bytes[index] = 0; |
| + bytes[index + 1] = 0; |
| + index += 2; |
| } |
| + } else { |
| + bytes[index] = value >> 8; |
| + bytes[index + 1] = value & 0xff; |
| + index += 2; |
| } |
| } |
| + return bytes; |
| + } |
| +} |
| - 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; |
| +class _Uri implements Uri { |
| + // We represent the missing scheme as an empty string. |
| + // A valid scheme cannot be empty. |
| + final String scheme; |
| + |
| + /** |
| + * The user-info part of the authority. |
| + * |
| + * Does not distinguish between an empty user-info and an absent one. |
| + * The value is always non-null. |
| + * Is considered absent if [_host] is `null`. |
| + */ |
| + final String _userInfo; |
| + |
| + /** |
| + * The host name of the URI. |
| + * |
| + * Set to `null` if there is no authority in the URI. |
| + * The host name is the only mandatory part of an authority, so we use |
| + * it to mark whether an authority part was present or not. |
| + */ |
| + final String _host; |
| + |
| + /** |
| + * The port number part of the authority. |
| + * |
| + * The port. Set to null if there is no port. Normalized to null if |
| + * the port is the default port for the scheme. |
| + */ |
| + int _port; |
| + |
| + /** |
| + * The path of the URI. |
| + * |
| + * Always non-null. |
| + */ |
| + String _path; |
| + |
| + // The query content, or null if there is no query. |
| + final String _query; |
| + |
| + // The fragment content, or null if there is no fragment. |
| + final String _fragment; |
| + |
| + /** |
| + * Cache the computed return value of [pathSegements]. |
| + */ |
| + 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; |
| + Map<String, List<String>> _queryParameterLists; |
| + |
| + /// Internal non-verifying constructor. Only call with validated arguments. |
| + _Uri._internal(this.scheme, |
| + this._userInfo, |
| + this._host, |
| + this._port, |
| + this._path, |
| + this._query, |
| + this._fragment); |
| + |
| + /// Create a [_Uri] from parts of [uri]. |
| + /// |
| + /// The parameters specify the start/end of particular components of the URI. |
| + /// The [scheme] may contain a string representing a normalized scheme |
| + /// component if one has already been discovered. |
| + factory _Uri.notSimple(String uri, int start, int end, int schemeEnd, |
| + int hostStart, int portStart, int pathStart, |
| + int queryStart, int fragmentStart, String scheme) { |
| + if (scheme == null) { |
| + scheme = ""; |
| + if (schemeEnd > start) { |
| + scheme = _makeScheme(uri, start, schemeEnd); |
| + } else if (schemeEnd == start) { |
| + _fail(uri, start, "Invalid empty scheme"); |
| + } |
| + } |
| + String userInfo = ""; |
| + String host; |
| + int port; |
| + if (hostStart > start) { |
| + int userInfoStart = schemeEnd + 3; |
| + if (userInfoStart < hostStart) { |
| + userInfo = _makeUserInfo(uri, userInfoStart, hostStart - 1); |
| } |
| - state = NOT_IN_PATH; |
| + 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); |
| + } |
| + } |
| + 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, |
| + host, |
| + port, |
| + path, |
| + query, |
| + fragment); |
| + } |
| - assert(state == NOT_IN_PATH); |
| + /// Implementation of [Uri.Uri]. |
| + factory _Uri({String scheme, |
| + String userInfo, |
| + String host, |
| + int port, |
| + String path, |
| + Iterable<String> pathSegments, |
| + String query, |
| + Map<String, dynamic/*String|Iterable<String>*/> queryParameters, |
| + String fragment}) { |
| + scheme = _makeScheme(scheme, 0, _stringOrNullLength(scheme)); |
| + userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo)); |
| + host = _makeHost(host, 0, _stringOrNullLength(host), false); |
| + // Special case this constructor for backwards compatibility. |
| + if (query == "") query = null; |
| + query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters); |
| + fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment)); |
| + port = _makePort(port, scheme); |
| + bool isFile = (scheme == "file"); |
| + if (host == null && |
| + (userInfo.isNotEmpty || port != null || isFile)) { |
| + host = ""; |
| + } |
| 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; |
| - } |
| - } |
| - if (numberSignIndex < 0) { |
| - query = _makeQuery(uri, index + 1, end, null); |
| - } else { |
| - query = _makeQuery(uri, index + 1, numberSignIndex, null); |
| - fragment = _makeFragment(uri, numberSignIndex + 1, end); |
| - } |
| - } else if (char == _NUMBER_SIGN) { |
| - fragment = _makeFragment(uri, index + 1, end); |
| + path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, |
| + 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); |
| + return new _Uri._internal(scheme, userInfo, host, port, |
| + path, query, fragment); |
| } |
| + /// Implementation of [Uri.http]. |
| + factory _Uri.http(String authority, |
| + String unencodedPath, |
| + [Map<String, String> queryParameters]) { |
| + return _makeHttpUri("http", authority, unencodedPath, queryParameters); |
| + } |
| + |
| + /// Implementation of [Uri.https]. |
| + factory _Uri.https(String authority, |
| + String unencodedPath, |
| + [Map<String, String> queryParameters]) { |
| + return _makeHttpUri("https", authority, unencodedPath, queryParameters); |
| + } |
| + |
| + String get authority { |
| + if (!hasAuthority) return ""; |
| + var sb = new StringBuffer(); |
| + _writeAuthority(sb); |
| + return sb.toString(); |
| + } |
| + |
| + String get userInfo => _userInfo; |
| + |
| + String get host { |
| + if (_host == null) return ""; |
| + if (_host.startsWith('[')) { |
| + return _host.substring(1, _host.length - 1); |
| + } |
| + return _host; |
| + } |
| + |
| + int get port { |
| + if (_port == null) return _defaultPort(scheme); |
| + return _port; |
| + } |
| + |
| + // The default port for the scheme of this Uri.. |
|
floitsch
2016/06/29 23:41:47
Remove trailing ".".
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Done.
|
| + static int _defaultPort(String scheme) { |
| + if (scheme == "http") return 80; |
| + if (scheme == "https") return 443; |
| + return 0; |
| + } |
| + |
| + String get path => _path; |
| + |
| + String get query => (_query == null) ? "" : _query; |
|
floitsch
2016/06/29 23:41:47
_query ?? ""
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Done.
|
| + |
| + String get fragment => (_fragment == null) ? "" : _fragment; |
|
floitsch
2016/06/29 23:41:47
_fragment ?? ""
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Done.
|
| + |
| // Report a parse failure. |
| static void _fail(String uri, int index, String message) { |
| throw new FormatException(message, uri, index); |
| @@ -612,7 +1460,8 @@ class Uri { |
| // Split off the user info. |
| bool hasUserInfo = false; |
| for (int i = 0; i < authority.length; i++) { |
| - if (authority.codeUnitAt(i) == _AT_SIGN) { |
| + const int atSign = 0x40; |
| + if (authority.codeUnitAt(i) == atSign) { |
| hasUserInfo = true; |
| userInfo = authority.substring(0, i); |
| hostStart = i + 1; |
| @@ -630,7 +1479,7 @@ class Uri { |
| throw new FormatException("Invalid IPv6 host entry.", |
| authority, hostStart); |
| } |
| - parseIPv6Address(authority, hostStart + 1, hostEnd); |
| + Uri.parseIPv6Address(authority, hostStart + 1, hostEnd); |
| hostEnd++; // Skip the closing bracket. |
| if (hostEnd != authority.length && |
| authority.codeUnitAt(hostEnd) != _COLON) { |
| @@ -658,185 +1507,22 @@ class Uri { |
| queryParameters: queryParameters); |
| } |
| - /** |
| - * Creates a new file URI from an absolute or relative file path. |
| - * |
| - * The file path is passed in [path]. |
| - * |
| - * This path is interpreted using either Windows or non-Windows |
| - * semantics. |
| - * |
| - * With non-Windows semantics the slash ("/") is used to separate |
| - * path segments. |
| - * |
| - * With Windows semantics, backslash ("\") and forward-slash ("/") |
| - * are used to separate path segments, except if the path starts |
| - * with "\\?\" in which case, only backslash ("\") separates path |
| - * segments. |
| - * |
| - * If the path starts with a path separator an absolute URI is |
| - * created. Otherwise a relative URI is created. One exception from |
| - * this rule is that when Windows semantics is used and the path |
| - * starts with a drive letter followed by a colon (":") and a |
| - * path separator then an absolute URI is created. |
| - * |
| - * The default for whether to use Windows or non-Windows semantics |
| - * determined from the platform Dart is running on. When running in |
| - * the standalone VM this is detected by the VM based on the |
| - * operating system. When running in a browser non-Windows semantics |
| - * is always used. |
| - * |
| - * To override the automatic detection of which semantics to use pass |
| - * a value for [windows]. Passing `true` will use Windows |
| - * semantics and passing `false` will use non-Windows semantics. |
| - * |
| - * Examples using non-Windows semantics: |
| - * |
| - * ``` |
| - * // xxx/yyy |
| - * new Uri.file("xxx/yyy", windows: false); |
| - * |
| - * // xxx/yyy/ |
| - * new Uri.file("xxx/yyy/", windows: false); |
| - * |
| - * // file:///xxx/yyy |
| - * new Uri.file("/xxx/yyy", windows: false); |
| - * |
| - * // file:///xxx/yyy/ |
| - * new Uri.file("/xxx/yyy/", windows: false); |
| - * |
| - * // C: |
| - * new Uri.file("C:", windows: false); |
| - * ``` |
| - * |
| - * Examples using Windows semantics: |
| - * |
| - * ``` |
| - * // xxx/yyy |
| - * new Uri.file(r"xxx\yyy", windows: true); |
| - * |
| - * // xxx/yyy/ |
| - * new Uri.file(r"xxx\yyy\", windows: true); |
| - * |
| - * file:///xxx/yyy |
| - * new Uri.file(r"\xxx\yyy", windows: true); |
| - * |
| - * file:///xxx/yyy/ |
| - * new Uri.file(r"\xxx\yyy/", windows: true); |
| - * |
| - * // file:///C:/xxx/yyy |
| - * new Uri.file(r"C:\xxx\yyy", windows: true); |
| - * |
| - * // This throws an error. A path with a drive letter is not absolute. |
| - * new Uri.file(r"C:", windows: true); |
| - * |
| - * // This throws an error. A path with a drive letter is not absolute. |
| - * new Uri.file(r"C:xxx\yyy", windows: true); |
| - * |
| - * // file://server/share/file |
| - * new Uri.file(r"\\server\share\file", windows: true); |
| - * ``` |
| - * |
| - * If the path passed is not a legal file path [ArgumentError] is thrown. |
| - */ |
| - factory Uri.file(String path, {bool windows}) { |
| - windows = (windows == null) ? Uri._isWindows : windows; |
| + /// Implementation of [Uri.file]. |
| + factory _Uri.file(String path, {bool windows}) { |
| + windows = (windows == null) ? _Uri._isWindows : windows; |
| return windows ? _makeWindowsFileUrl(path, false) |
| : _makeFileUri(path, false); |
| } |
| - /** |
| - * Like [Uri.file] except that a non-empty URI path ends in a slash. |
| - * |
| - * If [path] is not empty, and it doesn't end in a directory separator, |
| - * then a slash is added to the returned URI's path. |
| - * In all other cases, the result is the same as returned by `Uri.file`. |
| - */ |
| - factory Uri.directory(String path, {bool windows}) { |
| - windows = (windows == null) ? Uri._isWindows : windows; |
| + /// Implementation of [Uri.directory]. |
| + factory _Uri.directory(String path, {bool windows}) { |
| + windows = (windows == null) ? _Uri._isWindows : windows; |
| return windows ? _makeWindowsFileUrl(path, true) |
| : _makeFileUri(path, true); |
| } |
| - /** |
| - * Creates a `data:` URI containing the [content] string. |
| - * |
| - * Converts the content to a bytes using [encoding] or the charset specified |
| - * in [parameters] (defaulting to US-ASCII if not specified or unrecognized), |
| - * then encodes the bytes into the resulting data URI. |
| - * |
| - * Defaults to encoding using percent-encoding (any non-ASCII or non-URI-valid |
| - * bytes is replaced by a percent encoding). If [base64] is true, the bytes |
| - * are instead encoded using [BASE64]. |
| - * |
| - * If [encoding] is not provided and [parameters] has a `charset` entry, |
| - * that name is looked up using [Encoding.getByName], |
| - * and if the lookup returns an encoding, that encoding is used to convert |
| - * [content] to bytes. |
| - * If providing both an [encoding] and a charset [parameter], they should |
| - * agree, otherwise decoding won't be able to use the charset parameter |
| - * to determine the encoding. |
| - * |
| - * If [mimeType] and/or [parameters] are supplied, they are added to the |
| - * created URI. If any of these contain characters that are not allowed |
| - * in the data URI, the character is percent-escaped. If the character is |
| - * non-ASCII, it is first UTF-8 encoded and then the bytes are percent |
| - * encoded. An omitted [mimeType] in a data URI means `text/plain`, just |
| - * as an omitted `charset` parameter defaults to meaning `US-ASCII`. |
| - * |
| - * To read the content back, use [UriData.contentAsString]. |
| - */ |
| - factory Uri.dataFromString(String content, |
| - {String mimeType, |
| - Encoding encoding, |
| - Map<String, String> parameters, |
| - bool base64: false}) { |
| - UriData data = new UriData.fromString(content, |
| - mimeType: mimeType, |
| - encoding: encoding, |
| - parameters: parameters, |
| - base64: base64); |
| - return data.uri; |
| - } |
| - |
| - /** |
| - * Creates a `data:` URI containing an encoding of [bytes]. |
| - * |
| - * Defaults to Base64 encoding the bytes, but if [percentEncoded] |
| - * is `true`, the bytes will instead be percent encoded (any non-ASCII |
| - * or non-valid-ASCII-character byte is replaced by a percent encoding). |
| - * |
| - * To read the bytes back, use [UriData.contentAsBytes]. |
| - * |
| - * It defaults to having the mime-type `application/octet-stream`. |
| - * The [mimeType] and [parameters] are added to the created URI. |
| - * If any of these contain characters that are not allowed |
| - * in the data URI, the character is percent-escaped. If the character is |
| - * non-ASCII, it is first UTF-8 encoded and then the bytes are percent |
| - * encoded. |
| - */ |
| - factory Uri.dataFromBytes(List<int> bytes, |
| - {mimeType: "application/octet-stream", |
| - Map<String, String> parameters, |
| - percentEncoded: false}) { |
| - UriData data = new UriData.fromBytes(bytes, |
| - mimeType: mimeType, |
| - parameters: parameters, |
| - percentEncoded: percentEncoded); |
| - return data.uri; |
| - } |
| - |
| - /** |
| - * Returns the natural base URI for the current platform. |
| - * |
| - * When running in a browser this is the current URL (from |
| - * `window.location.href`). |
| - * |
| - * When not running in a browser this is the file URI referencing |
| - * the current working directory. |
| - */ |
| - external static Uri get base; |
| + /// Used internally in path-related constructors. |
| external static bool get _isWindows; |
| static _checkNonWindowsPathReservedCharacters(List<String> segments, |
| @@ -969,46 +1655,6 @@ class Uri { |
| } |
| } |
| - /** |
| - * Returns a new `Uri` based on this one, but with some parts replaced. |
| - * |
| - * This method takes the same parameters as the [new Uri] constructor, |
| - * and they have the same meaning. |
| - * |
| - * At most one of [path] and [pathSegments] must be provided. |
| - * Likewise, at most one of [query] and [queryParameters] must be provided. |
| - * |
| - * Each part that is not provided will default to the corresponding |
| - * value from this `Uri` instead. |
| - * |
| - * This method is different from [Uri.resolve] which overrides in a |
| - * hierarchial manner, |
| - * and can instead replace each part of a `Uri` individually. |
| - * |
| - * Example: |
| - * |
| - * Uri uri1 = Uri.parse("a://b@c:4/d/e?f#g"); |
| - * Uri uri2 = uri1.replace(scheme: "A", path: "D/E/E", fragment: "G"); |
| - * print(uri2); // prints "A://b@c:4/D/E/E/?f#G" |
| - * |
| - * This method acts similarly to using the `new Uri` constructor with |
| - * some of the arguments taken from this `Uri` . Example: |
| - * |
| - * Uri uri3 = new Uri( |
| - * scheme: "A", |
| - * userInfo: uri1.userInfo, |
| - * host: uri1.host, |
| - * port: uri1.port, |
| - * path: "D/E/E", |
| - * query: uri1.query, |
| - * fragment: "G"); |
| - * print(uri3); // prints "A://b@c:4/D/E/E/?f#G" |
| - * print(uri2 == uri3); // prints true. |
| - * |
| - * Using this method can be seen as a shorthand for the `Uri` constructor |
| - * call above, but may also be slightly faster because the parts taken |
| - * from this `Uri` need not be checked for validity again. |
| - */ |
| Uri replace({String scheme, |
| String userInfo, |
| String host, |
| @@ -1075,29 +1721,16 @@ class Uri { |
| fragment = this._fragment; |
| } |
| - return new Uri._internal( |
| + return new _Uri._internal( |
| scheme, userInfo, host, port, path, query, fragment); |
| } |
| - /** |
| - * Returns a `Uri` that differs from this only in not having a fragment. |
| - * |
| - * If this `Uri` does not have a fragment, it is itself returned. |
| - */ |
| Uri removeFragment() { |
| if (!this.hasFragment) return this; |
| - return new Uri._internal(scheme, _userInfo, _host, _port, |
| + return new _Uri._internal(scheme, _userInfo, _host, _port, |
| _path, _query, null); |
| } |
| - /** |
| - * Returns the URI path split into its segments. Each of the segments in the |
| - * returned list have been decoded. If the path is empty the empty list will |
| - * be returned. A leading slash `/` does not affect the segments returned. |
| - * |
| - * The returned list is unmodifiable and will throw [UnsupportedError] on any |
| - * calls that would mutate it. |
| - */ |
| List<String> get pathSegments { |
| var result = _pathSegments; |
| if (result != null) return result; |
| @@ -1114,43 +1747,14 @@ class Uri { |
| return result; |
| } |
| - /** |
| - * Returns the URI query split into a map according to the rules |
| - * specified for FORM post in the [HTML 4.01 specification section |
| - * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). |
| - * Each key and value in the returned map has been decoded. |
| - * If there is no query the empty map is returned. |
| - * |
| - * Keys in the query string that have no value are mapped to the |
| - * empty string. |
| - * If a key occurs more than once in the query string, it is mapped to |
| - * an arbitrary choice of possible value. |
| - * The [queryParametersAll] getter can provide a map |
| - * that maps keys to all of their values. |
| - * |
| - * The returned map is unmodifiable. |
| - */ |
| Map<String, String> get queryParameters { |
| if (_queryParameters == null) { |
| _queryParameters = |
| - new UnmodifiableMapView<String, String>(splitQueryString(query)); |
| + new UnmodifiableMapView<String, String>(Uri.splitQueryString(query)); |
| } |
| return _queryParameters; |
| } |
| - /** |
| - * Returns the URI query split into a map according to the rules |
| - * specified for FORM post in the [HTML 4.01 specification section |
| - * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). |
| - * Each key and value in the returned map has been decoded. If there is no |
| - * query the empty map is returned. |
| - * |
| - * Keys are mapped to lists of their values. If a key occurs only once, |
| - * its value is a singleton list. If a key occurs with no value, the |
| - * empty string is used as the value for that occurrence. |
| - * |
| - * The returned map and the lists it contains are unmodifiable. |
| - */ |
| Map<String, List<String>> get queryParametersAll { |
| if (_queryParameterLists == null) { |
| Map queryParameterLists = _splitQueryStringAll(query); |
| @@ -1164,22 +1768,6 @@ class Uri { |
| return _queryParameterLists; |
| } |
| - /** |
| - * Returns a URI where the path has been normalized. |
| - * |
| - * A normalized path does not contain `.` segments or non-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 `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 = _normalizePath(_path, scheme, hasAuthority); |
| if (identical(path, _path)) return this; |
| @@ -1212,7 +1800,7 @@ class Uri { |
| if (host.codeUnitAt(end - 1) != _RIGHT_BRACKET) { |
| _fail(host, start, 'Missing end `]` to match `[` in host'); |
| } |
| - parseIPv6Address(host, start + 1, end - 1); |
| + Uri.parseIPv6Address(host, start + 1, end - 1); |
| // RFC 5952 requires hex digits to be lower case. |
| return host.substring(start, end).toLowerCase(); |
| } |
| @@ -1220,7 +1808,7 @@ class Uri { |
| // TODO(lrn): skip if too short to be a valid IPv6 address? |
| for (int i = start; i < end; i++) { |
| if (host.codeUnitAt(i) == _COLON) { |
| - parseIPv6Address(host, start, end); |
| + Uri.parseIPv6Address(host, start, end); |
| return '[$host]'; |
| } |
| } |
| @@ -1333,6 +1921,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; |
| } |
| @@ -1419,8 +2018,6 @@ class Uri { |
| return _normalize(fragment, start, end, _queryCharTable); |
| } |
| - static int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; |
| - |
| /** |
| * Performs RFC 3986 Percent-Encoding Normalization. |
| * |
| @@ -1465,10 +2062,11 @@ class Uri { |
| // Converts a UTF-16 code-unit to its value as a hex digit. |
| // Returns -1 for non-hex digits. |
| static int _parseHexDigit(int char) { |
| - int digit = char ^ Uri._ZERO; |
| + const int zeroDigit = 0x30; |
| + int digit = char ^ zeroDigit; |
| if (digit <= 9) return digit; |
| int lowerCase = char | 0x20; |
| - if (Uri._LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_F) { |
| + if (_LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_F) { |
| return lowerCase - (_LOWER_CASE_A - 10); |
| } |
| return -1; |
| @@ -1703,34 +2301,10 @@ class Uri { |
| return output.join("/"); |
| } |
| - /** |
| - * Resolve [reference] as an URI relative to `this`. |
| - * |
| - * First turn [reference] into a URI using [Uri.parse]. Then resolve the |
| - * resulting URI relative to `this`. |
| - * |
| - * Returns the resolved URI. |
| - * |
| - * See [resolveUri] for details. |
| - */ |
| Uri resolve(String reference) { |
| return resolveUri(Uri.parse(reference)); |
| } |
| - /** |
| - * Resolve [reference] as an URI relative to `this`. |
| - * |
| - * Returns the resolved URI. |
| - * |
| - * 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. |
| String targetScheme; |
| @@ -1788,8 +2362,9 @@ class Uri { |
| 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 "..". |
| + // Non-RFC 3986 behavior. |
| + // If both base and reference are relative paths, |
| + // allow the merged path to start with "..". |
| // The RFC only specifies the case where the base has a scheme. |
| targetPath = _normalizeRelativePath(mergedPath); |
| } |
| @@ -1800,63 +2375,29 @@ class Uri { |
| } |
| } |
| String fragment = reference.hasFragment ? reference.fragment : null; |
| - return new Uri._internal(targetScheme, |
| - targetUserInfo, |
| - targetHost, |
| - targetPort, |
| - targetPath, |
| - targetQuery, |
| - fragment); |
| + return new _Uri._internal(targetScheme, |
| + targetUserInfo, |
| + targetHost, |
| + targetPort, |
| + targetPath, |
| + targetQuery, |
| + fragment); |
| } |
| - /** |
| - * 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; |
| - /** |
| - * Returns whether the URI has an explicit port. |
| - * |
| - * If the port number is the default port number |
| - * (zero for unrecognized schemes, with http (80) and https (443) being |
| - * recognized), |
| - * then the port is made implicit and omitted from the URI. |
| - */ |
| bool get hasPort => _port != null; |
| - /** |
| - * Returns whether the URI has a query part. |
| - */ |
| bool get hasQuery => _query != null; |
| - /** |
| - * Returns whether the URI has a fragment part. |
| - */ |
| 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. |
| - * |
| - * It is an error if the scheme is not "http" or "https". |
| - * |
| - * See: http://www.w3.org/TR/2011/WD-html5-20110405/origin-0.html#origin |
| - */ |
| String get origin { |
| if (scheme == "" || _host == null || _host == "") { |
| throw new StateError("Cannot use origin without a scheme: $this"); |
| @@ -1869,69 +2410,6 @@ class Uri { |
| return "$scheme://$_host:$_port"; |
| } |
| - /** |
| - * Returns the file path from a file URI. |
| - * |
| - * The returned path has either Windows or non-Windows |
| - * semantics. |
| - * |
| - * For non-Windows semantics the slash ("/") is used to separate |
| - * path segments. |
| - * |
| - * For Windows semantics the backslash ("\") separator is used to |
| - * separate path segments. |
| - * |
| - * If the URI is absolute the path starts with a path separator |
| - * unless Windows semantics is used and the first path segment is a |
| - * drive letter. When Windows semantics is used a host component in |
| - * the uri in interpreted as a file server and a UNC path is |
| - * returned. |
| - * |
| - * The default for whether to use Windows or non-Windows semantics |
| - * determined from the platform Dart is running on. When running in |
| - * the standalone VM this is detected by the VM based on the |
| - * operating system. When running in a browser non-Windows semantics |
| - * is always used. |
| - * |
| - * To override the automatic detection of which semantics to use pass |
| - * a value for [windows]. Passing `true` will use Windows |
| - * semantics and passing `false` will use non-Windows semantics. |
| - * |
| - * If the URI ends with a slash (i.e. the last path component is |
| - * empty) the returned file path will also end with a slash. |
| - * |
| - * With Windows semantics URIs starting with a drive letter cannot |
| - * be relative to the current drive on the designated drive. That is |
| - * for the URI `file:///c:abc` calling `toFilePath` will throw as a |
| - * path segment cannot contain colon on Windows. |
| - * |
| - * Examples using non-Windows semantics (resulting of calling |
| - * toFilePath in comment): |
| - * |
| - * Uri.parse("xxx/yyy"); // xxx/yyy |
| - * Uri.parse("xxx/yyy/"); // xxx/yyy/ |
| - * Uri.parse("file:///xxx/yyy"); // /xxx/yyy |
| - * Uri.parse("file:///xxx/yyy/"); // /xxx/yyy/ |
| - * Uri.parse("file:///C:"); // /C: |
| - * Uri.parse("file:///C:a"); // /C:a |
| - * |
| - * Examples using Windows semantics (resulting URI in comment): |
| - * |
| - * Uri.parse("xxx/yyy"); // xxx\yyy |
| - * Uri.parse("xxx/yyy/"); // xxx\yyy\ |
| - * Uri.parse("file:///xxx/yyy"); // \xxx\yyy |
| - * Uri.parse("file:///xxx/yyy/"); // \xxx\yyy/ |
| - * Uri.parse("file:///C:/xxx/yyy"); // C:\xxx\yyy |
| - * Uri.parse("file:C:xxx/yyy"); // Throws as a path segment |
| - * // cannot contain colon on Windows. |
| - * Uri.parse("file://server/share/file"); // \\server\share\file |
| - * |
| - * If the URI is not a file URI calling this throws |
| - * [UnsupportedError]. |
| - * |
| - * If the URI cannot be converted to a file path calling this throws |
| - * [UnsupportedError]. |
| - */ |
| String toFilePath({bool windows}) { |
| if (scheme != "" && scheme != "file") { |
| throw new UnsupportedError( |
| @@ -1946,25 +2424,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 +2452,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 +2496,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,192 +2506,32 @@ 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) { |
| - if (other is! Uri) return false; |
| - Uri uri = other; |
| - return scheme == uri.scheme && |
| - hasAuthority == uri.hasAuthority && |
| - userInfo == uri.userInfo && |
| - host == uri.host && |
| - port == uri.port && |
| - path == uri.path && |
| - hasQuery == uri.hasQuery && |
| - query == uri.query && |
| - hasFragment == uri.hasFragment && |
| - fragment == uri.fragment; |
| + if (identical(this, other)) return true; |
| + if (other is Uri) { |
| + Uri uri = other; |
| + return scheme == uri.scheme && |
| + hasAuthority == uri.hasAuthority && |
| + userInfo == uri.userInfo && |
| + host == uri.host && |
| + port == uri.port && |
| + path == uri.path && |
| + hasQuery == uri.hasQuery && |
| + query == uri.query && |
| + hasFragment == uri.hasFragment && |
| + fragment == uri.fragment; |
| + } |
| + return false; |
| } |
| 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); |
| - } |
| - } |
| - |
| - /** |
| - * Encode the string [component] using percent-encoding to make it |
| - * safe for literal use as a URI component. |
| - * |
| - * All characters except uppercase and lowercase letters, digits and |
| - * the characters `-_.!~*'()` are percent-encoded. This is the |
| - * set of characters specified in RFC 2396 and the which is |
| - * specified for the encodeUriComponent in ECMA-262 version 5.1. |
| - * |
| - * When manually encoding path segments or query components remember |
| - * to encode each part separately before building the path or query |
| - * string. |
| - * |
| - * For encoding the query part consider using |
| - * [encodeQueryComponent]. |
| - * |
| - * To avoid the need for explicitly encoding use the [pathSegments] |
| - * and [queryParameters] optional named arguments when constructing |
| - * a [Uri]. |
| - */ |
| - static String encodeComponent(String component) { |
| - return _uriEncode(_unreserved2396Table, component, UTF8, false); |
| - } |
| - |
| - /** |
| - * Encode the string [component] according to the HTML 4.01 rules |
| - * for encoding the posting of a HTML form as a query string |
| - * component. |
| - * |
| - * Encode the string [component] according to the HTML 4.01 rules |
| - * for encoding the posting of a HTML form as a query string |
| - * component. |
| - |
| - * The component is first encoded to bytes using [encoding]. |
| - * The default is to use [UTF8] encoding, which preserves all |
| - * the characters that don't need encoding. |
| - |
| - * Then the resulting bytes are "percent-encoded". This transforms |
| - * spaces (U+0020) to a plus sign ('+') and all bytes that are not |
| - * the ASCII decimal digits, letters or one of '-._~' are written as |
| - * a percent sign '%' followed by the two-digit hexadecimal |
| - * representation of the byte. |
| - |
| - * Note that the set of characters which are percent-encoded is a |
| - * superset of what HTML 4.01 requires, since it refers to RFC 1738 |
| - * for reserved characters. |
| - * |
| - * When manually encoding query components remember to encode each |
| - * part separately before building the query string. |
| - * |
| - * To avoid the need for explicitly encoding the query use the |
| - * [queryParameters] optional named arguments when constructing a |
| - * [Uri]. |
| - * |
| - * See http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 for more |
| - * details. |
| - */ |
| - static String encodeQueryComponent(String component, |
| - {Encoding encoding: UTF8}) { |
| - return _uriEncode(_unreservedTable, component, encoding, true); |
| - } |
| - |
| - /** |
| - * Decodes the percent-encoding in [encodedComponent]. |
| - * |
| - * Note that decoding a URI component might change its meaning as |
| - * some of the decoded characters could be characters with are |
| - * delimiters for a given URI componene type. Always split a URI |
| - * component using the delimiters for the component before decoding |
| - * the individual parts. |
| - * |
| - * For handling the [path] and [query] components consider using |
| - * [pathSegments] and [queryParameters] to get the separated and |
| - * decoded component. |
| - */ |
| - static String decodeComponent(String encodedComponent) { |
| - return _uriDecode(encodedComponent, 0, encodedComponent.length, |
| - UTF8, false); |
| - } |
| - |
| - /** |
| - * Decodes the percent-encoding in [encodedComponent], converting |
| - * pluses to spaces. |
| - * |
| - * It will create a byte-list of the decoded characters, and then use |
| - * [encoding] to decode the byte-list to a String. The default encoding is |
| - * UTF-8. |
| - */ |
| - static String decodeQueryComponent( |
| - String encodedComponent, |
| - {Encoding encoding: UTF8}) { |
| - return _uriDecode(encodedComponent, 0, encodedComponent.length, |
| - encoding, true); |
| - } |
| - |
| - /** |
| - * Encode the string [uri] using percent-encoding to make it |
| - * safe for literal use as a full URI. |
| - * |
| - * All characters except uppercase and lowercase letters, digits and |
| - * the characters `!#$&'()*+,-./:;=?@_~` are percent-encoded. This |
| - * is the set of characters specified in in ECMA-262 version 5.1 for |
| - * the encodeURI function . |
| - */ |
| - static String encodeFull(String uri) { |
| - return _uriEncode(_encodeFullTable, uri, UTF8, false); |
| - } |
| - |
| - /** |
| - * Decodes the percent-encoding in [uri]. |
| - * |
| - * Note that decoding a full URI might change its meaning as some of |
| - * the decoded characters could be reserved characters. In most |
| - * cases an encoded URI should be parsed into components using |
| - * [Uri.parse] before decoding the separate components. |
| - */ |
| - static String decodeFull(String uri) { |
| - return _uriDecode(uri, 0, uri.length, UTF8, false); |
| - } |
| - |
| - /** |
| - * Returns the [query] split into a map according to the rules |
| - * specified for FORM post in the [HTML 4.01 specification section |
| - * 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4"). |
| - * Each key and value in the returned map has been decoded. If the [query] |
| - * is the empty string an empty map is returned. |
| - * |
| - * Keys in the query string that have no value are mapped to the |
| - * empty string. |
| - * |
| - * Each query component will be decoded using [encoding]. The default encoding |
| - * is UTF-8. |
| - */ |
| - static Map<String, String> splitQueryString(String query, |
| - {Encoding encoding: UTF8}) { |
| - return query.split("&").fold({}, (map, element) { |
| - int index = element.indexOf("="); |
| - if (index == -1) { |
| - if (element != "") { |
| - map[decodeQueryComponent(element, encoding: encoding)] = ""; |
| - } |
| - } else if (index != 0) { |
| - var key = element.substring(0, index); |
| - var value = element.substring(index + 1); |
| - map[Uri.decodeQueryComponent(key, encoding: encoding)] = |
| - decodeQueryComponent(value, encoding: encoding); |
| - } |
| - return map; |
| - }); |
| + return (_text ?? toString()).hashCode; |
| } |
| static List _createList() => []; |
| @@ -2251,175 +2574,6 @@ class Uri { |
| return result; |
| } |
| - /** |
| - * Parse the [host] as an IP version 4 (IPv4) address, returning the address |
| - * as a list of 4 bytes in network byte order (big endian). |
| - * |
| - * Throws a [FormatException] if [host] is not a valid IPv4 address |
| - * representation. |
| - */ |
| - static List<int> parseIPv4Address(String host) { |
| - void error(String msg) { |
| - throw new FormatException('Illegal IPv4 address, $msg'); |
| - } |
| - var bytes = host.split('.'); |
| - if (bytes.length != 4) { |
| - error('IPv4 address should contain exactly 4 parts'); |
| - } |
| - // TODO(ajohnsen): Consider using Uint8List. |
| - return bytes |
| - .map((byteString) { |
| - int byte = int.parse(byteString); |
| - if (byte < 0 || byte > 255) { |
| - error('each part must be in the range of `0..255`'); |
| - } |
| - return byte; |
| - }) |
| - .toList(); |
| - } |
| - |
| - /** |
| - * Parse the [host] as an IP version 6 (IPv6) address, returning the address |
| - * as a list of 16 bytes in network byte order (big endian). |
| - * |
| - * Throws a [FormatException] if [host] is not a valid IPv6 address |
| - * representation. |
| - * |
| - * Acts on the substring from [start] to [end]. If [end] is omitted, it |
| - * defaults ot the end of the string. |
| - * |
| - * Some examples of IPv6 addresses: |
| - * * ::1 |
| - * * FEDC:BA98:7654:3210:FEDC:BA98:7654:3210 |
| - * * 3ffe:2a00:100:7031::1 |
| - * * ::FFFF:129.144.52.38 |
| - * * 2010:836B:4179::836B:4179 |
| - */ |
| - static List<int> parseIPv6Address(String host, [int start = 0, int end]) { |
| - if (end == null) end = host.length; |
| - // An IPv6 address consists of exactly 8 parts of 1-4 hex digits, seperated |
| - // by `:`'s, with the following exceptions: |
| - // |
| - // - One (and only one) wildcard (`::`) may be present, representing a fill |
| - // of 0's. The IPv6 `::` is thus 16 bytes of `0`. |
| - // - The last two parts may be replaced by an IPv4 address. |
| - void error(String msg, [position]) { |
| - throw new FormatException('Illegal IPv6 address, $msg', host, position); |
| - } |
| - int parseHex(int start, int end) { |
| - if (end - start > 4) { |
| - error('an IPv6 part can only contain a maximum of 4 hex digits', start); |
| - } |
| - int value = int.parse(host.substring(start, end), radix: 16); |
| - if (value < 0 || value > (1 << 16) - 1) { |
| - error('each part must be in the range of `0x0..0xFFFF`', start); |
| - } |
| - return value; |
| - } |
| - if (host.length < 2) error('address is too short'); |
| - List<int> parts = []; |
| - bool wildcardSeen = false; |
| - int partStart = start; |
| - // Parse all parts, except a potential last one. |
| - for (int i = start; i < end; i++) { |
| - if (host.codeUnitAt(i) == _COLON) { |
| - if (i == start) { |
| - // If we see a `:` in the beginning, expect wildcard. |
| - i++; |
| - if (host.codeUnitAt(i) != _COLON) { |
| - error('invalid start colon.', i); |
| - } |
| - partStart = i; |
| - } |
| - if (i == partStart) { |
| - // Wildcard. We only allow one. |
| - if (wildcardSeen) { |
| - error('only one wildcard `::` is allowed', i); |
| - } |
| - wildcardSeen = true; |
| - parts.add(-1); |
| - } else { |
| - // Found a single colon. Parse [partStart..i] as a hex entry. |
| - parts.add(parseHex(partStart, i)); |
| - } |
| - partStart = i + 1; |
| - } |
| - } |
| - if (parts.length == 0) error('too few parts'); |
| - bool atEnd = (partStart == end); |
| - bool isLastWildcard = (parts.last == -1); |
| - if (atEnd && !isLastWildcard) { |
| - error('expected a part after last `:`', end); |
| - } |
| - if (!atEnd) { |
| - try { |
| - parts.add(parseHex(partStart, end)); |
| - } catch (e) { |
| - // Failed to parse the last chunk as hex. Try IPv4. |
| - try { |
| - List<int> last = parseIPv4Address(host.substring(partStart, end)); |
| - parts.add(last[0] << 8 | last[1]); |
| - parts.add(last[2] << 8 | last[3]); |
| - } catch (e) { |
| - error('invalid end of IPv6 address.', partStart); |
| - } |
| - } |
| - } |
| - if (wildcardSeen) { |
| - if (parts.length > 7) { |
| - error('an address with a wildcard must have less than 7 parts'); |
| - } |
| - } else if (parts.length != 8) { |
| - error('an address without a wildcard must contain exactly 8 parts'); |
| - } |
| - List<int> bytes = new Uint8List(16); |
| - for (int i = 0, index = 0; i < parts.length; i++) { |
| - int value = parts[i]; |
| - if (value == -1) { |
| - int wildCardLength = 9 - parts.length; |
| - for (int j = 0; j < wildCardLength; j++) { |
| - bytes[index] = 0; |
| - bytes[index + 1] = 0; |
| - index += 2; |
| - } |
| - } else { |
| - bytes[index] = value >> 8; |
| - bytes[index + 1] = value & 0xff; |
| - index += 2; |
| - } |
| - } |
| - return bytes; |
| - } |
| - |
| - // Frequently used character codes. |
| - static const int _SPACE = 0x20; |
| - static const int _DOUBLE_QUOTE = 0x22; |
| - static const int _NUMBER_SIGN = 0x23; |
| - static const int _PERCENT = 0x25; |
| - static const int _ASTERISK = 0x2A; |
| - static const int _PLUS = 0x2B; |
| - static const int _DOT = 0x2E; |
| - static const int _SLASH = 0x2F; |
| - static const int _ZERO = 0x30; |
| - static const int _NINE = 0x39; |
| - static const int _COLON = 0x3A; |
| - static const int _LESS = 0x3C; |
| - static const int _GREATER = 0x3E; |
| - static const int _QUESTION = 0x3F; |
| - static const int _AT_SIGN = 0x40; |
| - static const int _UPPER_CASE_A = 0x41; |
| - static const int _UPPER_CASE_F = 0x46; |
| - static const int _UPPER_CASE_Z = 0x5A; |
| - static const int _LEFT_BRACKET = 0x5B; |
| - static const int _BACKSLASH = 0x5C; |
| - static const int _RIGHT_BRACKET = 0x5D; |
| - static const int _LOWER_CASE_A = 0x61; |
| - static const int _LOWER_CASE_F = 0x66; |
| - static const int _LOWER_CASE_Z = 0x7A; |
| - static const int _BAR = 0x7C; |
| - |
| - static const String _hexDigits = "0123456789ABCDEF"; |
| - |
| external static String _uriEncode(List<int> canonicalTable, |
| String text, |
| Encoding encoding, |
| @@ -2941,13 +3095,13 @@ class UriData { |
| throw new ArgumentError.value(mimeType, "mimeType", |
| "Invalid MIME type"); |
| } |
| - buffer.write(Uri._uriEncode(_tokenCharTable, |
| - mimeType.substring(0, slashIndex), |
| - UTF8, false)); |
| + buffer.write(_Uri._uriEncode(_tokenCharTable, |
| + mimeType.substring(0, slashIndex), |
| + UTF8, false)); |
| buffer.write("/"); |
| - buffer.write(Uri._uriEncode(_tokenCharTable, |
| - mimeType.substring(slashIndex + 1), |
| - UTF8, false)); |
| + buffer.write(_Uri._uriEncode(_tokenCharTable, |
| + mimeType.substring(slashIndex + 1), |
| + UTF8, false)); |
| } |
| if (charsetName != null) { |
| if (indices != null) { |
| @@ -2955,7 +3109,7 @@ class UriData { |
| ..add(buffer.length + 8); |
| } |
| buffer.write(";charset="); |
| - buffer.write(Uri._uriEncode(_tokenCharTable, charsetName, UTF8, false)); |
| + buffer.write(_Uri._uriEncode(_tokenCharTable, charsetName, UTF8, false)); |
| } |
| parameters?.forEach((var key, var value) { |
| if (key.isEmpty) { |
| @@ -2968,10 +3122,10 @@ class UriData { |
| if (indices != null) indices.add(buffer.length); |
| buffer.write(';'); |
| // Encode any non-RFC2045-token character and both '%' and '#'. |
| - buffer.write(Uri._uriEncode(_tokenCharTable, key, UTF8, false)); |
| + buffer.write(_Uri._uriEncode(_tokenCharTable, key, UTF8, false)); |
| if (indices != null) indices.add(buffer.length); |
| buffer.write('='); |
| - buffer.write(Uri._uriEncode(_tokenCharTable, value, UTF8, false)); |
| + buffer.write(_Uri._uriEncode(_tokenCharTable, value, UTF8, false)); |
| }); |
| } |
| @@ -2988,7 +3142,7 @@ class UriData { |
| int slashIndex = -1; |
| for (int i = 0; i < mimeType.length; i++) { |
| var char = mimeType.codeUnitAt(i); |
| - if (char != Uri._SLASH) continue; |
| + if (char != _SLASH) continue; |
| if (slashIndex < 0) { |
| slashIndex = i; |
| continue; |
| @@ -3008,7 +3162,7 @@ class UriData { |
| * ```` |
| * |
| * where `type`, `subtype`, `attribute` and `value` are specified in RFC-2045, |
| - * and `data` is a sequnce of URI-characters (RFC-2396 `uric`). |
| + * and `data` is a sequence of URI-characters (RFC-2396 `uric`). |
| * |
| * This means that all the characters must be ASCII, but the URI may contain |
| * percent-escapes for non-ASCII byte values that need an interpretation |
| @@ -3019,13 +3173,22 @@ class UriData { |
| * and `,` delimiters. |
| * |
| * Accessing the individual parts may fail later if they turn out to have |
| - * content that can't be decoded sucessfully as a string. |
| + * content that can't be decoded successfully as a string. |
| */ |
| static UriData parse(String uri) { |
| - if (!uri.startsWith("data:")) { |
| - throw new FormatException("Does not start with 'data:'", uri, 0); |
| + if (uri.length >= 5) { |
| + int dataDelta = _startsWithData(uri, 0); |
|
floitsch
2016/06/29 23:41:47
startsWithData should return a bool, and you shoul
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Wouldn't work. I need to distinguish "data:" from
|
| + if (dataDelta == 0) { |
| + // Exact match on "data:". |
| + return _parse(uri, 5, null); |
| + } |
| + if (dataDelta == 0x20) { |
| + // Starts with a non-normalized "data" scheme containing upper-case |
| + // letters. Parse anyway, but throw away the scheme. |
| + return _parse(uri.substring(5), 0, null); |
| + } |
| } |
| - return _parse(uri, 5, null); |
| + throw new FormatException("Does not start with 'data:'", uri, 0); |
| } |
| /** |
| @@ -3050,7 +3213,7 @@ class UriData { |
| // 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); |
| + _uriCache = new _Uri._internal("data", "", null, null, path, query, null); |
| return _uriCache; |
| } |
| @@ -3075,7 +3238,7 @@ class UriData { |
| int start = _separatorIndices[0] + 1; |
| int end = _separatorIndices[1]; |
| if (start == end) return "text/plain"; |
| - return Uri._uriDecode(_text, start, end, UTF8, false); |
| + return _Uri._uriDecode(_text, start, end, UTF8, false); |
| } |
| /** |
| @@ -3096,8 +3259,8 @@ class UriData { |
| var keyStart = _separatorIndices[i] + 1; |
| var keyEnd = _separatorIndices[i + 1]; |
| if (keyEnd == keyStart + 7 && _text.startsWith("charset", keyStart)) { |
| - return Uri._uriDecode(_text, keyEnd + 1, _separatorIndices[i + 2], |
| - UTF8, false); |
| + return _Uri._uriDecode(_text, keyEnd + 1, _separatorIndices[i + 2], |
| + UTF8, false); |
| } |
| } |
| return "US-ASCII"; |
| @@ -3155,8 +3318,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)); |
| + 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; |
| result[index++] = byte; |
| @@ -3177,7 +3340,7 @@ class UriData { |
| * If the content is Base64 encoded, it will be decoded to bytes and then |
| * decoded to a string using [encoding]. |
| * If encoding is omitted, the value of a `charset` parameter is used |
| - * if it is recongized by [Encoding.getByName], otherwise it defaults to |
| + * if it is recognized by [Encoding.getByName], otherwise it defaults to |
| * the [ASCII] encoding, which is the default encoding for data URIs |
| * that do not specify an encoding. |
| * |
| @@ -3199,7 +3362,7 @@ class UriData { |
| var converter = BASE64.decoder.fuse(encoding.decoder); |
| return converter.convert(text.substring(start)); |
| } |
| - return Uri._uriDecode(text, start, text.length, encoding, false); |
| + return _Uri._uriDecode(text, start, text.length, encoding, false); |
| } |
| /** |
| @@ -3222,8 +3385,8 @@ class UriData { |
| var start = _separatorIndices[i - 2] + 1; |
| var equals = _separatorIndices[i - 1]; |
| var end = _separatorIndices[i]; |
| - String key = Uri._uriDecode(_text, start, equals, UTF8, false); |
| - String value = Uri._uriDecode(_text,equals + 1, end, UTF8, false); |
| + String key = _Uri._uriDecode(_text, start, equals, UTF8, false); |
| + String value = _Uri._uriDecode(_text,equals + 1, end, UTF8, false); |
| result[key] = value; |
| } |
| return result; |
| @@ -3306,9 +3469,9 @@ class UriData { |
| ((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) { |
| buffer.writeCharCode(byte); |
| } else { |
| - buffer.writeCharCode(Uri._PERCENT); |
| - buffer.writeCharCode(Uri._hexDigits.codeUnitAt(byte >> 4)); |
| - buffer.writeCharCode(Uri._hexDigits.codeUnitAt(byte & 0x0f)); |
| + buffer.writeCharCode(_PERCENT); |
| + buffer.writeCharCode(_hexDigits.codeUnitAt(byte >> 4)); |
| + buffer.writeCharCode(_hexDigits.codeUnitAt(byte & 0x0f)); |
| } |
| } |
| if ((byteOr & ~0xFF) != 0) { |
| @@ -3357,5 +3520,865 @@ class UriData { |
| // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" |
| // |
| // This is the same characters as in a URI query (which is URI pchar plus '?') |
| - static const _uricTable = Uri._queryCharTable; |
| + static const _uricTable = _Uri._queryCharTable; |
| } |
| + |
| +// -------------------------------------------------------------------- |
| +// Constants used to read the scanner result. |
| +// The indices points into the table returned by [_scanUri] which contains |
| +// recognized positions in the scanned URI. |
| +// The `0` index is only used internally. |
| + |
| +/// Index of the position of that `:` after a scheme. |
| +const int _schemeEndIndex = 1; |
| +/// Index of the position of the character just before the host name. |
| +const int _hostStartIndex = 2; |
| +/// Index of the position of the `:` before a port value. |
| +const int _portStartIndex = 3; |
| +/// Index of the position of the first character of a path. |
| +const int _pathStartIndex = 4; |
| +/// Index of the position of the `?` before a query. |
| +const int _queryStartIndex = 5; |
| +/// Index of the position of the `#` before a fragment. |
| +const int _fragmentStartIndex = 6; |
| +/// Index of a position where the URI was determined to be "non-simple". |
| +const int _notSimpleIndex = 7; |
| + |
| +// Initial state for scanner. |
| +const int _uriStart = 00; |
| + |
| +// If scanning of a URI terminates in this state or above, |
| +// consider the URI non-simple |
| +const int _nonSimpleEndStates = 14; |
| + |
| +// Initial state for scheme validation. |
| +const int _schemeStart = 20; |
| + |
| +/// Transition tables used to scan a URI to determine its structure. |
| +/// |
| +/// The tables represent a state machine with output. |
| +/// |
| +/// To scan the URI, start in the [_uriStart] state, then read each character |
| +/// of the URI in order, from start to end, and for each character perform a |
| +/// transition to a new state while writing the current position into the output |
| +/// buffer at a designated index. |
| +/// |
| +/// Each state, represented by an integer which is an index into |
| +/// [_scannerTables], has a set of transitions, one for each character. |
| +/// The transitions are encoded as a 5-bit integer representing the next state |
| +/// and a 3-bit index into the output table. |
| +/// |
| +/// For URI scanning, only characters in the range U+0020 through U+007E are |
| +/// interesting, all characters outside that range are treated the same. |
| +/// The tables only contain 96 entries, representing that characters in the |
| +/// interesting range, plus one more to represent all values outside the range. |
| +/// The character entries are stored in one `Uint8List` per state, with the |
| +/// transition for a character at position `character ^ 0x60`, |
| +/// which maps the range U+0020 .. U+007F into positions 0 .. 95. |
| +/// All remaining characters are mapped to position 31 (`0x7f ^ 0x60`) which |
| +/// represents the transition for all remaining characters. |
| +final List<Uint8List> _scannerTables = _createTables(); |
| + |
| +// ---------------------------------------------------------------------- |
| +// Code to create the URI scanner table. |
| + |
| +/// Creates the tables for [_scannerTables] used by [_scanUri]. |
| +/// |
| +/// See [_scannerTables] for the generated format. |
| +/// |
| +/// The concrete tables are chosen as a trade-off between the number of states |
| +/// needed and the precision of the result. |
| +/// This allows definitely recognizing the general structure of the URI |
| +/// (presence and location of scheme, user-info, host, port, path, query and |
| +/// fragment) while at the same time detecting that some components are not |
| +/// in canonical form (anything containing a `%`, a host-name containing a |
| +/// capital letter). Since the scanner doesn't know whether something is a |
| +/// scheme or a path until it sees `:`, or user-info or host until it sees |
| +/// a `@`, a second pass is needed to validate the scheme and any user-info |
| +/// is considered non-canonical by default. |
| +List<Uint8List> _createTables() { |
| + // TODO(lrn): Use a precomputed table. |
| + |
| + // Total number of states for the scanner. |
| + const int stateCount = 22; |
| + |
| + // States used to scan a URI from scratch. |
| + 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; |
| + |
| + // States used to validate a scheme after its end position has been found. |
| + const int scheme0 = _schemeStart; |
| + const int scheme = 21; |
| + |
| + // Constants encoding the write-index for the state transition into the top 5 |
| + // bits of a byte. |
| + 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; |
| + |
| + /// The `unreserved` characters of RFC 3986. |
| + const unreserved = |
| + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~" ; |
| + /// The `sub-delim` characters of RFC 3986. |
| + const subDelims = r"!$&'()*+,;="; |
| + // The `pchar` characters of RFC 3986: characters that may occur in a path, |
| + // excluding escapes. |
| + const pchar = "$unreserved$subDelims"; |
| + |
| + var tables = new List<Uint8List>.generate(stateCount, |
| + (_) => new Uint8List(96)); |
| + |
| + // Helper function which initialize the table for [state] with a default |
| + // transition and returns the table. |
| + Uint8List build(state, defaultTransition) => |
| + tables[state]..fillRange(0, 96, defaultTransition); |
| + |
| + // Helper function which sets the transition for each character in [chars] |
| + // to [transition] in the [target] table. |
| + // The [chars] string must contain only characters in the U+0020 .. U+007E |
| + // range. |
| + void setChars(Uint8List target, String chars, int transition) { |
| + for (int i = 0; i < chars.length; i++) { |
| + var char = chars.codeUnitAt(i); |
| + target[char ^ 0x60] = transition; |
| + } |
| + } |
| + |
| + /// Helper function which sets the transition for all characters in the |
| + /// range from `range[0]` to `range[1]` to [transition] in the [target] table. |
| + /// |
| + /// The [range] must be a two-character string where both characters are in |
| + /// the U+0020 .. U+007E range and the former character must have a lower |
| + /// code point than the latter. |
| + void setRange(Uint8List target, String range, int transition) { |
| + for (int i = range.codeUnitAt(0), n = range.codeUnitAt(1); i <= n; i++) { |
| + target[i ^ 0x60] = transition; |
| + } |
| + } |
| + |
| + // Create the transitions for each state. |
| + var b; |
| + |
| + // Validate as path, if it is a scheme, we handle it later. |
| + b = build(_uriStart, schemeOrPath | notSimple); |
| + setChars(b, pchar, schemeOrPath); |
| + setChars(b, ".", schemeOrPathDot); |
| + 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, ':', 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); |
| + 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, "/", relPathSeg); |
| + setChars(b, "?", query | queryStart); // This should be non-simple. |
| + setChars(b, "#", fragment | fragmentStart); // This should be non-simple. |
| + |
| + 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, "/", 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 lower-case scheme names. |
| + // Any non-scheme character or upper-case letter is marked as non-simple. |
| + 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. |
| + |
| +/// Scan a string using the [_scannerTables] state machine. |
| +/// |
| +/// Scans [uri] from [start] to [end], startig in state [state] and |
| +/// writing output into [indices]. |
| +/// |
| +/// Returns the final state. |
| +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 with 0x60 to move range 0x20-0x7f into 0x00-0x5f |
| + int char = uri.codeUnitAt(i) ^ 0x60; |
| + // Use 0x1f (nee 0x7f) to represent all unhandled characters. |
| + if (char > 0x5f) char = 0x1f; |
| + int transition = table[char]; |
| + state = transition & 0x1f; |
| + indices[transition >> 5] = i; |
| + } |
| + return state; |
| +} |
| + |
| +/// Scan a URI to determine its structure. |
| +/// |
| +/// Scans the [uri] from [start] to [end] and returns a list of indices |
| +/// representing significant positions in the URI. The positions are |
| +/// sufficient to split the URI into its structural parts. |
| +/// |
| +/// The returned list of positions have the following values at the given |
| +/// indices: |
| +/// |
| +/// - [_schemeEndIndex]: Is `start-1` if the URI has no scheme, |
| +/// otherwise it is the position of the `:` after the scheme. |
| +/// - [_hostStartIndex]: Either [start] if the URI has no authority component, |
| +/// otherwise either the position of the `@` after a user-info part or the |
| +/// position of the final `/` in the `//` leading the authority. |
| +/// - [_portStartIndex]: Either [start] if the URI has no authority component, or |
|
floitsch
2016/06/29 23:41:47
long line.
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Reworded and moved after inlining _scanUri.
|
| +/// the position of the `:` leading a port, or the same position as the |
| +/// start of the path if the URI has no port. |
| +/// - [_pathStartIndex]: The position of the initial character of the path. |
| +/// If the path is empty, then the position may be [start] or the position |
| +/// of the initial `/` in the `//` leading an authority. |
| +/// If there is a scheme or authority, and the path start is before the |
| +/// query end or host start, the actual path is empty. |
| +/// - [_queryStartIndex]: The position of the `?` leading a query if the URI |
| +/// contains a query, otherwise [end]. |
| +/// - [_fragmentStartIndex]: The position of the `#` leading a fragment if the |
| +/// URI contains a fragment, otherwise [end]. |
| +/// - [_notSimpleIndex]: Equal to `start - 1` unless the URI is considered |
| +/// "not simple". This is elaborated below. |
| +/// |
| +/// The returned positions are limited by the scanners ability to write only |
| +/// one position per character, and only the current position. |
| +/// Scanning from left to right, we only know whether something is a scheme |
| +/// or a path when we see a `:` or `/`, and likewise we only know if the first |
| +/// `/` is part of the path or is leading an authority component when we see |
| +/// the next character. |
| +/// |
| +/// # Simple URIs |
| +/// A URI is considered "simple" if it is in a normalized form containing no |
| +/// escapes. This allows us to skip normalization and checking whether escapes |
| +/// are valid, and to extract components without worrying about unescaping. |
| +/// |
| +/// The scanner computes a conservative approximation of being "simple". |
| +/// It rejects any URI with an escape, with a user-info component (mainly |
| +/// because they are rare and would increase the number of states in the |
| +/// scanner significantly), with an IPV6 host or with a capital letter in |
| +/// the scheme or host name. |
| +/// Further, paths containing `..` or `.` path segments are considered |
| +/// non-simple except for relative paths (no scheme or authority) starting |
| +/// with a sequence of "../" segments. |
| +/// |
| +/// There are some scheme-based normalizations for `file`, `http` and `https` |
| +/// URIs that have to be handled after scanning, to either consider the URI |
| +/// non-simple or normalize the string. |
| +List<int> _scanUri(String uri, int start, int end) { |
| + var indices = new List<int>.filled(8, start - 1); |
| + // For some positions, pick a better default which will be the correct value |
| + // in some cases where the value isn't updated by the scanner. |
| + indices[_portStartIndex] = start; |
| + indices[_pathStartIndex] = start; |
| + indices[_queryStartIndex] = end; |
| + indices[_fragmentStartIndex] = end; |
| + var state = _scan(uri, start, end, _uriStart, indices); |
| + // Some states that should be non-simple, but the URI ended early. |
| + // In particular paths that end at a ".." must be normalized to end in "../". |
| + 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, _schemeStart, indices); |
| + if (state == _schemeStart) { |
| + // Empty scheme. |
| + indices[_notSimpleIndex] = schemeEnd; |
| + } |
| + } |
| + return indices; |
| +} |
| + |
| +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 == _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); |
|
floitsch
2016/06/29 23:41:47
long line.
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Done.
|
| + } 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 { |
| + // This will require normalization, so use the _Uri implementation. |
| + 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); |
| + } |
| + |
| + // The RFC 3986 algorithm merges the base path without its final segment |
| + // (anything after the final "/", or everything if the base path doesn't |
| + // contain any "/"), and the reference path. |
| + // Then it removes "." and ".." segments using the remove-dot-segment |
| + // algorithm. |
| + // This code combines the two steps. It is simplified by knowing that |
| + // the base path contains no "." or ".." segments, and the reference |
| + // path can only contain leading ".." segments. |
| + |
| + 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; |
| + |
| + int slashCount = 0; |
| + |
| + // Count leading ".." segments in reference path. |
| + 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. |
| + // (We could use a slash from the base path in most cases, but not if |
| + // we remove the entire base path). |
| + String insert = ""; |
| + while (baseEnd > baseStart) { |
| + baseEnd--; |
| + int char = baseUri.codeUnitAt(baseEnd); |
| + if (char == _SLASH) { |
| + insert = "/"; |
| + backCount--; |
| + if (backCount == 0) break; |
| + } |
| + } |
| + |
| + var delta = baseEnd - refStart + insert.length; |
| + var newUri = "${base._uri.substring(0, baseEnd)}$insert" |
| + "${ref._uri.substring(refStart)}"; |
| + |
| + return new _SimpleUri(newUri, |
| + base._schemeEnd, |
| + base._hostStart, |
| + base._portStart, |
| + base._pathStart, |
| + ref._queryStart + delta, |
| + ref._fragmentStart + delta, |
| + base._schemeCache); |
| + } |
| + |
| + 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 (identical(this, other)) return true; |
| + if (other is Uri) return _uri == other.toString(); |
| + return false; |
| + } |
| + |
| + 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; |
| +} |
| + |
| +/// Checks whether [text] starts with "data:" at position [start]. |
| +/// |
| +/// The text must be long enough to allow reading five characters |
| +/// from the [start] position. |
| +/// |
| +/// Returns an integer value which is zero if text starts with all-lowercase |
| +/// "data:" and 0x20 if the text starts with "data:" that isn't all lower-case. |
| +/// All other values means the text starts with some other character. |
| +int _startsWithData(String text, int start) { |
|
floitsch
2016/06/29 23:41:47
This should return a bool. (return delta == 0 || d
Lasse Reichstein Nielsen
2016/06/30 10:27:31
Again, I actually need to distinguish the two case
floitsch
2016/06/30 18:15:22
I think that would be better. Even just changing t
|
| + // Multiply by 3 to avoid a non-colon character making delta be 0x20. |
| + int delta = (text.codeUnitAt(start + 4) ^ _COLON) * 3; |
| + delta |= text.codeUnitAt(start) ^ 0x64 /*d*/; |
| + delta |= text.codeUnitAt(start + 1) ^ 0x61 /*a*/; |
| + delta |= text.codeUnitAt(start + 2) ^ 0x74 /*t*/; |
| + delta |= text.codeUnitAt(start + 3) ^ 0x61 /*a*/; |
| + return delta; |
| +} |
| + |
| +/// Helper function returning the length of a string, or `0` for `null`. |
| +int _stringOrNullLength(String s) => (s == null) ? 0 : s.length; |