mirror of
https://github.com/dart-lang/sdk
synced 2024-09-16 03:27:43 +00:00
63180b95ed
Fixes #51095 TEST=ci CoreLibraryReviewExempt: There are no API changes, just removal of superfluous words in the comments. Change-Id: Ib1020c62fe6baed5ca68f0074323f025cc90e9f8 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/279500 Reviewed-by: Lasse Nielsen <lrn@google.com> Commit-Queue: Lasse Nielsen <lrn@google.com> Reviewed-by: Siva Annamalai <asiva@google.com>
5003 lines
179 KiB
Dart
5003 lines
179 KiB
Dart
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
part of dart.core;
|
|
|
|
// Frequently used character codes.
|
|
const int _SPACE = 0x20;
|
|
const int _PERCENT = 0x25;
|
|
const int _AMPERSAND = 0x26;
|
|
const int _PLUS = 0x2B;
|
|
const int _DOT = 0x2E;
|
|
const int _SLASH = 0x2F;
|
|
const int _COLON = 0x3A;
|
|
const int _EQUALS = 0x3d;
|
|
const int _UPPER_CASE_A = 0x41;
|
|
const int _UPPER_CASE_Z = 0x5A;
|
|
const int _LEFT_BRACKET = 0x5B;
|
|
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.
|
|
///
|
|
/// To create a URI with specific components, use [new Uri]:
|
|
/// ```dart
|
|
/// var httpsUri = Uri(
|
|
/// scheme: 'https',
|
|
/// host: 'dart.dev',
|
|
/// path: '/guides/libraries/library-tour',
|
|
/// fragment: 'numbers');
|
|
/// print(httpsUri); // https://dart.dev/guides/libraries/library-tour#numbers
|
|
///
|
|
/// httpsUri = Uri(
|
|
/// scheme: 'https',
|
|
/// host: 'example.com',
|
|
/// path: '/page/',
|
|
/// queryParameters: {'search': 'blue', 'limit': '10'});
|
|
/// print(httpsUri); // https://example.com/page/?search=blue&limit=10
|
|
///
|
|
/// final mailtoUri = Uri(
|
|
/// scheme: 'mailto',
|
|
/// path: 'John.Doe@example.com',
|
|
/// queryParameters: {'subject': 'Example'});
|
|
/// print(mailtoUri); // mailto:John.Doe@example.com?subject=Example
|
|
/// ```
|
|
///
|
|
/// ## HTTP and HTTPS URI
|
|
/// To create a URI with https scheme, use [Uri.https] or [Uri.http]:
|
|
/// ```dart
|
|
/// final httpsUri = Uri.https('example.com', 'api/fetch', {'limit': '10'});
|
|
/// print(httpsUri); // https://example.com/api/fetch?limit=10
|
|
/// ```
|
|
/// ## File URI
|
|
/// To create a URI from file path, use [Uri.file]:
|
|
/// ```dart
|
|
/// final fileUriUnix =
|
|
/// Uri.file(r'/home/myself/images/image.png', windows: false);
|
|
/// print(fileUriUnix); // file:///home/myself/images/image.png
|
|
///
|
|
/// final fileUriWindows =
|
|
/// Uri.file(r'C:\Users\myself\Documents\image.png', windows: true);
|
|
/// print(fileUriWindows); // file:///C:/Users/myself/Documents/image.png
|
|
/// ```
|
|
/// If the URI is not a file URI, calling this throws [UnsupportedError].
|
|
///
|
|
/// ## Directory URI
|
|
/// Like [Uri.file] except that a non-empty URI path ends in a slash.
|
|
/// ```dart
|
|
/// final fileDirectory =
|
|
/// Uri.directory('/home/myself/data/image', windows: false);
|
|
/// print(fileDirectory); // file:///home/myself/data/image/
|
|
///
|
|
/// final fileDirectoryWindows = Uri.directory('/data/images', windows: true);
|
|
/// print(fileDirectoryWindows); // file:///data/images/
|
|
/// ```
|
|
///
|
|
/// ## URI from string
|
|
/// To create a URI from string, use [Uri.parse] or [Uri.tryParse]:
|
|
/// ```dart
|
|
/// final uri = Uri.parse(
|
|
/// 'https://dart.dev/guides/libraries/library-tour#utility-classes');
|
|
/// print(uri); // https://dart.dev
|
|
/// print(uri.isScheme('https')); // true
|
|
/// print(uri.origin); // https://dart.dev
|
|
/// print(uri.host); // dart.dev
|
|
/// print(uri.authority); // dart.dev
|
|
/// print(uri.port); // 443
|
|
/// print(uri.path); // guides/libraries/library-tour
|
|
/// print(uri.pathSegments); // [guides, libraries, library-tour]
|
|
/// print(uri.fragment); // utility-classes
|
|
/// print(uri.hasQuery); // false
|
|
/// print(uri.data); // null
|
|
/// ```
|
|
///
|
|
/// **See also:**
|
|
/// * [URIs][uris] in the [library tour][libtour]
|
|
/// * [RFC-3986](https://tools.ietf.org/html/rfc3986)
|
|
/// * [RFC-2396](https://tools.ietf.org/html/rfc2396)
|
|
/// * [RFC-2045](https://tools.ietf.org/html/rfc2045)
|
|
///
|
|
/// [uris]: https://dart.dev/guides/libraries/library-tour#uris
|
|
/// [libtour]: https://dart.dev/guides/libraries/library-tour
|
|
abstract class Uri {
|
|
/// The natural base URI for the current platform.
|
|
///
|
|
/// When running in a browser, this is the current URL of the current page
|
|
/// (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;
|
|
|
|
/// Creates a new URI from its components.
|
|
///
|
|
/// Each component is set through a named argument. Any number of
|
|
/// components can be provided. The [path] and [query] components can be set
|
|
/// using either of two different named arguments.
|
|
///
|
|
/// The scheme component is set through [scheme]. The scheme is
|
|
/// normalized to all lowercase letters. If the scheme is omitted or empty,
|
|
/// the URI will not have a scheme part.
|
|
///
|
|
/// The user info part of the authority component is set through
|
|
/// [userInfo]. It defaults to the empty string, which will be omitted
|
|
/// from the string representation of the URI.
|
|
///
|
|
/// The host part of the authority component is set through
|
|
/// [host]. The host can either be a hostname, an IPv4 address or an
|
|
/// IPv6 address, contained in `'['` and `']'`. If the host contains a
|
|
/// ':' character, the `'['` and `']'` are added if not already provided.
|
|
/// The host is normalized to all lowercase letters.
|
|
///
|
|
/// The port part of the authority component is set through
|
|
/// [port].
|
|
/// If [port] is omitted or `null`, it implies the default port for
|
|
/// the URI's scheme, and is equivalent to passing that port explicitly.
|
|
/// The recognized schemes, and their default ports, are "http" (80) and
|
|
/// "https" (443). All other schemes are considered as having zero as the
|
|
/// default port.
|
|
///
|
|
/// If any of `userInfo`, `host` or `port` are provided,
|
|
/// the URI has an authority according to [hasAuthority].
|
|
///
|
|
/// The path component is set through either [path] or
|
|
/// [pathSegments].
|
|
/// When [path] is used, it should be a valid URI path,
|
|
/// but invalid characters, except the general delimiters ':/@[]?#',
|
|
/// will be escaped if necessary. A backslash, `\`, will be converted
|
|
/// to a slash `/`.
|
|
/// When [pathSegments] is used, each of the provided segments
|
|
/// is first percent-encoded and then joined using the forward slash
|
|
/// separator.
|
|
///
|
|
/// The percent-encoding of the path segments encodes all
|
|
/// characters except for the unreserved characters and the following
|
|
/// list of characters: `!$&'()*+,;=:@`. If the other components
|
|
/// necessitate an absolute path, a leading slash `/` is prepended if
|
|
/// not already there.
|
|
///
|
|
/// The query component is set through either [query] or [queryParameters].
|
|
/// When [query] is used, the provided string should be a valid URI query,
|
|
/// but invalid characters, other than general delimiters,
|
|
/// will be escaped if necessary.
|
|
/// When [queryParameters] is used, the query is built from the
|
|
/// provided map. Each key and value in the map is percent-encoded
|
|
/// and joined using equal and ampersand characters.
|
|
/// A value in the map must be either `null`, a string, or an [Iterable] of
|
|
/// strings. An iterable corresponds to multiple values for the same key,
|
|
/// and an empty iterable or `null` corresponds to no value for the key.
|
|
///
|
|
/// The percent-encoding of the keys and values encodes all characters
|
|
/// except for the unreserved characters, and replaces spaces with `+`.
|
|
/// If [query] is the empty string, it is equivalent to omitting it.
|
|
/// To have an actual empty query part,
|
|
/// use an empty map for [queryParameters].
|
|
///
|
|
/// If both [query] and [queryParameters] are omitted or `null`,
|
|
/// the URI has no query part.
|
|
///
|
|
/// The fragment component is set through [fragment].
|
|
/// It should be a valid URI fragment, but invalid characters other than
|
|
/// general delimiters are escaped if necessary.
|
|
/// If [fragment] is omitted or `null`, the URI has no fragment part.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final httpsUri = Uri(
|
|
/// scheme: 'https',
|
|
/// host: 'dart.dev',
|
|
/// path: 'guides/libraries/library-tour',
|
|
/// fragment: 'numbers');
|
|
/// print(httpsUri); // https://dart.dev/guides/libraries/library-tour#numbers
|
|
///
|
|
/// final mailtoUri = Uri(
|
|
/// scheme: 'mailto',
|
|
/// path: 'John.Doe@example.com',
|
|
/// queryParameters: {'subject': 'Example'});
|
|
/// print(mailtoUri); // mailto:John.Doe@example.com?subject=Example
|
|
/// ```
|
|
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}) = _Uri;
|
|
|
|
/// Creates a new `http` URI from authority, path and query.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// var uri = Uri.http('example.org', '/path', { 'q' : 'dart' });
|
|
/// print(uri); // http://example.org/path?q=dart
|
|
///
|
|
/// uri = Uri.http('user:password@localhost:8080', '');
|
|
/// print(uri); // http://user:password@localhost:8080
|
|
///
|
|
/// uri = Uri.http('example.org', 'a b');
|
|
/// print(uri); // http://example.org/a%20b
|
|
///
|
|
/// uri = Uri.http('example.org', '/a%2F');
|
|
/// print(uri); // http://example.org/a%252F
|
|
/// ```
|
|
///
|
|
/// The `scheme` is always set to `http`.
|
|
///
|
|
/// The `userInfo`, `host` and `port` components are set from the
|
|
/// [authority] argument. If `authority` is `null` or empty,
|
|
/// the created `Uri` has no authority, and isn't directly usable
|
|
/// as an HTTP URL, which must have a non-empty host.
|
|
///
|
|
/// The `path` component is set from the [unencodedPath]
|
|
/// argument. The path passed must not be encoded as this constructor
|
|
/// encodes the path. Only `/` is recognized as path separator.
|
|
/// If omitted, the path defaults to being empty.
|
|
///
|
|
/// The `query` component is set from the optional [queryParameters]
|
|
/// argument.
|
|
factory Uri.http(
|
|
String authority, [
|
|
String unencodedPath,
|
|
Map<String, dynamic /*String?|Iterable<String>*/ >? queryParameters,
|
|
]) = _Uri.http;
|
|
|
|
/// Creates a new `https` URI from authority, path and query.
|
|
///
|
|
/// This constructor is the same as [Uri.http] except for the scheme
|
|
/// which is set to `https`.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// var uri = Uri.https('example.org', '/path', {'q': 'dart'});
|
|
/// print(uri); // https://example.org/path?q=dart
|
|
///
|
|
/// uri = Uri.https('user:password@localhost:8080', '');
|
|
/// print(uri); // https://user:password@localhost:8080
|
|
///
|
|
/// uri = Uri.https('example.org', 'a b');
|
|
/// print(uri); // https://example.org/a%20b
|
|
///
|
|
/// uri = Uri.https('example.org', '/a%2F');
|
|
/// print(uri); // https://example.org/a%252F
|
|
/// ```
|
|
factory Uri.https(String authority,
|
|
[String unencodedPath,
|
|
Map<String, dynamic>? 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 in the input [path].
|
|
///
|
|
/// With Windows semantics, backslash (`\`) and forward-slash (`/`)
|
|
/// are used to separate path segments in the input [path],
|
|
/// except if the path starts with `\\?\` in which case
|
|
/// only backslash (`\`) separates path segments in [path].
|
|
///
|
|
/// If the path starts with a path separator, an absolute URI (with the
|
|
/// `file` scheme and an empty authority) is created.
|
|
/// Otherwise a relative URI reference with no scheme or authority is created.
|
|
/// One exception to 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
|
|
/// is 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:
|
|
/// ```dart
|
|
/// // xxx/yyy
|
|
/// Uri.file('xxx/yyy', windows: false);
|
|
///
|
|
/// // xxx/yyy/
|
|
/// Uri.file('xxx/yyy/', windows: false);
|
|
///
|
|
/// // file:///xxx/yyy
|
|
/// Uri.file('/xxx/yyy', windows: false);
|
|
///
|
|
/// // file:///xxx/yyy/
|
|
/// Uri.file('/xxx/yyy/', windows: false);
|
|
///
|
|
/// // C%3A
|
|
/// Uri.file('C:', windows: false);
|
|
/// ```
|
|
///
|
|
/// Examples using Windows semantics:
|
|
/// ```dart
|
|
/// // xxx/yyy
|
|
/// Uri.file(r'xxx\yyy', windows: true);
|
|
///
|
|
/// // xxx/yyy/
|
|
/// Uri.file(r'xxx\yyy\', windows: true);
|
|
///
|
|
/// file:///xxx/yyy
|
|
/// Uri.file(r'\xxx\yyy', windows: true);
|
|
///
|
|
/// file:///xxx/yyy/
|
|
/// Uri.file(r'\xxx\yyy/', windows: true);
|
|
///
|
|
/// // file:///C:/xxx/yyy
|
|
/// Uri.file(r'C:\xxx\yyy', windows: true);
|
|
///
|
|
/// // This throws an error. A path with a drive letter, but no following
|
|
/// // path, is not allowed.
|
|
/// Uri.file(r'C:', windows: true);
|
|
///
|
|
/// // This throws an error. A path with a drive letter is not absolute.
|
|
/// Uri.file(r'C:xxx\yyy', windows: true);
|
|
///
|
|
/// // file://server/share/file
|
|
/// Uri.file(r'\\server\share\file', windows: true);
|
|
/// ```
|
|
///
|
|
/// If the path passed is not a valid file path, an error 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`.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final fileDirectory = Uri.directory('data/images', windows: false);
|
|
/// print(fileDirectory); // data/images/
|
|
///
|
|
/// final fileDirectoryWindows =
|
|
/// Uri.directory(r'C:\data\images', windows: true);
|
|
/// print(fileDirectoryWindows); // file:///C:/data/images/
|
|
/// ```
|
|
factory Uri.directory(String path, {bool? windows}) = _Uri.directory;
|
|
|
|
/// Creates a `data:` URI containing the [content] string.
|
|
///
|
|
/// Converts the content to 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 in [parameters], 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].
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final uri = Uri.dataFromString(
|
|
/// 'example content',
|
|
/// mimeType: 'text/plain',
|
|
/// parameters: <String, String>{'search': 'file', 'max': '10'},
|
|
/// );
|
|
/// print(uri); // data:;search=name;max=10,example%20content
|
|
/// ```
|
|
factory Uri.dataFromString(String content,
|
|
{String? mimeType,
|
|
Encoding? encoding,
|
|
Map<String, String>? parameters,
|
|
bool base64 = false}) {
|
|
UriData data = 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.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final uri = Uri.dataFromBytes([68, 97, 114, 116]);
|
|
/// print(uri); // data:application/octet-stream;base64,RGFydA==
|
|
/// ```
|
|
factory Uri.dataFromBytes(List<int> bytes,
|
|
{String mimeType = "application/octet-stream",
|
|
Map<String, String>? parameters,
|
|
bool percentEncoded = false}) {
|
|
UriData data = UriData.fromBytes(bytes,
|
|
mimeType: mimeType,
|
|
parameters: parameters,
|
|
percentEncoded: percentEncoded);
|
|
return data.uri;
|
|
}
|
|
|
|
/// The scheme component of the URI.
|
|
///
|
|
/// The value is 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;
|
|
|
|
/// The authority component.
|
|
///
|
|
/// The authority is formatted from the [userInfo], [host] and [port]
|
|
/// parts.
|
|
///
|
|
/// The value is the empty string if there is no authority component.
|
|
String get authority;
|
|
|
|
/// The user info part of the authority component.
|
|
///
|
|
/// The value is the empty string if there is no user info in the
|
|
/// authority component.
|
|
String get userInfo;
|
|
|
|
/// The host part of the authority component.
|
|
///
|
|
/// The value is the empty string if there is no authority component and
|
|
/// hence no host.
|
|
///
|
|
/// If the host is an IP version 6 address, the surrounding `[` and `]` is
|
|
/// removed.
|
|
///
|
|
/// The host string is case-insensitive.
|
|
/// The returned host name is canonicalized to lower-case
|
|
/// with upper-case percent-escapes.
|
|
String get host;
|
|
|
|
/// The port part of the authority component.
|
|
///
|
|
/// The value is the default 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;
|
|
|
|
/// The path component.
|
|
///
|
|
/// The path is the actual substring of the URI representing the path,
|
|
/// and it is encoded where necessary. To get direct access to the decoded
|
|
/// path, use [pathSegments].
|
|
///
|
|
/// The path value is the empty string if there is no path component.
|
|
String get path;
|
|
|
|
/// The query component.
|
|
///
|
|
/// The value is the actual substring of the URI representing the query part,
|
|
/// and it is encoded where necessary.
|
|
/// To get direct access to the decoded query, use [queryParameters].
|
|
///
|
|
/// The value is the empty string if there is no query component.
|
|
String get query;
|
|
|
|
/// The fragment identifier component.
|
|
///
|
|
/// The value is the empty string if there is no fragment identifier
|
|
/// component.
|
|
String get fragment;
|
|
|
|
/// The URI path split into its segments.
|
|
///
|
|
/// Each of the segments in the list has been decoded.
|
|
/// If the path is empty, the empty list will
|
|
/// be returned. A leading slash `/` does not affect the segments returned.
|
|
///
|
|
/// The list is unmodifiable and will throw [UnsupportedError] on any
|
|
/// calls that would mutate it.
|
|
List<String> get pathSegments;
|
|
|
|
/// 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](https://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 resulting 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.
|
|
///
|
|
/// Example:
|
|
/// ```dart import:convert
|
|
/// final uri =
|
|
/// Uri.parse('https://example.com/api/fetch?limit=10,20,30&max=100');
|
|
/// print(jsonEncode(uri.queryParameters));
|
|
/// // {"limit":"10,20,30","max":"100"}
|
|
/// ```
|
|
///
|
|
/// The 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](https://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 resulting map has been decoded. If there is no
|
|
/// query, the map is empty.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// Example:
|
|
/// ```dart import:convert
|
|
/// final uri =
|
|
/// Uri.parse('https://example.com/api/fetch?limit=10,20,30&max=100');
|
|
/// print(jsonEncode(uri.queryParameters)); // {"limit":"10,20,30","max":"100"}
|
|
/// ```
|
|
///
|
|
/// The map and the lists it contains are unmodifiable.
|
|
Map<String, List<String>> get queryParametersAll;
|
|
|
|
/// 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;
|
|
|
|
/// Whether the URI has a [scheme] component.
|
|
bool get hasScheme => scheme.isNotEmpty;
|
|
|
|
/// Whether the URI has an [authority] component.
|
|
bool get hasAuthority;
|
|
|
|
/// 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;
|
|
|
|
/// Whether the URI has a query part.
|
|
bool get hasQuery;
|
|
|
|
/// Whether the URI has a fragment part.
|
|
bool get hasFragment;
|
|
|
|
/// Whether the URI has an empty path.
|
|
bool get hasEmptyPath;
|
|
|
|
/// 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", or if the host name
|
|
/// is missing or empty.
|
|
///
|
|
/// See: https://www.w3.org/TR/2011/WD-html5-20110405/origin-0.html#origin
|
|
String get origin;
|
|
|
|
/// Whether the scheme of this [Uri] is [scheme].
|
|
///
|
|
/// The [scheme] should be the same as the one returned by [Uri.scheme],
|
|
/// but doesn't have to be case-normalized to lower-case characters.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// var uri = Uri.parse('http://example.com');
|
|
/// print(uri.isScheme('HTTP')); // true
|
|
///
|
|
/// final uriNoScheme = Uri(host: 'example.com');
|
|
/// print(uriNoScheme.isScheme('HTTP')); // false
|
|
/// ```
|
|
///
|
|
/// An empty [scheme] string matches a URI with no scheme
|
|
/// (one where [hasScheme] returns false).
|
|
bool isScheme(String scheme);
|
|
|
|
/// Creates a 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
|
|
/// is 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):
|
|
/// ```dart
|
|
/// 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):
|
|
/// ```dart
|
|
/// 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 string representation
|
|
/// have the same hash code.
|
|
int get hashCode;
|
|
|
|
/// A URI is equal to another URI with the same normalized representation.
|
|
bool operator ==(Object other);
|
|
|
|
/// The normalized string representation of the URI.
|
|
String toString();
|
|
|
|
/// Creates a new `Uri` based on this one, but with some parts replaced.
|
|
///
|
|
/// This method takes the same parameters as the [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
|
|
/// hierarchical manner,
|
|
/// and can instead replace each part of a `Uri` individually.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final uri1 = Uri.parse(
|
|
/// 'http://dart.dev/guides/libraries/library-tour#utility-classes');
|
|
///
|
|
/// final uri2 = uri1.replace(
|
|
/// scheme: 'https',
|
|
/// path: 'guides/libraries/library-tour',
|
|
/// fragment: 'uris');
|
|
/// print(uri2); // https://dart.dev/guides/libraries/library-tour#uris
|
|
/// ```
|
|
/// This method acts similarly to using the `Uri` constructor with
|
|
/// some of the arguments taken from this `Uri`. Example:
|
|
/// ``` dart continued
|
|
/// final Uri uri3 = Uri(
|
|
/// scheme: 'https',
|
|
/// userInfo: uri1.userInfo,
|
|
/// host: uri1.host,
|
|
/// port: uri2.port,
|
|
/// path: '/guides/language/language-tour',
|
|
/// query: uri1.query,
|
|
/// fragment: null);
|
|
/// print(uri3); // https://dart.dev/guides/language/language-tour
|
|
/// ```
|
|
/// Using this method can be seen as 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});
|
|
|
|
/// Creates a `Uri` that differs from this only in not having a fragment.
|
|
///
|
|
/// If this `Uri` does not have a fragment, it is itself returned.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final uri =
|
|
/// Uri.parse('https://example.org:8080/foo/bar#frag').removeFragment();
|
|
/// print(uri); // https://example.org:8080/foo/bar
|
|
/// ```
|
|
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 a URI relative to `this`.
|
|
///
|
|
/// Returns the resolved URI.
|
|
///
|
|
/// The algorithm "Transform Reference" for resolving a reference is described
|
|
/// in [RFC-3986 Section 5](https://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 and no 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 `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.
|
|
///
|
|
/// If [start] and [end] are provided, they must specify a valid substring
|
|
/// of [uri], and only the substring from `start` to `end` is parsed as a URI.
|
|
///
|
|
/// If the [uri] string is not valid as a URI or URI reference,
|
|
/// a [FormatException] is thrown.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final uri =
|
|
/// Uri.parse('https://example.com/api/fetch?limit=10,20,30&max=100');
|
|
/// print(uri); // https://example.com/api/fetch?limit=10,20,30&max=100
|
|
///
|
|
/// Uri.parse('::Not valid URI::'); // Throws FormatException.
|
|
/// ```
|
|
static Uri parse(String uri, [int start = 0, int? end]) {
|
|
// This parsing will not validate percent-encoding, IPv6, etc.
|
|
// When done splitting into parts, it will call, e.g., [_makeFragment]
|
|
// to do the final parsing.
|
|
//
|
|
// Important parts of the RFC 3986 used here:
|
|
// URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
|
|
//
|
|
// hier-part = "//" authority path-abempty
|
|
// / path-absolute
|
|
// / path-rootless
|
|
// / path-empty
|
|
//
|
|
// URI-reference = URI / relative-ref
|
|
//
|
|
// absolute-URI = scheme ":" hier-part [ "?" query ]
|
|
//
|
|
// relative-ref = relative-part [ "?" query ] [ "#" fragment ]
|
|
//
|
|
// relative-part = "//" authority path-abempty
|
|
// / path-absolute
|
|
// / path-noscheme
|
|
// / path-empty
|
|
//
|
|
// scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
|
//
|
|
// authority = [ userinfo "@" ] host [ ":" port ]
|
|
// userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
|
|
// host = IP-literal / IPv4address / reg-name
|
|
// IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture ) "]"
|
|
// IPv6addrz = IPv6address "%25" ZoneID
|
|
// ZoneID = 1*( unreserved / pct-encoded )
|
|
// port = *DIGIT
|
|
// reg-name = *( unreserved / pct-encoded / sub-delims )
|
|
//
|
|
// path = path-abempty ; begins with "/" or is empty
|
|
// / path-absolute ; begins with "/" but not "//"
|
|
// / path-noscheme ; begins with a non-colon segment
|
|
// / path-rootless ; begins with a segment
|
|
// / path-empty ; zero characters
|
|
//
|
|
// path-abempty = *( "/" segment )
|
|
// path-absolute = "/" [ segment-nz *( "/" segment ) ]
|
|
// path-noscheme = segment-nz-nc *( "/" segment )
|
|
// path-rootless = segment-nz *( "/" segment )
|
|
// path-empty = 0<pchar>
|
|
//
|
|
// segment = *pchar
|
|
// segment-nz = 1*pchar
|
|
// segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
|
|
// ; non-zero-length segment without any colon ":"
|
|
//
|
|
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
|
//
|
|
// query = *( pchar / "/" / "?" )
|
|
//
|
|
// fragment = *( pchar / "/" / "?" )
|
|
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;
|
|
}
|
|
// Otherwise the URI doesn't start with "data:" or any case variant of it.
|
|
}
|
|
|
|
// The following index-normalization belongs with the scanning, but is
|
|
// easier to do here because we already have extracted variables from the
|
|
// indices list.
|
|
var indices = List<int>.filled(8, 0, growable: false);
|
|
|
|
// Set default values for each position.
|
|
// The value will either be correct in some cases where it isn't set
|
|
// by the scanner, or it is clearly recognizable as an unset value.
|
|
indices
|
|
..[0] = 0
|
|
..[_schemeEndIndex] = start - 1
|
|
..[_hostStartIndex] = start - 1
|
|
..[_notSimpleIndex] = start - 1
|
|
..[_portStartIndex] = start
|
|
..[_pathStartIndex] = start
|
|
..[_queryStartIndex] = end
|
|
..[_fragmentStartIndex] = end;
|
|
var state = _scan(uri, start, end, _uriStart, indices);
|
|
// Some states that should be non-simple, but the URI ended early.
|
|
// 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;
|
|
}
|
|
}
|
|
// 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.
|
|
|
|
int hostStart = indices[_hostStartIndex] + 1;
|
|
int portStart = indices[_portStartIndex];
|
|
int pathStart = indices[_pathStartIndex];
|
|
int queryStart = indices[_queryStartIndex];
|
|
int fragmentStart = indices[_fragmentStartIndex];
|
|
|
|
// We may discover the scheme while handling special cases.
|
|
String? scheme;
|
|
|
|
// Derive some positions that weren't set to normalize the indices.
|
|
if (fragmentStart < queryStart) queryStart = fragmentStart;
|
|
// If pathStart isn't set (it's before scheme end or host start), then
|
|
// the path is empty, or there is no authority part and the path
|
|
// starts with a non-simple character.
|
|
if (pathStart < hostStart) {
|
|
// There is an authority, but no path. The path would start with `/`
|
|
// if it was there.
|
|
pathStart = queryStart;
|
|
} else if (pathStart <= schemeEnd) {
|
|
// There is a scheme, but no authority.
|
|
pathStart = schemeEnd + 1;
|
|
}
|
|
// If there is an authority with no port, set the port position
|
|
// to be at the end of the authority (equal to pathStart).
|
|
// This also handles a ":" in a user-info component incorrectly setting
|
|
// the port start position.
|
|
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 (uri.startsWith(r"\", pathStart) ||
|
|
hostStart > start &&
|
|
(uri.startsWith(r"\", hostStart - 1) ||
|
|
uri.startsWith(r"\", hostStart - 2))) {
|
|
// Seeing a `\` anywhere.
|
|
// The scanner doesn't record when the first path character is a `\`
|
|
// or when the last slash before the authority is a `\`.
|
|
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.
|
|
if (start == 0 && end == uri.length) {
|
|
uri = uri.replaceRange(pathStart, queryStart, "/");
|
|
queryStart += 1;
|
|
fragmentStart += 1;
|
|
end += 1;
|
|
} else {
|
|
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)) {
|
|
if (start == 0 && end == uri.length) {
|
|
uri = uri.replaceRange(portStart, pathStart, "");
|
|
pathStart -= 3;
|
|
queryStart -= 3;
|
|
fragmentStart -= 3;
|
|
end -= 3;
|
|
} else {
|
|
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)) {
|
|
if (start == 0 && end == uri.length) {
|
|
uri = uri.replaceRange(portStart, pathStart, "");
|
|
pathStart -= 4;
|
|
queryStart -= 4;
|
|
fragmentStart -= 4;
|
|
end -= 3;
|
|
} else {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isSimple) {
|
|
if (start > 0 || end < uri.length) {
|
|
uri = uri.substring(start, end);
|
|
schemeEnd -= start;
|
|
hostStart -= start;
|
|
portStart -= start;
|
|
pathStart -= start;
|
|
queryStart -= start;
|
|
fragmentStart -= start;
|
|
}
|
|
return _SimpleUri(uri, schemeEnd, hostStart, portStart, pathStart,
|
|
queryStart, fragmentStart, scheme);
|
|
}
|
|
|
|
return _Uri.notSimple(uri, start, end, schemeEnd, hostStart, portStart,
|
|
pathStart, queryStart, fragmentStart, scheme);
|
|
}
|
|
|
|
/// Creates a new `Uri` object by parsing a URI string.
|
|
///
|
|
/// If [start] and [end] are provided, they must specify a valid substring
|
|
/// of [uri], and only the substring from `start` to `end` is parsed as a URI.
|
|
///
|
|
/// Returns `null` if the [uri] string is not valid as a URI or URI reference.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final uri = Uri.tryParse(
|
|
/// 'https://dart.dev/guides/libraries/library-tour#utility-classes', 0,
|
|
/// 16);
|
|
/// print(uri); // https://dart.dev
|
|
///
|
|
/// var notUri = Uri.tryParse('::Not valid URI::');
|
|
/// print(notUri); // null
|
|
/// ```
|
|
static Uri? tryParse(String uri, [int start = 0, int? end]) {
|
|
// TODO: Optimize to avoid throwing-and-recatching.
|
|
try {
|
|
return parse(uri, start, end);
|
|
} on FormatException {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// 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 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].
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// const request = 'http://example.com/search=Dart';
|
|
/// final encoded = Uri.encodeComponent(request);
|
|
/// print(encoded); // http%3A%2F%2Fexample.com%2Fsearch%3DDart
|
|
/// ```
|
|
static String encodeComponent(String component) {
|
|
return _Uri._uriEncode(_Uri._unreserved2396Table, component, utf8, false);
|
|
}
|
|
|
|
/**
|
|
* Encodes 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 https://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 which are
|
|
/// delimiters for a given URI component 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.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final decoded =
|
|
/// Uri.decodeComponent('http%3A%2F%2Fexample.com%2Fsearch%3DDart');
|
|
/// print(decoded); // http://example.com/search=Dart
|
|
/// ```
|
|
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);
|
|
}
|
|
|
|
/// Encodes 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 ECMA-262 version 5.1 for
|
|
/// the encodeURI function.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final encoded =
|
|
/// Uri.encodeFull('https://example.com/api/query?search= dart is');
|
|
/// print(encoded); // https://example.com/api/query?search=%20dart%20is
|
|
/// ```
|
|
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.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// final decoded =
|
|
/// Uri.decodeFull('https://example.com/api/query?search=%20dart%20is');
|
|
/// print(decoded); // https://example.com/api/query?search= dart is
|
|
/// ```
|
|
static String decodeFull(String uri) {
|
|
return _Uri._uriDecode(uri, 0, uri.length, utf8, false);
|
|
}
|
|
|
|
/// Splits the [query] into a map according to the rules
|
|
/// specified for FORM post in the [HTML 4.01 specification section
|
|
/// 17.13.4](https://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.
|
|
///
|
|
/// Example:
|
|
/// ```dart import:convert
|
|
/// final queryStringMap =
|
|
/// Uri.splitQueryString('limit=10&max=100&search=Dart%20is%20fun');
|
|
/// print(jsonEncode(queryStringMap));
|
|
/// // {"limit":"10","max":"100","search":"Dart is fun"}
|
|
///
|
|
/// ```
|
|
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[decodeQueryComponent(key, encoding: encoding)] =
|
|
decodeQueryComponent(value, encoding: encoding);
|
|
}
|
|
return map;
|
|
});
|
|
}
|
|
|
|
/// Parses 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) =>
|
|
_parseIPv4Address(host, 0, host.length);
|
|
|
|
/// Implementation of [parseIPv4Address] that can work on a substring.
|
|
static List<int> _parseIPv4Address(String host, int start, int end) {
|
|
void error(String msg, int position) {
|
|
throw FormatException('Illegal IPv4 address, $msg', host, position);
|
|
}
|
|
|
|
var result = Uint8List(4);
|
|
int partIndex = 0;
|
|
int partStart = start;
|
|
for (int i = start; i < end; i++) {
|
|
int char = host.codeUnitAt(i);
|
|
if (char != _DOT) {
|
|
if (char ^ 0x30 > 9) {
|
|
// Fail on a non-digit character.
|
|
error("invalid character", i);
|
|
}
|
|
} else {
|
|
if (partIndex == 3) {
|
|
error('IPv4 address should contain exactly 4 parts', i);
|
|
}
|
|
int part = int.parse(host.substring(partStart, i));
|
|
if (part > 255) {
|
|
error("each part must be in the range 0..255", partStart);
|
|
}
|
|
result[partIndex++] = part;
|
|
partStart = i + 1;
|
|
}
|
|
}
|
|
|
|
if (partIndex != 3) {
|
|
error('IPv4 address should contain exactly 4 parts', end);
|
|
}
|
|
|
|
int part = int.parse(host.substring(partStart, end));
|
|
if (part > 255) {
|
|
error("each part must be in the range 0..255", partStart);
|
|
}
|
|
result[partIndex] = part;
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Parses the [host] as an IP version 6 (IPv6) address.
|
|
///
|
|
/// Returns 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 to 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]) {
|
|
end ??= host.length;
|
|
// An IPv6 address consists of exactly 8 parts of 1-4 hex digits, separated
|
|
// 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 "dotted-quad" address.
|
|
|
|
// Helper function for reporting a badly formatted IPv6 address.
|
|
void error(String msg, int? position) {
|
|
throw FormatException('Illegal IPv6 address, $msg', host, position);
|
|
}
|
|
|
|
// Parse a hex block.
|
|
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 > 0xFFFF) {
|
|
error('each part must be in the range of `0x0..0xFFFF`', start);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
if (host.length < 2) error('address is too short', null);
|
|
List<int> parts = [];
|
|
bool wildcardSeen = false;
|
|
// Set if seeing a ".", suggesting that there is an IPv4 address.
|
|
bool seenDot = false;
|
|
int partStart = start;
|
|
// Parse all parts, except a potential last one.
|
|
for (int i = start; i < end; i++) {
|
|
int char = host.codeUnitAt(i);
|
|
if (char == _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;
|
|
} else if (char == _DOT) {
|
|
seenDot = true;
|
|
}
|
|
}
|
|
if (parts.length == 0) error('too few parts', null);
|
|
bool atEnd = (partStart == end);
|
|
bool isLastWildcard = (parts.last == -1);
|
|
if (atEnd && !isLastWildcard) {
|
|
error('expected a part after last `:`', end);
|
|
}
|
|
if (!atEnd) {
|
|
if (!seenDot) {
|
|
parts.add(parseHex(partStart, end));
|
|
} else {
|
|
List<int> last = _parseIPv4Address(host, partStart, end);
|
|
parts.add(last[0] << 8 | last[1]);
|
|
parts.add(last[2] << 8 | last[3]);
|
|
}
|
|
}
|
|
if (wildcardSeen) {
|
|
if (parts.length > 7) {
|
|
error('an address with a wildcard must have less than 7 parts', null);
|
|
}
|
|
} else if (parts.length != 8) {
|
|
error('an address without a wildcard must contain exactly 8 parts', null);
|
|
}
|
|
List<int> bytes = 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;
|
|
}
|
|
}
|
|
|
|
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.
|
|
final 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 of the full normalized text representation of the URI.
|
|
late final String _text = this._initializeText();
|
|
|
|
/// Cache of the list of path segments.
|
|
late final List<String> pathSegments = _computePathSegments(this.path);
|
|
|
|
/// Lazily computed and cached hashCode of [_text].
|
|
late final int hashCode = this._text.hashCode;
|
|
|
|
/// Cache the computed return value of [queryParameters].
|
|
late final Map<String, String> queryParameters =
|
|
UnmodifiableMapView<String, String>(Uri.splitQueryString(this.query));
|
|
|
|
/// Cache the computed return value of [queryParametersAll].
|
|
late final Map<String, List<String>> queryParametersAll =
|
|
_computeQueryParametersAll(this.query);
|
|
|
|
/// Internal non-verifying constructor. Only call with validated arguments.
|
|
///
|
|
/// The components must be properly normalized.
|
|
///
|
|
/// Use `null` for [_host] if there is no authority. In that case, always
|
|
/// pass `null` for [_port] and an empty string for [_userInfo] as well.
|
|
///
|
|
/// Use `null` for [_port], [_query] and [_fragment] if there is
|
|
/// component of that type, and empty string for [_userInfo].
|
|
///
|
|
/// The [path] and [scheme] are never empty.
|
|
_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);
|
|
}
|
|
host = _makeHost(uri, hostStart, portStart, false);
|
|
if (portStart + 1 < pathStart) {
|
|
int portNumber =
|
|
int.tryParse(uri.substring(portStart + 1, pathStart)) ??
|
|
(throw FormatException("Invalid port", uri, portStart + 1));
|
|
port = _makePort(portNumber, 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 _Uri._internal(scheme, userInfo, host, port, path, query, fragment);
|
|
}
|
|
|
|
/// 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}) {
|
|
if (scheme == null) {
|
|
scheme = "";
|
|
} else {
|
|
scheme = _makeScheme(scheme, 0, scheme.length);
|
|
}
|
|
userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo));
|
|
if (userInfo == null) {
|
|
// TODO(dart-lang/language#440): Remove when promotion works.
|
|
throw "unreachable";
|
|
}
|
|
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 (path == null) {
|
|
// TODO(dart-lang/language#440): Remove when promotion works.
|
|
throw "unreachable";
|
|
}
|
|
if (scheme.isEmpty && host == null && !path.startsWith('/')) {
|
|
bool allowScheme = scheme.isNotEmpty || host != null;
|
|
path = _normalizeRelativePath(path, allowScheme);
|
|
} else {
|
|
path = _removeDotSegments(path);
|
|
}
|
|
if (host == null && path.startsWith("//")) {
|
|
host = "";
|
|
}
|
|
return _Uri._internal(scheme, userInfo, host, port, path, query, fragment);
|
|
}
|
|
|
|
/// Implementation of [Uri.http].
|
|
factory _Uri.http(String authority,
|
|
[String unencodedPath = '', Map<String, dynamic>? queryParameters]) {
|
|
return _makeHttpUri("http", authority, unencodedPath, queryParameters);
|
|
}
|
|
|
|
/// Implementation of [Uri.https].
|
|
factory _Uri.https(String authority,
|
|
[String unencodedPath = '', Map<String, dynamic>? queryParameters]) {
|
|
return _makeHttpUri("https", authority, unencodedPath, queryParameters);
|
|
}
|
|
|
|
String get authority {
|
|
if (!hasAuthority) return "";
|
|
var sb = StringBuffer();
|
|
_writeAuthority(sb);
|
|
return sb.toString();
|
|
}
|
|
|
|
String get userInfo => _userInfo;
|
|
|
|
String get host {
|
|
String? host = _host;
|
|
if (host == null) return "";
|
|
if (host.startsWith('[')) {
|
|
return host.substring(1, host.length - 1);
|
|
}
|
|
return host;
|
|
}
|
|
|
|
int get port {
|
|
return _port ?? _defaultPort(scheme);
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
String get query => _query ?? "";
|
|
|
|
String get fragment => _fragment ?? "";
|
|
|
|
bool isScheme(String scheme) {
|
|
String thisScheme = this.scheme;
|
|
if (scheme == null) return thisScheme.isEmpty;
|
|
if (scheme.length != thisScheme.length) return false;
|
|
return _caseInsensitiveStartsWith(scheme, thisScheme, 0);
|
|
}
|
|
|
|
/// Report a parse failure.
|
|
static Never _fail(String uri, int index, String message) {
|
|
throw FormatException(message, uri, index);
|
|
}
|
|
|
|
static _Uri _makeHttpUri(String scheme, String? authority,
|
|
String unencodedPath, Map<String, dynamic>? queryParameters) {
|
|
var userInfo = "";
|
|
String? host;
|
|
int? port;
|
|
|
|
if (authority != null && authority.isNotEmpty) {
|
|
var hostStart = 0;
|
|
// Split off the user info.
|
|
for (int i = 0; i < authority.length; i++) {
|
|
const int atSign = 0x40;
|
|
if (authority.codeUnitAt(i) == atSign) {
|
|
userInfo = authority.substring(0, i);
|
|
hostStart = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
var hostEnd = hostStart;
|
|
if (hostStart < authority.length &&
|
|
authority.codeUnitAt(hostStart) == _LEFT_BRACKET) {
|
|
// IPv6 host.
|
|
int escapeForZoneID = -1;
|
|
for (; hostEnd < authority.length; hostEnd++) {
|
|
int char = authority.codeUnitAt(hostEnd);
|
|
if (char == _PERCENT && escapeForZoneID < 0) {
|
|
escapeForZoneID = hostEnd;
|
|
if (authority.startsWith("25", hostEnd + 1)) {
|
|
hostEnd += 2; // Might as well skip the already checked escape.
|
|
}
|
|
} else if (char == _RIGHT_BRACKET) {
|
|
break;
|
|
}
|
|
}
|
|
if (hostEnd == authority.length) {
|
|
throw FormatException(
|
|
"Invalid IPv6 host entry.", authority, hostStart);
|
|
}
|
|
Uri.parseIPv6Address(authority, hostStart + 1,
|
|
(escapeForZoneID < 0) ? hostEnd : escapeForZoneID);
|
|
hostEnd++; // Skip the closing bracket.
|
|
if (hostEnd != authority.length &&
|
|
authority.codeUnitAt(hostEnd) != _COLON) {
|
|
throw FormatException("Invalid end of authority", authority, hostEnd);
|
|
}
|
|
}
|
|
// Split host and port.
|
|
for (; hostEnd < authority.length; hostEnd++) {
|
|
if (authority.codeUnitAt(hostEnd) == _COLON) {
|
|
var portString = authority.substring(hostEnd + 1);
|
|
// We allow the empty port - falling back to initial value.
|
|
if (portString.isNotEmpty) port = int.parse(portString);
|
|
break;
|
|
}
|
|
}
|
|
host = authority.substring(hostStart, hostEnd);
|
|
}
|
|
return _Uri(
|
|
scheme: scheme,
|
|
userInfo: userInfo,
|
|
host: host,
|
|
port: port,
|
|
pathSegments: unencodedPath.split("/"),
|
|
queryParameters: queryParameters);
|
|
}
|
|
|
|
/// Implementation of [Uri.file].
|
|
factory _Uri.file(String path, {bool? windows}) {
|
|
return (windows ?? _Uri._isWindows)
|
|
? _makeWindowsFileUrl(path, false)
|
|
: _makeFileUri(path, false);
|
|
}
|
|
|
|
/// Implementation of [Uri.directory].
|
|
factory _Uri.directory(String path, {bool? windows}) {
|
|
return (windows ?? _Uri._isWindows)
|
|
? _makeWindowsFileUrl(path, true)
|
|
: _makeFileUri(path, true);
|
|
}
|
|
|
|
/// Used internally in path-related constructors.
|
|
external static bool get _isWindows;
|
|
|
|
static void _checkNonWindowsPathReservedCharacters(
|
|
List<String> segments, bool argumentError) {
|
|
for (var segment in segments) {
|
|
if (segment.contains("/")) {
|
|
if (argumentError) {
|
|
throw ArgumentError("Illegal path character $segment");
|
|
} else {
|
|
throw UnsupportedError("Illegal path character $segment");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void _checkWindowsPathReservedCharacters(
|
|
List<String> segments, bool argumentError,
|
|
[int firstSegment = 0]) {
|
|
for (var segment in segments.skip(firstSegment)) {
|
|
if (segment.contains(RegExp(r'["*/:<>?\\|]'))) {
|
|
if (argumentError) {
|
|
throw ArgumentError("Illegal character in path");
|
|
} else {
|
|
throw UnsupportedError("Illegal character in path: $segment");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void _checkWindowsDriveLetter(int charCode, bool argumentError) {
|
|
if ((_UPPER_CASE_A <= charCode && charCode <= _UPPER_CASE_Z) ||
|
|
(_LOWER_CASE_A <= charCode && charCode <= _LOWER_CASE_Z)) {
|
|
return;
|
|
}
|
|
if (argumentError) {
|
|
throw ArgumentError(
|
|
"Illegal drive letter " + String.fromCharCode(charCode));
|
|
} else {
|
|
throw UnsupportedError(
|
|
"Illegal drive letter " + String.fromCharCode(charCode));
|
|
}
|
|
}
|
|
|
|
static Uri _makeFileUri(String path, bool slashTerminated) {
|
|
const String sep = "/";
|
|
var segments = path.split(sep);
|
|
if (slashTerminated && segments.isNotEmpty && segments.last.isNotEmpty) {
|
|
segments.add(""); // Extra separator at end.
|
|
}
|
|
if (path.startsWith(sep)) {
|
|
// Absolute file:// URI.
|
|
return Uri(scheme: "file", pathSegments: segments);
|
|
} else {
|
|
// Relative URI.
|
|
return Uri(pathSegments: segments);
|
|
}
|
|
}
|
|
|
|
static _makeWindowsFileUrl(String path, bool slashTerminated) {
|
|
if (path.startsWith(r"\\?\")) {
|
|
if (path.startsWith(r"UNC\", 4)) {
|
|
path = path.replaceRange(0, 7, r'\');
|
|
} else {
|
|
path = path.substring(4);
|
|
if (path.length < 3 ||
|
|
path.codeUnitAt(1) != _COLON ||
|
|
path.codeUnitAt(2) != _BACKSLASH) {
|
|
throw ArgumentError(
|
|
r"Windows paths with \\?\ prefix must be absolute");
|
|
}
|
|
}
|
|
} else {
|
|
path = path.replaceAll("/", r'\');
|
|
}
|
|
const String sep = r'\';
|
|
if (path.length > 1 && path.codeUnitAt(1) == _COLON) {
|
|
_checkWindowsDriveLetter(path.codeUnitAt(0), true);
|
|
if (path.length == 2 || path.codeUnitAt(2) != _BACKSLASH) {
|
|
throw ArgumentError("Windows paths with drive letter must be absolute");
|
|
}
|
|
// Absolute file://C:/ URI.
|
|
var pathSegments = path.split(sep);
|
|
if (slashTerminated && pathSegments.last.isNotEmpty) {
|
|
pathSegments.add(""); // Extra separator at end.
|
|
}
|
|
_checkWindowsPathReservedCharacters(pathSegments, true, 1);
|
|
return Uri(scheme: "file", pathSegments: pathSegments);
|
|
}
|
|
|
|
if (path.startsWith(sep)) {
|
|
if (path.startsWith(sep, 1)) {
|
|
// Absolute file:// URI with host.
|
|
int pathStart = path.indexOf(r'\', 2);
|
|
String hostPart =
|
|
(pathStart < 0) ? path.substring(2) : path.substring(2, pathStart);
|
|
String pathPart = (pathStart < 0) ? "" : path.substring(pathStart + 1);
|
|
var pathSegments = pathPart.split(sep);
|
|
_checkWindowsPathReservedCharacters(pathSegments, true);
|
|
if (slashTerminated && pathSegments.last.isNotEmpty) {
|
|
pathSegments.add(""); // Extra separator at end.
|
|
}
|
|
return Uri(scheme: "file", host: hostPart, pathSegments: pathSegments);
|
|
} else {
|
|
// Absolute file:// URI.
|
|
var pathSegments = path.split(sep);
|
|
if (slashTerminated && pathSegments.last.isNotEmpty) {
|
|
pathSegments.add(""); // Extra separator at end.
|
|
}
|
|
_checkWindowsPathReservedCharacters(pathSegments, true);
|
|
return Uri(scheme: "file", pathSegments: pathSegments);
|
|
}
|
|
} else {
|
|
// Relative URI.
|
|
var pathSegments = path.split(sep);
|
|
_checkWindowsPathReservedCharacters(pathSegments, true);
|
|
if (slashTerminated &&
|
|
pathSegments.isNotEmpty &&
|
|
pathSegments.last.isNotEmpty) {
|
|
pathSegments.add(""); // Extra separator at end.
|
|
}
|
|
return Uri(pathSegments: pathSegments);
|
|
}
|
|
}
|
|
|
|
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}) {
|
|
// Set to true if the scheme has (potentially) changed.
|
|
// In that case, the default port may also have changed and we need
|
|
// to check even the existing port.
|
|
bool schemeChanged = false;
|
|
if (scheme != null) {
|
|
scheme = _makeScheme(scheme, 0, scheme.length);
|
|
schemeChanged = (scheme != this.scheme);
|
|
} else {
|
|
scheme = this.scheme;
|
|
}
|
|
|
|
bool isFile = (scheme == "file");
|
|
if (userInfo != null) {
|
|
userInfo = _makeUserInfo(userInfo, 0, userInfo.length);
|
|
} else {
|
|
userInfo = this._userInfo;
|
|
}
|
|
|
|
if (port != null) {
|
|
port = _makePort(port, scheme);
|
|
} else {
|
|
port = this._port;
|
|
if (schemeChanged) {
|
|
// The default port might have changed.
|
|
port = _makePort(port, scheme);
|
|
}
|
|
}
|
|
if (host != null) {
|
|
host = _makeHost(host, 0, host.length, false);
|
|
} else if (this.hasAuthority) {
|
|
host = this._host;
|
|
} else if (userInfo.isNotEmpty || port != null || isFile) {
|
|
host = "";
|
|
}
|
|
|
|
bool hasAuthority = host != null;
|
|
if (path != null || pathSegments != null) {
|
|
path = _makePath(path, 0, _stringOrNullLength(path), pathSegments, scheme,
|
|
hasAuthority);
|
|
} else {
|
|
var currentPath = this.path;
|
|
if ((isFile || (hasAuthority && !currentPath.isEmpty)) &&
|
|
!currentPath.startsWith('/')) {
|
|
currentPath = "/" + currentPath;
|
|
}
|
|
path = currentPath;
|
|
}
|
|
|
|
if (query != null || queryParameters != null) {
|
|
query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters);
|
|
} else {
|
|
query = this._query;
|
|
}
|
|
|
|
if (fragment != null) {
|
|
fragment = _makeFragment(fragment, 0, fragment.length);
|
|
} else {
|
|
fragment = this._fragment;
|
|
}
|
|
|
|
return _Uri._internal(scheme, userInfo, host, port, path, query, fragment);
|
|
}
|
|
|
|
Uri removeFragment() {
|
|
if (!this.hasFragment) return this;
|
|
return _Uri._internal(scheme, _userInfo, _host, _port, path, _query, null);
|
|
}
|
|
|
|
static List<String> _computePathSegments(String pathToSplit) {
|
|
if (pathToSplit.isNotEmpty && pathToSplit.codeUnitAt(0) == _SLASH) {
|
|
pathToSplit = pathToSplit.substring(1);
|
|
}
|
|
return (pathToSplit.isEmpty)
|
|
? const <String>[]
|
|
: List<String>.unmodifiable(
|
|
pathToSplit.split("/").map(Uri.decodeComponent));
|
|
}
|
|
|
|
static Map<String, List<String>> _computeQueryParametersAll(String? query) {
|
|
if (query == null || query.isEmpty) return const <String, List<String>>{};
|
|
Map<String, List<String>> queryParameterLists = _splitQueryStringAll(query);
|
|
queryParameterLists.updateAll(_toUnmodifiableStringList);
|
|
return Map<String, List<String>>.unmodifiable(queryParameterLists);
|
|
}
|
|
|
|
Uri normalizePath() {
|
|
String path = _normalizePath(this.path, scheme, hasAuthority);
|
|
if (identical(path, this.path)) return this;
|
|
return this.replace(path: path);
|
|
}
|
|
|
|
static int? _makePort(int? port, String scheme) {
|
|
// Perform scheme specific normalization.
|
|
if (port != null && port == _defaultPort(scheme)) return null;
|
|
return port;
|
|
}
|
|
|
|
/// Check and normalize a host name.
|
|
///
|
|
/// If the host name starts and ends with '[' and ']', it is considered an
|
|
/// IPv6 address. If [strictIPv6] is false, the address is also considered
|
|
/// an IPv6 address if it contains any ':' character.
|
|
///
|
|
/// If it is not an IPv6 address, it is case- and escape-normalized.
|
|
/// This escapes all characters not valid in a reg-name,
|
|
/// and converts all non-escape upper-case letters to lower-case.
|
|
static String? _makeHost(String? host, int start, int end, bool strictIPv6) {
|
|
// TODO(lrn): Should we normalize IPv6 addresses according to RFC 5952?
|
|
if (host == null) return null;
|
|
if (start == end) return "";
|
|
// Host is an IPv6 address if it starts with '[' or contains a colon.
|
|
if (host.codeUnitAt(start) == _LEFT_BRACKET) {
|
|
if (host.codeUnitAt(end - 1) != _RIGHT_BRACKET) {
|
|
_fail(host, start, 'Missing end `]` to match `[` in host');
|
|
}
|
|
String zoneID = "";
|
|
int index = _checkZoneID(host, start + 1, end - 1);
|
|
if (index < end - 1) {
|
|
int zoneIDstart =
|
|
(host.startsWith("25", index + 1)) ? index + 3 : index + 1;
|
|
zoneID = _normalizeZoneID(host, zoneIDstart, end - 1, "%25");
|
|
}
|
|
Uri.parseIPv6Address(host, start + 1, index);
|
|
// RFC 5952 requires hex digits to be lower case.
|
|
return host.substring(start, index).toLowerCase() + zoneID + ']';
|
|
}
|
|
if (!strictIPv6) {
|
|
// TODO(lrn): skip if too short to be a valid IPv6 address?
|
|
for (int i = start; i < end; i++) {
|
|
if (host.codeUnitAt(i) == _COLON) {
|
|
String zoneID = "";
|
|
int index = _checkZoneID(host, start, end);
|
|
if (index < end) {
|
|
int zoneIDstart =
|
|
(host.startsWith("25", index + 1)) ? index + 3 : index + 1;
|
|
zoneID = _normalizeZoneID(host, zoneIDstart, end, "%25");
|
|
}
|
|
Uri.parseIPv6Address(host, start, index);
|
|
return '[${host.substring(start, index)}' + zoneID + ']';
|
|
}
|
|
}
|
|
}
|
|
return _normalizeRegName(host, start, end);
|
|
}
|
|
|
|
/// RFC 6874 check for ZoneID
|
|
/// Return the index of first appeared `%`.
|
|
static int _checkZoneID(String host, int start, int end) {
|
|
int index = host.indexOf('%', start);
|
|
index = (index >= start && index < end) ? index : end;
|
|
return index;
|
|
}
|
|
|
|
static bool _isZoneIDChar(int char) {
|
|
return char < 127 && (_zoneIDTable[char >> 4] & (1 << (char & 0xf))) != 0;
|
|
}
|
|
|
|
/// Validates and does case- and percent-encoding normalization.
|
|
///
|
|
/// The same as [_normalizeOrSubstring]
|
|
/// except this function does not convert characters to lower case.
|
|
/// The [host] must be an RFC6874 "ZoneID".
|
|
/// ZoneID = 1*(unreserved / pct-encoded)
|
|
static String _normalizeZoneID(String host, int start, int end,
|
|
[String prefix = '']) {
|
|
StringBuffer? buffer;
|
|
if (prefix != '') {
|
|
buffer = StringBuffer(prefix);
|
|
}
|
|
int sectionStart = start;
|
|
int index = start;
|
|
// Whether all characters between sectionStart and index are normalized,
|
|
bool isNormalized = true;
|
|
|
|
while (index < end) {
|
|
int char = host.codeUnitAt(index);
|
|
if (char == _PERCENT) {
|
|
String? replacement = _normalizeEscape(host, index, true);
|
|
if (replacement == null && isNormalized) {
|
|
index += 3;
|
|
continue;
|
|
}
|
|
buffer ??= StringBuffer();
|
|
String slice = host.substring(sectionStart, index);
|
|
buffer.write(slice);
|
|
int sourceLength = 3;
|
|
if (replacement == null) {
|
|
replacement = host.substring(index, index + 3);
|
|
} else if (replacement == "%") {
|
|
_fail(host, index, "ZoneID should not contain % anymore");
|
|
}
|
|
buffer.write(replacement);
|
|
index += sourceLength;
|
|
sectionStart = index;
|
|
isNormalized = true;
|
|
} else if (_isZoneIDChar(char)) {
|
|
if (isNormalized && _UPPER_CASE_A <= char && _UPPER_CASE_Z >= char) {
|
|
// Put initial slice in buffer and continue in non-normalized mode
|
|
buffer ??= StringBuffer();
|
|
if (sectionStart < index) {
|
|
buffer.write(host.substring(sectionStart, index));
|
|
sectionStart = index;
|
|
}
|
|
isNormalized = false;
|
|
}
|
|
index++;
|
|
} else {
|
|
int sourceLength = 1;
|
|
if ((char & 0xFC00) == 0xD800 && (index + 1) < end) {
|
|
int tail = host.codeUnitAt(index + 1);
|
|
if ((tail & 0xFC00) == 0xDC00) {
|
|
char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff);
|
|
sourceLength = 2;
|
|
}
|
|
}
|
|
String slice = host.substring(sectionStart, index);
|
|
(buffer ??= StringBuffer())
|
|
..write(slice)
|
|
..write(_escapeChar(char));
|
|
index += sourceLength;
|
|
sectionStart = index;
|
|
}
|
|
}
|
|
if (buffer == null) return host.substring(start, end);
|
|
if (sectionStart < end) {
|
|
String slice = host.substring(sectionStart, end);
|
|
buffer.write(slice);
|
|
}
|
|
return buffer.toString();
|
|
}
|
|
|
|
static bool _isRegNameChar(int char) {
|
|
return char < 127 && (_regNameTable[char >> 4] & (1 << (char & 0xf))) != 0;
|
|
}
|
|
|
|
/// Validates and does case- and percent-encoding normalization.
|
|
///
|
|
/// The [host] must be an RFC3986 "reg-name". It is converted
|
|
/// to lower case, and percent escapes are converted to either
|
|
/// lower case unreserved characters or upper case escapes.
|
|
static String _normalizeRegName(String host, int start, int end) {
|
|
StringBuffer? buffer;
|
|
int sectionStart = start;
|
|
int index = start;
|
|
// Whether all characters between sectionStart and index are normalized,
|
|
bool isNormalized = true;
|
|
|
|
while (index < end) {
|
|
int char = host.codeUnitAt(index);
|
|
if (char == _PERCENT) {
|
|
// The _regNameTable contains "%", so we check that first.
|
|
String? replacement = _normalizeEscape(host, index, true);
|
|
if (replacement == null && isNormalized) {
|
|
index += 3;
|
|
continue;
|
|
}
|
|
buffer ??= StringBuffer();
|
|
String slice = host.substring(sectionStart, index);
|
|
if (!isNormalized) slice = slice.toLowerCase();
|
|
buffer.write(slice);
|
|
int sourceLength = 3;
|
|
if (replacement == null) {
|
|
replacement = host.substring(index, index + 3);
|
|
} else if (replacement == "%") {
|
|
replacement = "%25";
|
|
sourceLength = 1;
|
|
}
|
|
buffer.write(replacement);
|
|
index += sourceLength;
|
|
sectionStart = index;
|
|
isNormalized = true;
|
|
} else if (_isRegNameChar(char)) {
|
|
if (isNormalized && _UPPER_CASE_A <= char && _UPPER_CASE_Z >= char) {
|
|
// Put initial slice in buffer and continue in non-normalized mode
|
|
buffer ??= StringBuffer();
|
|
if (sectionStart < index) {
|
|
buffer.write(host.substring(sectionStart, index));
|
|
sectionStart = index;
|
|
}
|
|
isNormalized = false;
|
|
}
|
|
index++;
|
|
} else if (_isGeneralDelimiter(char)) {
|
|
_fail(host, index, "Invalid character");
|
|
} else {
|
|
int sourceLength = 1;
|
|
if ((char & 0xFC00) == 0xD800 && (index + 1) < end) {
|
|
int tail = host.codeUnitAt(index + 1);
|
|
if ((tail & 0xFC00) == 0xDC00) {
|
|
char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff);
|
|
sourceLength = 2;
|
|
}
|
|
}
|
|
String slice = host.substring(sectionStart, index);
|
|
if (!isNormalized) slice = slice.toLowerCase();
|
|
(buffer ??= StringBuffer())
|
|
..write(slice)
|
|
..write(_escapeChar(char));
|
|
index += sourceLength;
|
|
sectionStart = index;
|
|
}
|
|
}
|
|
if (buffer == null) return host.substring(start, end);
|
|
if (sectionStart < end) {
|
|
String slice = host.substring(sectionStart, end);
|
|
if (!isNormalized) slice = slice.toLowerCase();
|
|
buffer.write(slice);
|
|
}
|
|
return buffer.toString();
|
|
}
|
|
|
|
/// Validates scheme characters and does case-normalization.
|
|
///
|
|
/// Schemes are converted to lower case. They cannot contain escapes.
|
|
static String _makeScheme(String scheme, int start, int end) {
|
|
if (start == end) return "";
|
|
final int firstCodeUnit = scheme.codeUnitAt(start);
|
|
if (!_isAlphabeticCharacter(firstCodeUnit)) {
|
|
_fail(scheme, start, "Scheme not starting with alphabetic character");
|
|
}
|
|
bool containsUpperCase = false;
|
|
for (int i = start; i < end; i++) {
|
|
final int codeUnit = scheme.codeUnitAt(i);
|
|
if (!_isSchemeCharacter(codeUnit)) {
|
|
_fail(scheme, i, "Illegal scheme character");
|
|
}
|
|
if (_UPPER_CASE_A <= codeUnit && codeUnit <= _UPPER_CASE_Z) {
|
|
containsUpperCase = true;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
static String _makeUserInfo(String? userInfo, int start, int end) {
|
|
if (userInfo == null) return "";
|
|
return _normalizeOrSubstring(userInfo, start, end, _userinfoTable);
|
|
}
|
|
|
|
static String _makePath(String? path, int start, int end,
|
|
Iterable<String>? pathSegments, String scheme, bool hasAuthority) {
|
|
bool isFile = (scheme == "file");
|
|
bool ensureLeadingSlash = isFile || hasAuthority;
|
|
String result;
|
|
if (path == null) {
|
|
if (pathSegments == null) return isFile ? "/" : "";
|
|
result = pathSegments
|
|
.map((s) => _uriEncode(_pathCharTable, s, utf8, false))
|
|
.join("/");
|
|
} else if (pathSegments != null) {
|
|
throw ArgumentError('Both path and pathSegments specified');
|
|
} else {
|
|
result = _normalizeOrSubstring(path, start, end, _pathCharOrSlashTable,
|
|
escapeDelimiters: true, replaceBackslash: true);
|
|
}
|
|
if (result.isEmpty) {
|
|
if (isFile) return "/";
|
|
} else if (ensureLeadingSlash && !result.startsWith('/')) {
|
|
result = "/" + result;
|
|
}
|
|
result = _normalizePath(result, scheme, hasAuthority);
|
|
return result;
|
|
}
|
|
|
|
/// Performs path normalization (remove dot segments) on a path.
|
|
///
|
|
/// If the URI has neither scheme nor authority, it's considered a
|
|
/// "pure path" and normalization won't remove leading ".." segments.
|
|
/// Otherwise it follows the RFC 3986 "remove dot segments" algorithm.
|
|
static String _normalizePath(String path, String scheme, bool hasAuthority) {
|
|
if (scheme.isEmpty &&
|
|
!hasAuthority &&
|
|
!path.startsWith('/') &&
|
|
!path.startsWith(r'\')) {
|
|
return _normalizeRelativePath(path, scheme.isNotEmpty || hasAuthority);
|
|
}
|
|
return _removeDotSegments(path);
|
|
}
|
|
|
|
static String? _makeQuery(String? query, int start, int end,
|
|
Map<String, dynamic /*String?|Iterable<String>*/ >? queryParameters) {
|
|
if (query != null) {
|
|
if (queryParameters != null) {
|
|
throw ArgumentError('Both query and queryParameters specified');
|
|
}
|
|
return _normalizeOrSubstring(query, start, end, _queryCharTable,
|
|
escapeDelimiters: true);
|
|
}
|
|
if (queryParameters == null) return null;
|
|
|
|
var result = StringBuffer();
|
|
var separator = "";
|
|
|
|
void writeParameter(String key, String? value) {
|
|
result.write(separator);
|
|
separator = "&";
|
|
result.write(Uri.encodeQueryComponent(key));
|
|
if (value != null && value.isNotEmpty) {
|
|
result.write("=");
|
|
result.write(Uri.encodeQueryComponent(value));
|
|
}
|
|
}
|
|
|
|
queryParameters.forEach((key, value) {
|
|
if (value == null || value is String) {
|
|
writeParameter(key, value);
|
|
} else {
|
|
Iterable values = value;
|
|
for (String value in values) {
|
|
writeParameter(key, value);
|
|
}
|
|
}
|
|
});
|
|
return result.toString();
|
|
}
|
|
|
|
static String? _makeFragment(String? fragment, int start, int end) {
|
|
if (fragment == null) return null;
|
|
return _normalizeOrSubstring(fragment, start, end, _queryCharTable,
|
|
escapeDelimiters: true);
|
|
}
|
|
|
|
/// Performs RFC 3986 Percent-Encoding Normalization.
|
|
///
|
|
/// Returns a replacement string that should replace the original escape.
|
|
/// Returns null if no replacement is necessary because the escape is
|
|
/// not for an unreserved character and is already non-lower-case.
|
|
///
|
|
/// Returns "%" if the escape is invalid (not two valid hex digits following
|
|
/// the percent sign). The calling code should replace the percent
|
|
/// sign with "%25", but leave the following two characters unmodified.
|
|
///
|
|
/// If [lowerCase] is true, a single character returned is always lower case,
|
|
static String? _normalizeEscape(String source, int index, bool lowerCase) {
|
|
assert(source.codeUnitAt(index) == _PERCENT);
|
|
if (index + 2 >= source.length) {
|
|
return "%"; // Marks the escape as invalid.
|
|
}
|
|
int firstDigit = source.codeUnitAt(index + 1);
|
|
int secondDigit = source.codeUnitAt(index + 2);
|
|
int firstDigitValue = hexDigitValue(firstDigit);
|
|
int secondDigitValue = hexDigitValue(secondDigit);
|
|
if (firstDigitValue < 0 || secondDigitValue < 0) {
|
|
return "%"; // Marks the escape as invalid.
|
|
}
|
|
int value = firstDigitValue * 16 + secondDigitValue;
|
|
if (_isUnreservedChar(value)) {
|
|
if (lowerCase && _UPPER_CASE_A <= value && _UPPER_CASE_Z >= value) {
|
|
value |= 0x20;
|
|
}
|
|
return String.fromCharCode(value);
|
|
}
|
|
if (firstDigit >= _LOWER_CASE_A || secondDigit >= _LOWER_CASE_A) {
|
|
// Either digit is lower case.
|
|
return source.substring(index, index + 3).toUpperCase();
|
|
}
|
|
// Escape is retained, and is already non-lower case, so return null to
|
|
// represent "no replacement necessary".
|
|
return null;
|
|
}
|
|
|
|
static String _escapeChar(int char) {
|
|
assert(char <= 0x10ffff); // It's a valid unicode code point.
|
|
List<int> codeUnits;
|
|
if (char < 0x80) {
|
|
// ASCII, a single percent encoded sequence.
|
|
codeUnits = Uint8List(3);
|
|
codeUnits[0] = _PERCENT;
|
|
codeUnits[1] = _hexDigits.codeUnitAt(char >> 4);
|
|
codeUnits[2] = _hexDigits.codeUnitAt(char & 0xf);
|
|
} else {
|
|
// Do UTF-8 encoding of character, then percent encode bytes.
|
|
int flag = 0xc0; // The high-bit markers on the first byte of UTF-8.
|
|
int encodedBytes = 2;
|
|
if (char > 0x7ff) {
|
|
flag = 0xe0;
|
|
encodedBytes = 3;
|
|
if (char > 0xffff) {
|
|
encodedBytes = 4;
|
|
flag = 0xf0;
|
|
}
|
|
}
|
|
codeUnits = Uint8List(3 * encodedBytes);
|
|
int index = 0;
|
|
while (--encodedBytes >= 0) {
|
|
int byte = ((char >> (6 * encodedBytes)) & 0x3f) | flag;
|
|
codeUnits[index] = _PERCENT;
|
|
codeUnits[index + 1] = _hexDigits.codeUnitAt(byte >> 4);
|
|
codeUnits[index + 2] = _hexDigits.codeUnitAt(byte & 0xf);
|
|
index += 3;
|
|
flag = 0x80; // Following bytes have only high bit set.
|
|
}
|
|
}
|
|
return String.fromCharCodes(codeUnits);
|
|
}
|
|
|
|
/// Normalizes using [_normalize] or returns substring of original.
|
|
///
|
|
/// If [_normalize] returns `null` (original content is already normalized),
|
|
/// this methods returns the substring if [component] from [start] to [end].
|
|
static String _normalizeOrSubstring(
|
|
String component, int start, int end, List<int> charTable,
|
|
{bool escapeDelimiters = false, bool replaceBackslash = false}) {
|
|
return _normalize(component, start, end, charTable,
|
|
escapeDelimiters: escapeDelimiters,
|
|
replaceBackslash: replaceBackslash) ??
|
|
component.substring(start, end);
|
|
}
|
|
|
|
/// Runs through component checking that each character is valid and
|
|
/// normalizes percent escapes.
|
|
///
|
|
/// Uses [charTable] to check if a non-`%` character is allowed.
|
|
/// Each `%` character must be followed by two hex digits.
|
|
/// If the hex-digits are lowercase letters, they are converted to
|
|
/// uppercase.
|
|
///
|
|
/// Returns `null` if the original content was already normalized.
|
|
static String? _normalize(
|
|
String component, int start, int end, List<int> charTable,
|
|
{bool escapeDelimiters = false, bool replaceBackslash = false}) {
|
|
StringBuffer? buffer;
|
|
int sectionStart = start;
|
|
int index = start;
|
|
// Loop while characters are valid and escapes correct and upper-case.
|
|
while (index < end) {
|
|
int char = component.codeUnitAt(index);
|
|
if (char < 127 && (charTable[char >> 4] & (1 << (char & 0x0f))) != 0) {
|
|
index++;
|
|
} else {
|
|
String? replacement;
|
|
int sourceLength;
|
|
if (char == _PERCENT) {
|
|
replacement = _normalizeEscape(component, index, false);
|
|
// Returns null if we should keep the existing escape.
|
|
if (replacement == null) {
|
|
index += 3;
|
|
continue;
|
|
}
|
|
// Returns "%" if we should escape the existing percent.
|
|
if ("%" == replacement) {
|
|
replacement = "%25";
|
|
sourceLength = 1;
|
|
} else {
|
|
sourceLength = 3;
|
|
}
|
|
} else if (char == _BACKSLASH && replaceBackslash) {
|
|
replacement = "/";
|
|
sourceLength = 1;
|
|
} else if (!escapeDelimiters && _isGeneralDelimiter(char)) {
|
|
_fail(component, index, "Invalid character");
|
|
throw "unreachable"; // TODO(lrn): Remove when Never-returning functions are recognized as throwing.
|
|
} else {
|
|
sourceLength = 1;
|
|
if ((char & 0xFC00) == 0xD800) {
|
|
// Possible lead surrogate.
|
|
if (index + 1 < end) {
|
|
int tail = component.codeUnitAt(index + 1);
|
|
if ((tail & 0xFC00) == 0xDC00) {
|
|
// Tail surrogate.
|
|
sourceLength = 2;
|
|
char = 0x10000 | ((char & 0x3ff) << 10) | (tail & 0x3ff);
|
|
}
|
|
}
|
|
}
|
|
replacement = _escapeChar(char);
|
|
}
|
|
(buffer ??= StringBuffer())
|
|
..write(component.substring(sectionStart, index))
|
|
..write(replacement);
|
|
index += sourceLength;
|
|
sectionStart = index;
|
|
}
|
|
}
|
|
if (buffer == null) {
|
|
return null;
|
|
}
|
|
if (sectionStart < end) {
|
|
buffer.write(component.substring(sectionStart, end));
|
|
}
|
|
return buffer.toString();
|
|
}
|
|
|
|
static bool _isSchemeCharacter(int ch) {
|
|
return ch < 128 && ((_schemeTable[ch >> 4] & (1 << (ch & 0x0f))) != 0);
|
|
}
|
|
|
|
static bool _isGeneralDelimiter(int ch) {
|
|
return ch <= _RIGHT_BRACKET &&
|
|
((_genDelimitersTable[ch >> 4] & (1 << (ch & 0x0f))) != 0);
|
|
}
|
|
|
|
/// Whether the URI is absolute.
|
|
bool get isAbsolute => scheme != "" && fragment == "";
|
|
|
|
String _mergePaths(String base, String reference) {
|
|
// Optimize for the case: absolute base, reference beginning with "../".
|
|
int backCount = 0;
|
|
int refStart = 0;
|
|
// Count number of "../" at beginning of reference.
|
|
while (reference.startsWith("../", refStart)) {
|
|
refStart += 3;
|
|
backCount++;
|
|
}
|
|
|
|
// Drop last segment - everything after last '/' of base.
|
|
int baseEnd = base.lastIndexOf('/');
|
|
// Drop extra segments for each leading "../" of reference.
|
|
while (baseEnd > 0 && backCount > 0) {
|
|
int newEnd = base.lastIndexOf('/', baseEnd - 1);
|
|
if (newEnd < 0) {
|
|
break;
|
|
}
|
|
int delta = baseEnd - newEnd;
|
|
// If we see a "." or ".." segment in base, stop here and let
|
|
// _removeDotSegments handle it.
|
|
if ((delta == 2 || delta == 3) &&
|
|
base.codeUnitAt(newEnd + 1) == _DOT &&
|
|
(delta == 2 || base.codeUnitAt(newEnd + 2) == _DOT)) {
|
|
break;
|
|
}
|
|
baseEnd = newEnd;
|
|
backCount--;
|
|
}
|
|
return base.replaceRange(
|
|
baseEnd + 1, null, reference.substring(refStart - 3 * backCount));
|
|
}
|
|
|
|
/// Make a guess at whether a path contains a `..` or `.` segment.
|
|
///
|
|
/// This is a primitive test that can cause false positives.
|
|
/// It's only used to avoid a more expensive operation in the case where
|
|
/// it's not necessary.
|
|
static bool _mayContainDotSegments(String path) {
|
|
if (path.startsWith('.')) return true;
|
|
int index = path.indexOf("/.");
|
|
return index != -1;
|
|
}
|
|
|
|
/// Removes '.' and '..' segments from a path.
|
|
///
|
|
/// Follows the RFC 2986 "remove dot segments" algorithm.
|
|
/// This algorithm is only used on paths of URIs with a scheme,
|
|
/// and it treats the path as if it is absolute (leading '..' are removed).
|
|
static String _removeDotSegments(String path) {
|
|
if (!_mayContainDotSegments(path)) return path;
|
|
assert(path.isNotEmpty); // An empty path would not have dot segments.
|
|
List<String> output = [];
|
|
bool appendSlash = false;
|
|
for (String segment in path.split("/")) {
|
|
appendSlash = false;
|
|
if (segment == "..") {
|
|
if (output.isNotEmpty) {
|
|
output.removeLast();
|
|
if (output.isEmpty) {
|
|
output.add("");
|
|
}
|
|
}
|
|
appendSlash = true;
|
|
} else if ("." == segment) {
|
|
appendSlash = true;
|
|
} else {
|
|
output.add(segment);
|
|
}
|
|
}
|
|
if (appendSlash) output.add("");
|
|
return output.join("/");
|
|
}
|
|
|
|
/// Removes all `.` segments and any non-leading `..` segments.
|
|
///
|
|
/// If the path starts with something that looks like a scheme,
|
|
/// and [allowScheme] is false, the colon is escaped.
|
|
///
|
|
/// Removing the ".." from a "bar/foo/.." sequence results in "bar/"
|
|
/// (trailing "/"). If the entire path is removed (because it contains as
|
|
/// many ".." segments as real segments), the result is "./".
|
|
/// This is different from an empty string, which represents "no path"
|
|
/// when you resolve it against a base URI with a path with a non-empty
|
|
/// final segment.
|
|
static String _normalizeRelativePath(String path, bool allowScheme) {
|
|
assert(!path.startsWith('/')); // Only get called for relative paths.
|
|
if (!_mayContainDotSegments(path)) {
|
|
if (!allowScheme) path = _escapeScheme(path);
|
|
return path;
|
|
}
|
|
assert(path.isNotEmpty); // An empty path would not have dot segments.
|
|
List<String> output = [];
|
|
bool appendSlash = false;
|
|
for (String segment in path.split("/")) {
|
|
appendSlash = false;
|
|
if (".." == segment) {
|
|
if (!output.isEmpty && output.last != "..") {
|
|
output.removeLast();
|
|
appendSlash = true;
|
|
} else {
|
|
output.add("..");
|
|
}
|
|
} else if ("." == segment) {
|
|
appendSlash = true;
|
|
} else {
|
|
output.add(segment);
|
|
}
|
|
}
|
|
if (output.isEmpty || (output.length == 1 && output[0].isEmpty)) {
|
|
return "./";
|
|
}
|
|
if (appendSlash || output.last == '..') output.add("");
|
|
if (!allowScheme) output[0] = _escapeScheme(output[0]);
|
|
return output.join("/");
|
|
}
|
|
|
|
/// If [path] starts with a valid scheme, escape the percent.
|
|
static String _escapeScheme(String path) {
|
|
if (path.length >= 2 && _isAlphabeticCharacter(path.codeUnitAt(0))) {
|
|
for (int i = 1; i < path.length; i++) {
|
|
int char = path.codeUnitAt(i);
|
|
if (char == _COLON) {
|
|
return "${path.substring(0, i)}%3A${path.substring(i + 1)}";
|
|
}
|
|
if (char > 127 ||
|
|
((_schemeTable[char >> 4] & (1 << (char & 0x0f))) == 0)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return path;
|
|
}
|
|
|
|
Uri resolve(String reference) {
|
|
return resolveUri(Uri.parse(reference));
|
|
}
|
|
|
|
// Returns the index of the `/` after the package name of a package URI.
|
|
//
|
|
// Returns negative if the URI is not a valid package URI:
|
|
// * Scheme must be "package".
|
|
// * No authority.
|
|
// * Path starts with "something"/
|
|
// * where "something" is not all "." characters,
|
|
// * and contains no escapes or colons.
|
|
//
|
|
// The characters are necessarily valid path characters.
|
|
static int _packageNameEnd(Uri uri, String path) {
|
|
if (uri.isScheme("package") && !uri.hasAuthority) {
|
|
return _skipPackageNameChars(path, 0, path.length);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
Uri resolveUri(Uri reference) {
|
|
// From RFC 3986.
|
|
String targetScheme;
|
|
String targetUserInfo = "";
|
|
String? targetHost;
|
|
int? targetPort;
|
|
String targetPath;
|
|
String? targetQuery;
|
|
if (reference.scheme.isNotEmpty) {
|
|
targetScheme = reference.scheme;
|
|
if (reference.hasAuthority) {
|
|
targetUserInfo = reference.userInfo;
|
|
targetHost = reference.host;
|
|
targetPort = reference.hasPort ? reference.port : null;
|
|
}
|
|
targetPath = _removeDotSegments(reference.path);
|
|
if (reference.hasQuery) {
|
|
targetQuery = reference.query;
|
|
}
|
|
} else {
|
|
targetScheme = this.scheme;
|
|
if (reference.hasAuthority) {
|
|
targetUserInfo = reference.userInfo;
|
|
targetHost = reference.host;
|
|
targetPort =
|
|
_makePort(reference.hasPort ? reference.port : null, targetScheme);
|
|
targetPath = _removeDotSegments(reference.path);
|
|
if (reference.hasQuery) targetQuery = reference.query;
|
|
} else {
|
|
targetUserInfo = this._userInfo;
|
|
targetHost = this._host;
|
|
targetPort = this._port;
|
|
if (reference.path == "") {
|
|
targetPath = this.path;
|
|
if (reference.hasQuery) {
|
|
targetQuery = reference.query;
|
|
} else {
|
|
targetQuery = this._query;
|
|
}
|
|
} else {
|
|
String basePath = this.path;
|
|
int packageNameEnd = _packageNameEnd(this, basePath);
|
|
if (packageNameEnd > 0) {
|
|
assert(targetScheme == "package");
|
|
assert(!this.hasAuthority);
|
|
assert(!this.hasEmptyPath);
|
|
// Merging a path into a package URI.
|
|
String packageName = basePath.substring(0, packageNameEnd);
|
|
if (reference.hasAbsolutePath) {
|
|
targetPath = packageName + _removeDotSegments(reference.path);
|
|
} else {
|
|
targetPath = packageName +
|
|
_removeDotSegments(_mergePaths(
|
|
basePath.substring(packageName.length), reference.path));
|
|
}
|
|
} else if (reference.hasAbsolutePath) {
|
|
targetPath = _removeDotSegments(reference.path);
|
|
} else {
|
|
// This is the RFC 3986 behavior for merging.
|
|
if (this.hasEmptyPath) {
|
|
if (!this.hasAuthority) {
|
|
if (!this.hasScheme) {
|
|
// Keep the path relative if no scheme or authority.
|
|
targetPath = reference.path;
|
|
} else {
|
|
// Remove leading dot-segments if the path is put
|
|
// beneath a scheme.
|
|
targetPath = _removeDotSegments(reference.path);
|
|
}
|
|
} else {
|
|
// RFC algorithm for base with authority and empty path.
|
|
targetPath = _removeDotSegments("/" + reference.path);
|
|
}
|
|
} else {
|
|
var mergedPath = _mergePaths(this.path, reference.path);
|
|
if (this.hasScheme || this.hasAuthority || this.hasAbsolutePath) {
|
|
targetPath = _removeDotSegments(mergedPath);
|
|
} else {
|
|
// Non-RFC 3986 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, this.hasScheme || this.hasAuthority);
|
|
}
|
|
}
|
|
}
|
|
if (reference.hasQuery) targetQuery = reference.query;
|
|
}
|
|
}
|
|
}
|
|
String? fragment = reference.hasFragment ? reference.fragment : null;
|
|
return _Uri._internal(targetScheme, targetUserInfo, targetHost, targetPort,
|
|
targetPath, targetQuery, fragment);
|
|
}
|
|
|
|
bool get hasScheme => scheme.isNotEmpty;
|
|
|
|
bool get hasAuthority => _host != null;
|
|
|
|
bool get hasPort => _port != null;
|
|
|
|
bool get hasQuery => _query != null;
|
|
|
|
bool get hasFragment => _fragment != null;
|
|
|
|
bool get hasEmptyPath => path.isEmpty;
|
|
|
|
bool get hasAbsolutePath => path.startsWith('/');
|
|
|
|
String get origin {
|
|
if (scheme == "") {
|
|
throw StateError("Cannot use origin without a scheme: $this");
|
|
}
|
|
if (scheme != "http" && scheme != "https") {
|
|
throw StateError(
|
|
"Origin is only applicable schemes http and https: $this");
|
|
}
|
|
String? host = _host;
|
|
if (host == null || host == "") {
|
|
throw StateError(
|
|
"A $scheme: URI should have a non-empty host name: $this");
|
|
}
|
|
int? port = _port;
|
|
if (port == null) return "$scheme://$host";
|
|
return "$scheme://$host:$port";
|
|
}
|
|
|
|
String toFilePath({bool? windows}) {
|
|
if (scheme != "" && scheme != "file") {
|
|
throw UnsupportedError("Cannot extract a file path from a $scheme URI");
|
|
}
|
|
if (query != "") {
|
|
throw UnsupportedError(
|
|
"Cannot extract a file path from a URI with a query component");
|
|
}
|
|
if (fragment != "") {
|
|
throw UnsupportedError(
|
|
"Cannot extract a file path from a URI with a fragment component");
|
|
}
|
|
return (windows ?? _isWindows) ? _toWindowsFilePath(this) : _toFilePath();
|
|
}
|
|
|
|
String _toFilePath() {
|
|
if (hasAuthority && host != "") {
|
|
throw 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 = StringBuffer();
|
|
if (hasAbsolutePath) result.write("/");
|
|
result.writeAll(pathSegments, "/");
|
|
return result.toString();
|
|
}
|
|
|
|
static String _toWindowsFilePath(Uri uri) {
|
|
bool hasDriveLetter = false;
|
|
var segments = uri.pathSegments;
|
|
if (segments.length > 0 &&
|
|
segments[0].length == 2 &&
|
|
segments[0].codeUnitAt(1) == _COLON) {
|
|
_checkWindowsDriveLetter(segments[0].codeUnitAt(0), false);
|
|
_checkWindowsPathReservedCharacters(segments, false, 1);
|
|
hasDriveLetter = true;
|
|
} else {
|
|
_checkWindowsPathReservedCharacters(segments, false, 0);
|
|
}
|
|
var result = StringBuffer();
|
|
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, r"\");
|
|
if (hasDriveLetter && segments.length == 1) result.write(r"\");
|
|
return result.toString();
|
|
}
|
|
|
|
void _writeAuthority(StringSink ss) {
|
|
if (_userInfo.isNotEmpty) {
|
|
ss.write(_userInfo);
|
|
ss.write("@");
|
|
}
|
|
if (_host != null) ss.write(_host);
|
|
if (_port != null) {
|
|
ss.write(":");
|
|
ss.write(_port);
|
|
}
|
|
}
|
|
|
|
/// 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 => (scheme == "data") ? UriData.fromUri(this) : null;
|
|
|
|
String toString() => _text;
|
|
|
|
String _initializeText() {
|
|
StringBuffer sb = StringBuffer();
|
|
if (scheme.isNotEmpty)
|
|
sb
|
|
..write(scheme)
|
|
..write(":");
|
|
if (hasAuthority || (scheme == "file")) {
|
|
// File URIS always have the authority, even if it is empty.
|
|
// The empty URI means "localhost".
|
|
sb.write("//");
|
|
_writeAuthority(sb);
|
|
}
|
|
sb.write(path);
|
|
if (_query != null)
|
|
sb
|
|
..write("?")
|
|
..write(_query);
|
|
if (_fragment != null)
|
|
sb
|
|
..write("#")
|
|
..write(_fragment);
|
|
return sb.toString();
|
|
}
|
|
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) return true;
|
|
return other is Uri &&
|
|
scheme == other.scheme &&
|
|
hasAuthority == other.hasAuthority &&
|
|
userInfo == other.userInfo &&
|
|
host == other.host &&
|
|
port == other.port &&
|
|
path == other.path &&
|
|
hasQuery == other.hasQuery &&
|
|
query == other.query &&
|
|
hasFragment == other.hasFragment &&
|
|
fragment == other.fragment;
|
|
}
|
|
|
|
static List<String> _createList() => <String>[];
|
|
|
|
static Map<String, List<String>> _splitQueryStringAll(String query,
|
|
{Encoding encoding = utf8}) {
|
|
var result = <String, List<String>>{};
|
|
int i = 0;
|
|
int start = 0;
|
|
int equalsIndex = -1;
|
|
|
|
void parsePair(int start, int equalsIndex, int end) {
|
|
String key;
|
|
String value;
|
|
if (start == end) return;
|
|
if (equalsIndex < 0) {
|
|
key = _uriDecode(query, start, end, encoding, true);
|
|
value = "";
|
|
} else {
|
|
key = _uriDecode(query, start, equalsIndex, encoding, true);
|
|
value = _uriDecode(query, equalsIndex + 1, end, encoding, true);
|
|
}
|
|
result.putIfAbsent(key, _createList).add(value);
|
|
}
|
|
|
|
while (i < query.length) {
|
|
int char = query.codeUnitAt(i);
|
|
if (char == _EQUALS) {
|
|
if (equalsIndex < 0) equalsIndex = i;
|
|
} else if (char == _AMPERSAND) {
|
|
parsePair(start, equalsIndex, i);
|
|
start = i + 1;
|
|
equalsIndex = -1;
|
|
}
|
|
i++;
|
|
}
|
|
parsePair(start, equalsIndex, i);
|
|
return result;
|
|
}
|
|
|
|
external static String _uriEncode(List<int> canonicalTable, String text,
|
|
Encoding encoding, bool spaceToPlus);
|
|
|
|
/// Convert a byte (2 character hex sequence) in string [s] starting
|
|
/// at position [pos] to its ordinal value
|
|
static int _hexCharPairToByte(String s, int pos) {
|
|
int byte = 0;
|
|
for (int i = 0; i < 2; i++) {
|
|
var charCode = s.codeUnitAt(pos + i);
|
|
if (0x30 <= charCode && charCode <= 0x39) {
|
|
byte = byte * 16 + charCode - 0x30;
|
|
} else {
|
|
// Check ranges A-F (0x41-0x46) and a-f (0x61-0x66).
|
|
charCode |= 0x20;
|
|
if (0x61 <= charCode && charCode <= 0x66) {
|
|
byte = byte * 16 + charCode - 0x57;
|
|
} else {
|
|
throw ArgumentError("Invalid URL encoding");
|
|
}
|
|
}
|
|
}
|
|
return byte;
|
|
}
|
|
|
|
/// Uri-decode a percent-encoded string.
|
|
///
|
|
/// It unescapes the string [text] and returns the unescaped string.
|
|
///
|
|
/// This function is similar to the JavaScript-function `decodeURI`.
|
|
///
|
|
/// If [plusToSpace] is `true`, plus characters will be converted to spaces.
|
|
///
|
|
/// The decoder will create a byte-list of the percent-encoded parts, and then
|
|
/// decode the byte-list using [encoding]. The default encoding is UTF-8.
|
|
static String _uriDecode(
|
|
String text, int start, int end, Encoding encoding, bool plusToSpace) {
|
|
assert(0 <= start);
|
|
assert(start <= end);
|
|
assert(end <= text.length);
|
|
// First check whether there is any characters which need special handling.
|
|
bool simple = true;
|
|
for (int i = start; i < end; i++) {
|
|
var codeUnit = text.codeUnitAt(i);
|
|
if (codeUnit > 127 ||
|
|
codeUnit == _PERCENT ||
|
|
(plusToSpace && codeUnit == _PLUS)) {
|
|
simple = false;
|
|
break;
|
|
}
|
|
}
|
|
List<int> bytes;
|
|
if (simple) {
|
|
if (utf8 == encoding || latin1 == encoding || ascii == encoding) {
|
|
return text.substring(start, end);
|
|
} else {
|
|
bytes = text.substring(start, end).codeUnits;
|
|
}
|
|
} else {
|
|
bytes = <int>[];
|
|
for (int i = start; i < end; i++) {
|
|
var codeUnit = text.codeUnitAt(i);
|
|
if (codeUnit > 127) {
|
|
throw ArgumentError("Illegal percent encoding in URI");
|
|
}
|
|
if (codeUnit == _PERCENT) {
|
|
if (i + 3 > text.length) {
|
|
throw ArgumentError('Truncated URI');
|
|
}
|
|
bytes.add(_hexCharPairToByte(text, i + 1));
|
|
i += 2;
|
|
} else if (plusToSpace && codeUnit == _PLUS) {
|
|
bytes.add(_SPACE);
|
|
} else {
|
|
bytes.add(codeUnit);
|
|
}
|
|
}
|
|
}
|
|
return encoding.decode(bytes);
|
|
}
|
|
|
|
static bool _isAlphabeticCharacter(int codeUnit) {
|
|
var lowerCase = codeUnit | 0x20;
|
|
return (_LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_Z);
|
|
}
|
|
|
|
static bool _isUnreservedChar(int char) {
|
|
return char < 127 &&
|
|
((_unreservedTable[char >> 4] & (1 << (char & 0x0f))) != 0);
|
|
}
|
|
|
|
// Tables of char-codes organized as a bit vector of 128 bits where
|
|
// each bit indicate whether a character code on the 0-127 needs to
|
|
// be escaped or not.
|
|
|
|
// The unreserved characters of RFC 3986.
|
|
static const _unreservedTable = <int>[
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
|
// -.
|
|
0x6000, // 0x20 - 0x2f 0000000000000110
|
|
// 0123456789
|
|
0x03ff, // 0x30 - 0x3f 1111111111000000
|
|
// ABCDEFGHIJKLMNO
|
|
0xfffe, // 0x40 - 0x4f 0111111111111111
|
|
// PQRSTUVWXYZ _
|
|
0x87ff, // 0x50 - 0x5f 1111111111100001
|
|
// abcdefghijklmno
|
|
0xfffe, // 0x60 - 0x6f 0111111111111111
|
|
// pqrstuvwxyz ~
|
|
0x47ff, // 0x70 - 0x7f 1111111111100010
|
|
];
|
|
|
|
// The unreserved characters of RFC 2396.
|
|
static const _unreserved2396Table = <int>[
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
|
// ! '()* -.
|
|
0x6782, // 0x20 - 0x2f 0100000111100110
|
|
// 0123456789
|
|
0x03ff, // 0x30 - 0x3f 1111111111000000
|
|
// ABCDEFGHIJKLMNO
|
|
0xfffe, // 0x40 - 0x4f 0111111111111111
|
|
// PQRSTUVWXYZ _
|
|
0x87ff, // 0x50 - 0x5f 1111111111100001
|
|
// abcdefghijklmno
|
|
0xfffe, // 0x60 - 0x6f 0111111111111111
|
|
// pqrstuvwxyz ~
|
|
0x47ff, // 0x70 - 0x7f 1111111111100010
|
|
];
|
|
|
|
// Table of reserved characters specified by ECMAScript 5.
|
|
static const _encodeFullTable = <int>[
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
|
// ! #$ &'()*+,-./
|
|
0xffda, // 0x20 - 0x2f 0101101111111111
|
|
// 0123456789:; = ?
|
|
0xafff, // 0x30 - 0x3f 1111111111110101
|
|
// @ABCDEFGHIJKLMNO
|
|
0xffff, // 0x40 - 0x4f 1111111111111111
|
|
// PQRSTUVWXYZ _
|
|
0x87ff, // 0x50 - 0x5f 1111111111100001
|
|
// abcdefghijklmno
|
|
0xfffe, // 0x60 - 0x6f 0111111111111111
|
|
// pqrstuvwxyz ~
|
|
0x47ff, // 0x70 - 0x7f 1111111111100010
|
|
];
|
|
|
|
// Characters allowed in the scheme.
|
|
static const _schemeTable = <int>[
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
|
// + -.
|
|
0x6800, // 0x20 - 0x2f 0000000000010110
|
|
// 0123456789
|
|
0x03ff, // 0x30 - 0x3f 1111111111000000
|
|
// ABCDEFGHIJKLMNO
|
|
0xfffe, // 0x40 - 0x4f 0111111111111111
|
|
// PQRSTUVWXYZ
|
|
0x07ff, // 0x50 - 0x5f 1111111111100001
|
|
// abcdefghijklmno
|
|
0xfffe, // 0x60 - 0x6f 0111111111111111
|
|
// pqrstuvwxyz
|
|
0x07ff, // 0x70 - 0x7f 1111111111100010
|
|
];
|
|
|
|
// General delimiter characters, RFC 3986 section 2.2.
|
|
// gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
|
|
//
|
|
static const _genDelimitersTable = <int>[
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
|
// # /
|
|
0x8008, // 0x20 - 0x2f 0001000000000001
|
|
// : ?
|
|
0x8400, // 0x30 - 0x3f 0000000000100001
|
|
// @
|
|
0x0001, // 0x40 - 0x4f 1000000000000000
|
|
// [ ]
|
|
0x2800, // 0x50 - 0x5f 0000000000010100
|
|
//
|
|
0x0000, // 0x60 - 0x6f 0000000000000000
|
|
//
|
|
0x0000, // 0x70 - 0x7f 0000000000000000
|
|
];
|
|
|
|
// Characters allowed in the userinfo as of RFC 3986.
|
|
// RFC 3986 Appendix A
|
|
// userinfo = *( unreserved / pct-encoded / sub-delims / ':')
|
|
static const _userinfoTable = <int>[
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
|
// ! $ &'()*+,-.
|
|
0x7fd2, // 0x20 - 0x2f 0100101111111110
|
|
// 0123456789:; =
|
|
0x2fff, // 0x30 - 0x3f 1111111111110100
|
|
// ABCDEFGHIJKLMNO
|
|
0xfffe, // 0x40 - 0x4f 0111111111111111
|
|
// PQRSTUVWXYZ _
|
|
0x87ff, // 0x50 - 0x5f 1111111111100001
|
|
// abcdefghijklmno
|
|
0xfffe, // 0x60 - 0x6f 0111111111111111
|
|
// pqrstuvwxyz ~
|
|
0x47ff, // 0x70 - 0x7f 1111111111100010
|
|
];
|
|
|
|
// Characters allowed in the reg-name as of RFC 3986.
|
|
// RFC 3986 Appendix A
|
|
// reg-name = *( unreserved / pct-encoded / sub-delims )
|
|
static const _regNameTable = <int>[
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
|
// ! $%&'()*+,-.
|
|
0x7ff2, // 0x20 - 0x2f 0100111111111110
|
|
// 0123456789 ; =
|
|
0x2bff, // 0x30 - 0x3f 1111111111010100
|
|
// ABCDEFGHIJKLMNO
|
|
0xfffe, // 0x40 - 0x4f 0111111111111111
|
|
// PQRSTUVWXYZ _
|
|
0x87ff, // 0x50 - 0x5f 1111111111100001
|
|
// abcdefghijklmno
|
|
0xfffe, // 0x60 - 0x6f 0111111111111111
|
|
// pqrstuvwxyz ~
|
|
0x47ff, // 0x70 - 0x7f 1111111111100010
|
|
];
|
|
|
|
// Characters allowed in the path as of RFC 3986.
|
|
// RFC 3986 section 3.3.
|
|
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
|
static const _pathCharTable = <int>[
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
|
// ! $ &'()*+,-.
|
|
0x7fd2, // 0x20 - 0x2f 0100101111111110
|
|
// 0123456789:; =
|
|
0x2fff, // 0x30 - 0x3f 1111111111110100
|
|
// @ABCDEFGHIJKLMNO
|
|
0xffff, // 0x40 - 0x4f 1111111111111111
|
|
// PQRSTUVWXYZ _
|
|
0x87ff, // 0x50 - 0x5f 1111111111100001
|
|
// abcdefghijklmno
|
|
0xfffe, // 0x60 - 0x6f 0111111111111111
|
|
// pqrstuvwxyz ~
|
|
0x47ff, // 0x70 - 0x7f 1111111111100010
|
|
];
|
|
|
|
// Characters allowed in the path as of RFC 3986.
|
|
// RFC 3986 section 3.3 *and* slash.
|
|
static const _pathCharOrSlashTable = [
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
|
// ! $ &'()*+,-./
|
|
0xffd2, // 0x20 - 0x2f 0100101111111111
|
|
// 0123456789:; =
|
|
0x2fff, // 0x30 - 0x3f 1111111111110100
|
|
// @ABCDEFGHIJKLMNO
|
|
0xffff, // 0x40 - 0x4f 1111111111111111
|
|
|
|
// PQRSTUVWXYZ _
|
|
0x87ff, // 0x50 - 0x5f 1111111111100001
|
|
// abcdefghijklmno
|
|
0xfffe, // 0x60 - 0x6f 0111111111111111
|
|
// pqrstuvwxyz ~
|
|
0x47ff, // 0x70 - 0x7f 1111111111100010
|
|
];
|
|
|
|
// Characters allowed in the query as of RFC 3986.
|
|
// RFC 3986 section 3.4.
|
|
// query = *( pchar / "/" / "?" )
|
|
static const _queryCharTable = [
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
|
// ! $ &'()*+,-./
|
|
0xffd2, // 0x20 - 0x2f 0100101111111111
|
|
// 0123456789:; = ?
|
|
0xafff, // 0x30 - 0x3f 1111111111110101
|
|
// @ABCDEFGHIJKLMNO
|
|
0xffff, // 0x40 - 0x4f 1111111111111111
|
|
// PQRSTUVWXYZ _
|
|
0x87ff, // 0x50 - 0x5f 1111111111100001
|
|
// abcdefghijklmno
|
|
0xfffe, // 0x60 - 0x6f 0111111111111111
|
|
// pqrstuvwxyz ~
|
|
0x47ff, // 0x70 - 0x7f 1111111111100010
|
|
];
|
|
|
|
// Characters allowed in the ZoneID as of RFC 6874.
|
|
// ZoneID = 1*( unreserved / pct-encoded )
|
|
static const _zoneIDTable = <int>[
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 0000000000000000
|
|
0x0000, // 0x10 - 0x1f 0000000000000000
|
|
// ! $%&'()*+,-.
|
|
0x6000, // 0x20 - 0x2f 0000000000000110
|
|
// 0123456789 ; =
|
|
0x03ff, // 0x30 - 0x3f 1111111111000000
|
|
// ABCDEFGHIJKLMNO
|
|
0xfffe, // 0x40 - 0x4f 0111111111111111
|
|
// PQRSTUVWXYZ _
|
|
0x87ff, // 0x50 - 0x5f 1111111111100001
|
|
// abcdefghijklmno
|
|
0xfffe, // 0x60 - 0x6f 0111111111111111
|
|
// pqrstuvwxyz ~
|
|
0x47ff, // 0x70 - 0x7f 1111111111100010
|
|
];
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// Data URI
|
|
// --------------------------------------------------------------------
|
|
|
|
/// A way to access the structure of a `data:` URI.
|
|
///
|
|
/// Data URIs are non-hierarchical URIs that can contain any binary data.
|
|
/// They are defined by [RFC 2397](https://tools.ietf.org/html/rfc2397).
|
|
///
|
|
/// This class allows parsing the URI text, extracting individual parts of the
|
|
/// URI, as well as building the URI text from structured parts.
|
|
class UriData {
|
|
static const int _noScheme = -1;
|
|
|
|
/// Contains the text content of a `data:` URI, with or without a
|
|
/// leading `data:`.
|
|
///
|
|
/// If [_separatorIndices] starts with `4` (the index of the `:`), then
|
|
/// there is a leading `data:`, otherwise [_separatorIndices] starts with
|
|
/// `-1`.
|
|
final String _text;
|
|
|
|
/// List of the separators (';', '=' and ',') in the text.
|
|
///
|
|
/// Starts with the index of the `:` in `data:` of the mimeType.
|
|
/// That is always either -1 or 4, depending on whether `_text` includes the
|
|
/// `data:` scheme or not.
|
|
///
|
|
/// The first separator ends the mime type. We don't bother with finding
|
|
/// the '/' inside the mime type.
|
|
///
|
|
/// Each two separators after that mark a parameter key and value.
|
|
///
|
|
/// If there is a single separator left, it ends the "base64" marker.
|
|
///
|
|
/// So the following separators are found for a text:
|
|
/// ```plaintext
|
|
/// data:text/plain;foo=bar;base64,ARGLEBARGLE=
|
|
/// ^ ^ ^ ^ ^
|
|
/// ```
|
|
final List<int> _separatorIndices;
|
|
|
|
/// Cache of the result returned by [uri].
|
|
Uri? _uriCache;
|
|
|
|
UriData._(this._text, this._separatorIndices, this._uriCache);
|
|
|
|
// Avoid shadowing by argument.
|
|
static const Base64Codec _base64 = base64;
|
|
|
|
/// Creates a `data:` URI containing the [content] string.
|
|
///
|
|
/// Equivalent to `Uri.dataFromString(...).data`, but may
|
|
/// be more efficient if the [uri] itself isn't used.
|
|
factory UriData.fromString(String content,
|
|
{String? mimeType,
|
|
Encoding? encoding,
|
|
Map<String, String>? parameters,
|
|
bool base64 = false}) {
|
|
StringBuffer buffer = StringBuffer();
|
|
List<int> indices = [_noScheme];
|
|
String? charsetName = parameters?["charset"];
|
|
String? encodingName;
|
|
if (encoding == null) {
|
|
if (charsetName != null) {
|
|
encoding = Encoding.getByName(charsetName);
|
|
}
|
|
} else if (charsetName == null) {
|
|
// Non-null only if parameters does not contain "charset".
|
|
encodingName = encoding.name;
|
|
}
|
|
encoding ??= ascii;
|
|
_writeUri(mimeType, encodingName, parameters, buffer, indices);
|
|
indices.add(buffer.length);
|
|
if (base64) {
|
|
buffer.write(';base64,');
|
|
indices.add(buffer.length - 1);
|
|
buffer.write(encoding.fuse(_base64).encode(content));
|
|
} else {
|
|
buffer.write(',');
|
|
_uriEncodeBytes(_uricTable, encoding.encode(content), buffer);
|
|
}
|
|
return UriData._(buffer.toString(), indices, null);
|
|
}
|
|
|
|
/// Creates a `data:` URI containing an encoding of [bytes].
|
|
///
|
|
/// Equivalent to `Uri.dataFromBytes(...).data`, but may
|
|
/// be more efficient if the [uri] itself isn't used.
|
|
factory UriData.fromBytes(List<int> bytes,
|
|
{String mimeType = "application/octet-stream",
|
|
Map<String, String>? parameters,
|
|
bool percentEncoded = false}) {
|
|
StringBuffer buffer = StringBuffer();
|
|
List<int> indices = [_noScheme];
|
|
_writeUri(mimeType, null, parameters, buffer, indices);
|
|
indices.add(buffer.length);
|
|
if (percentEncoded) {
|
|
buffer.write(',');
|
|
_uriEncodeBytes(_uricTable, bytes, buffer);
|
|
} else {
|
|
buffer.write(';base64,');
|
|
indices.add(buffer.length - 1);
|
|
_base64.encoder
|
|
.startChunkedConversion(StringConversionSink.fromStringSink(buffer))
|
|
.addSlice(bytes, 0, bytes.length, true);
|
|
}
|
|
|
|
return UriData._(buffer.toString(), indices, null);
|
|
}
|
|
|
|
/// Creates a `DataUri` from a [Uri] which must have `data` as [Uri.scheme].
|
|
///
|
|
/// The [uri] must have scheme `data` and no authority or fragment,
|
|
/// and the path (concatenated with the query, if there is one) must be valid
|
|
/// as data URI content with the same rules as [parse].
|
|
factory UriData.fromUri(Uri uri) {
|
|
if (!uri.isScheme("data")) {
|
|
throw ArgumentError.value(uri, "uri", "Scheme must be 'data'");
|
|
}
|
|
if (uri.hasAuthority) {
|
|
throw ArgumentError.value(uri, "uri", "Data uri must not have authority");
|
|
}
|
|
if (uri.hasFragment) {
|
|
throw ArgumentError.value(
|
|
uri, "uri", "Data uri must not have a fragment part");
|
|
}
|
|
if (!uri.hasQuery) {
|
|
return _parse(uri.path, 0, uri);
|
|
}
|
|
// Includes path and query (and leading "data:").
|
|
return _parse(uri.toString(), 5, uri);
|
|
}
|
|
|
|
/// Writes the initial part of a `data:` uri, from after the "data:"
|
|
/// until just before the ',' before the data, or before a `;base64,`
|
|
/// marker.
|
|
///
|
|
/// If an [indices] list is passed, separator indices are stored in that
|
|
/// list.
|
|
static void _writeUri(
|
|
String? mimeType,
|
|
String? charsetName,
|
|
Map<String, String>? parameters,
|
|
StringBuffer buffer,
|
|
List<int>? indices) {
|
|
if (mimeType == null || _caseInsensitiveEquals("text/plain", mimeType)) {
|
|
mimeType = "";
|
|
}
|
|
|
|
if (mimeType.isEmpty || identical(mimeType, "application/octet-stream")) {
|
|
buffer.write(mimeType); // Common cases need no escaping.
|
|
} else {
|
|
int slashIndex = _validateMimeType(mimeType);
|
|
if (slashIndex < 0) {
|
|
throw ArgumentError.value(mimeType, "mimeType", "Invalid MIME type");
|
|
}
|
|
buffer.write(_Uri._uriEncode(
|
|
_tokenCharTable, mimeType.substring(0, slashIndex), utf8, false));
|
|
buffer.write("/");
|
|
buffer.write(_Uri._uriEncode(
|
|
_tokenCharTable, mimeType.substring(slashIndex + 1), utf8, false));
|
|
}
|
|
if (charsetName != null) {
|
|
indices
|
|
?..add(buffer.length)
|
|
..add(buffer.length + 8);
|
|
buffer.write(";charset=");
|
|
buffer.write(_Uri._uriEncode(_tokenCharTable, charsetName, utf8, false));
|
|
}
|
|
parameters?.forEach((key, value) {
|
|
if (key.isEmpty) {
|
|
throw ArgumentError.value("", "Parameter names must not be empty");
|
|
}
|
|
if (value.isEmpty) {
|
|
throw ArgumentError.value(
|
|
"", "Parameter values must not be empty", 'parameters["$key"]');
|
|
}
|
|
indices?.add(buffer.length);
|
|
buffer.write(';');
|
|
// Encode any non-RFC2045-token character and both '%' and '#'.
|
|
buffer.write(_Uri._uriEncode(_tokenCharTable, key, utf8, false));
|
|
indices?.add(buffer.length);
|
|
buffer.write('=');
|
|
buffer.write(_Uri._uriEncode(_tokenCharTable, value, utf8, false));
|
|
});
|
|
}
|
|
|
|
/// Checks mimeType is valid-ish (`token '/' token`).
|
|
///
|
|
/// Returns the index of the slash, or -1 if the mime type is not
|
|
/// considered valid.
|
|
///
|
|
/// Currently only looks for slashes, all other characters will be
|
|
/// percent-encoded as UTF-8 if necessary.
|
|
static int _validateMimeType(String mimeType) {
|
|
int slashIndex = -1;
|
|
for (int i = 0; i < mimeType.length; i++) {
|
|
var char = mimeType.codeUnitAt(i);
|
|
if (char != _SLASH) continue;
|
|
if (slashIndex < 0) {
|
|
slashIndex = i;
|
|
continue;
|
|
}
|
|
return -1;
|
|
}
|
|
return slashIndex;
|
|
}
|
|
|
|
/// Parses a string as a `data` URI.
|
|
///
|
|
/// The string must have the format:
|
|
///
|
|
/// ```plaintext
|
|
/// 'data:' (type '/' subtype)? (';' attribute '=' value)* (';base64')? ',' data
|
|
/// ````
|
|
///
|
|
/// where `type`, `subtype`, `attribute` and `value` are specified in RFC-2045,
|
|
/// 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
|
|
/// to be converted to the corresponding string.
|
|
///
|
|
/// Parsing checks that Base64 encoded data is valid, and it normalizes it
|
|
/// to use the default Base64 alphabet and to use padding.
|
|
/// Non-Base64 data is escaped using percent-escapes as necessary to make
|
|
/// it valid, and existing escapes are case normalized.
|
|
///
|
|
/// Accessing the individual parts may fail later if they turn out to have
|
|
/// content that cannot be decoded successfully as a string, for example if
|
|
/// existing percent escapes represent bytes that cannot be decoded
|
|
/// by the chosen [Encoding] (see [contentAsString]).
|
|
///
|
|
/// A [FormatException] is thrown if [uri] is not a valid data URI.
|
|
static UriData parse(String uri) {
|
|
if (uri.length >= 5) {
|
|
int dataDelta = _startsWithData(uri, 0);
|
|
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);
|
|
}
|
|
}
|
|
throw FormatException("Does not start with 'data:'", uri, 0);
|
|
}
|
|
|
|
/// The [Uri] that this `UriData` is giving access to.
|
|
///
|
|
/// Returns a `Uri` with scheme `data` and the remainder of the data URI
|
|
/// as path.
|
|
Uri get uri {
|
|
return _uriCache ??= _computeUri();
|
|
}
|
|
|
|
Uri _computeUri() {
|
|
String path = _text;
|
|
String? query;
|
|
int colonIndex = _separatorIndices[0];
|
|
int queryIndex = _text.indexOf('?', colonIndex + 1);
|
|
int end = _text.length;
|
|
if (queryIndex >= 0) {
|
|
query = _Uri._normalizeOrSubstring(
|
|
_text, queryIndex + 1, end, _Uri._queryCharTable);
|
|
end = queryIndex;
|
|
}
|
|
path = _Uri._normalizeOrSubstring(
|
|
_text, colonIndex + 1, end, _Uri._pathCharOrSlashTable);
|
|
return _DataUri(this, path, query);
|
|
}
|
|
|
|
/// The MIME type of the data URI.
|
|
///
|
|
/// A data URI consists of a "media type" followed by data.
|
|
/// The media type starts with a MIME type and can be followed by
|
|
/// extra parameters.
|
|
/// If the MIME type representation in the URI text contains URI escapes,
|
|
/// they are unescaped in the returned string.
|
|
/// If the value contain non-ASCII percent escapes, they are decoded as UTF-8.
|
|
///
|
|
/// Example:
|
|
/// ```
|
|
/// data:text/plain;charset=utf-8,Hello%20World!
|
|
/// ```
|
|
/// This data URI has the media type `text/plain;charset=utf-8`, which is the
|
|
/// MIME type `text/plain` with the parameter `charset` with value `utf-8`.
|
|
/// See [RFC 2045](https://tools.ietf.org/html/rfc2045) for more detail.
|
|
///
|
|
/// If the first part of the data URI is empty, it defaults to `text/plain`.
|
|
String get mimeType {
|
|
int start = _separatorIndices[0] + 1;
|
|
int end = _separatorIndices[1];
|
|
if (start == end) return "text/plain";
|
|
return _Uri._uriDecode(_text, start, end, utf8, false);
|
|
}
|
|
|
|
/// Whether the [UriData.mimeType] is equal to [mimeType].
|
|
///
|
|
/// Compares the `data:` URI's MIME type to [mimeType] with a case-
|
|
/// insensitive comparison which ignores the case of ASCII letters.
|
|
///
|
|
/// An empty [mimeType] is considered equivalent to `text/plain`,
|
|
/// both in the [mimeType] argument and in the `data:` URI itself.
|
|
@Since("2.17")
|
|
bool isMimeType(String mimeType) {
|
|
int start = _separatorIndices[0] + 1;
|
|
int end = _separatorIndices[1];
|
|
if (start == end) {
|
|
return mimeType.isEmpty ||
|
|
identical(mimeType, "text/plain") ||
|
|
_caseInsensitiveEquals(mimeType, "text/plain");
|
|
}
|
|
if (mimeType.isEmpty) mimeType = "text/plain";
|
|
return (mimeType.length == end - start) &&
|
|
_caseInsensitiveStartsWith(mimeType, _text, start);
|
|
}
|
|
|
|
/// The charset parameter of the media type.
|
|
///
|
|
/// If the parameters of the media type contains a `charset` parameter
|
|
/// then this returns its value, otherwise it returns `US-ASCII`,
|
|
/// which is the default charset for data URIs.
|
|
/// If the values contain non-ASCII percent escapes, they are decoded as UTF-8.
|
|
///
|
|
/// If the MIME type representation in the URI text contains URI escapes,
|
|
/// they are unescaped in the returned string.
|
|
String get charset {
|
|
var charsetIndex = _findCharsetIndex();
|
|
if (charsetIndex >= 0) {
|
|
var valueStart = _separatorIndices[charsetIndex + 1] + 1;
|
|
var valueEnd = _separatorIndices[charsetIndex + 2];
|
|
return _Uri._uriDecode(_text, valueStart, valueEnd, utf8, false);
|
|
}
|
|
return "US-ASCII";
|
|
}
|
|
|
|
/// Finds the index of the separator before the "charset" parameter.
|
|
///
|
|
/// Returns the index in [_separatorIndices] of the separator before
|
|
/// the name of the "charset" parameter, or -1 if there is no "charset"
|
|
/// parameter.
|
|
int _findCharsetIndex() {
|
|
var separatorIndices = _separatorIndices;
|
|
// Loop over all MIME-type parameters.
|
|
// Check that the parameter can have two parts (key/value)
|
|
// to ignore a trailing base-64 marker.
|
|
for (int i = 3; i <= separatorIndices.length; i += 2) {
|
|
var keyStart = separatorIndices[i - 2] + 1;
|
|
var keyEnd = separatorIndices[i - 1];
|
|
if (keyEnd == keyStart + "charset".length &&
|
|
_caseInsensitiveStartsWith("charset", _text, keyStart)) {
|
|
return i - 2;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// Checks whether the charset parameter of the mime type is [charset].
|
|
///
|
|
/// If this URI has no "charset" parameter, it is assumed to have a default
|
|
/// of `charset=US-ASCII`.
|
|
/// If [charset] is empty, it's treated like `"US-ASCII"`.
|
|
///
|
|
/// Returns true if [charset] and the "charset" parameter value are
|
|
/// equal strings, ignoring the case of ASCII letters, or both
|
|
/// correspond to the same [Encoding], as given by [Encoding.getByName].
|
|
@Since("2.17")
|
|
bool isCharset(String charset) {
|
|
var charsetIndex = _findCharsetIndex();
|
|
if (charsetIndex < 0) {
|
|
return charset.isEmpty ||
|
|
_caseInsensitiveEquals(charset, "US-ASCII") ||
|
|
identical(Encoding.getByName(charset), ascii);
|
|
}
|
|
if (charset.isEmpty) charset = "US-ASCII";
|
|
var valueStart = _separatorIndices[charsetIndex + 1] + 1;
|
|
var valueEnd = _separatorIndices[charsetIndex + 2];
|
|
var length = valueEnd - valueStart;
|
|
if (charset.length == length &&
|
|
_caseInsensitiveStartsWith(charset, _text, valueStart)) {
|
|
return true;
|
|
}
|
|
var checkedEncoding = Encoding.getByName(charset);
|
|
return checkedEncoding != null &&
|
|
identical(
|
|
checkedEncoding,
|
|
Encoding.getByName(
|
|
_Uri._uriDecode(_text, valueStart, valueEnd, utf8, false)));
|
|
}
|
|
|
|
/// Whether the charset parameter represents [encoding].
|
|
///
|
|
/// If the "charset" parameter is not present in the URI,
|
|
/// it defaults to "US-ASCII", which is the [ascii] encoding.
|
|
/// If present, it's converted to an [Encoding] using [Encoding.getByName],
|
|
/// and compared to [encoding].
|
|
@Since("2.17")
|
|
bool isEncoding(Encoding encoding) {
|
|
var charsetIndex = _findCharsetIndex();
|
|
if (charsetIndex < 0) {
|
|
return identical(encoding, ascii);
|
|
}
|
|
var valueStart = _separatorIndices[charsetIndex + 1] + 1;
|
|
var valueEnd = _separatorIndices[charsetIndex + 2];
|
|
return identical(
|
|
encoding,
|
|
Encoding.getByName(
|
|
_Uri._uriDecode(_text, valueStart, valueEnd, utf8, false)));
|
|
}
|
|
|
|
/// Whether the data is Base64 encoded or not.
|
|
bool get isBase64 => _separatorIndices.length.isOdd;
|
|
|
|
/// The content part of the data URI, as its actual representation.
|
|
///
|
|
/// This string may contain percent escapes.
|
|
String get contentText => _text.substring(_separatorIndices.last + 1);
|
|
|
|
/// The content part of the data URI as bytes.
|
|
///
|
|
/// If the data is Base64 encoded, it will be decoded to bytes.
|
|
///
|
|
/// If the data is not Base64 encoded, it will be decoded by unescaping
|
|
/// percent-escaped characters and returning byte values of each unescaped
|
|
/// character. The bytes will not be, e.g., UTF-8 decoded.
|
|
Uint8List contentAsBytes() {
|
|
String text = _text;
|
|
int start = _separatorIndices.last + 1;
|
|
if (isBase64) {
|
|
return base64.decoder.convert(text, start);
|
|
}
|
|
|
|
// Not base64, do percent-decoding and return the remaining bytes.
|
|
// Compute result size.
|
|
const int percent = 0x25;
|
|
int length = text.length - start;
|
|
for (int i = start; i < text.length; i++) {
|
|
var codeUnit = text.codeUnitAt(i);
|
|
if (codeUnit == percent) {
|
|
i += 2;
|
|
length -= 2;
|
|
}
|
|
}
|
|
// Fill result array.
|
|
Uint8List result = Uint8List(length);
|
|
if (length == text.length) {
|
|
result.setRange(0, length, text.codeUnits, start);
|
|
return result;
|
|
}
|
|
int index = 0;
|
|
for (int i = start; i < text.length; i++) {
|
|
var codeUnit = text.codeUnitAt(i);
|
|
if (codeUnit != percent) {
|
|
result[index++] = codeUnit;
|
|
} else {
|
|
if (i + 2 < text.length) {
|
|
int byte = parseHexByte(text, i + 1);
|
|
if (byte >= 0) {
|
|
result[index++] = byte;
|
|
i += 2;
|
|
continue;
|
|
}
|
|
}
|
|
throw FormatException("Invalid percent escape", text, i);
|
|
}
|
|
}
|
|
assert(index == result.length);
|
|
return result;
|
|
}
|
|
|
|
/// Creates a string from the content of the data URI.
|
|
///
|
|
/// 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 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.
|
|
///
|
|
/// If the content is not Base64 encoded, it will first have percent-escapes
|
|
/// converted to bytes and then the character codes and byte values are
|
|
/// decoded using [encoding].
|
|
String contentAsString({Encoding? encoding}) {
|
|
if (encoding == null) {
|
|
var charset = this.charset; // Returns "US-ASCII" if not present.
|
|
encoding = Encoding.getByName(charset);
|
|
if (encoding == null) {
|
|
throw UnsupportedError("Unknown charset: $charset");
|
|
}
|
|
}
|
|
String text = _text;
|
|
int start = _separatorIndices.last + 1;
|
|
if (isBase64) {
|
|
var converter = base64.decoder.fuse(encoding.decoder);
|
|
return converter.convert(text.substring(start));
|
|
}
|
|
return _Uri._uriDecode(text, start, text.length, encoding, false);
|
|
}
|
|
|
|
/// A map representing the parameters of the media type.
|
|
///
|
|
/// A data URI may contain parameters between the MIME type and the
|
|
/// data. This converts these parameters to a map from parameter name
|
|
/// to parameter value.
|
|
/// The map only contains parameters that actually occur in the URI.
|
|
/// The `charset` parameter has a default value even if it doesn't occur
|
|
/// in the URI, which is reflected by the [charset] getter. This means that
|
|
/// [charset] may return a value even if `parameters["charset"]` is `null`.
|
|
///
|
|
/// If the values contain non-ASCII values or percent escapes,
|
|
/// they are decoded as UTF-8.
|
|
Map<String, String> get parameters {
|
|
var result = <String, String>{};
|
|
for (int i = 3; i < _separatorIndices.length; i += 2) {
|
|
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);
|
|
result[key] = value;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static UriData _parse(String text, int start, Uri? sourceUri) {
|
|
assert(start == 0 || start == 5);
|
|
assert((start == 5) == text.startsWith("data:"));
|
|
|
|
/// Character codes.
|
|
const int comma = 0x2c;
|
|
const int slash = 0x2f;
|
|
const int semicolon = 0x3b;
|
|
const int equals = 0x3d;
|
|
List<int> indices = [start - 1];
|
|
int slashIndex = -1;
|
|
var char;
|
|
int i = start;
|
|
for (; i < text.length; i++) {
|
|
char = text.codeUnitAt(i);
|
|
if (char == comma || char == semicolon) break;
|
|
if (char == slash) {
|
|
if (slashIndex < 0) {
|
|
slashIndex = i;
|
|
continue;
|
|
}
|
|
throw FormatException("Invalid MIME type", text, i);
|
|
}
|
|
}
|
|
if (slashIndex < 0 && i > start) {
|
|
// An empty MIME type is allowed, but if non-empty it must contain
|
|
// exactly one slash.
|
|
throw FormatException("Invalid MIME type", text, i);
|
|
}
|
|
while (char != comma) {
|
|
// Parse parameters and/or "base64".
|
|
indices.add(i);
|
|
i++;
|
|
int equalsIndex = -1;
|
|
for (; i < text.length; i++) {
|
|
char = text.codeUnitAt(i);
|
|
if (char == equals) {
|
|
if (equalsIndex < 0) equalsIndex = i;
|
|
} else if (char == semicolon || char == comma) {
|
|
break;
|
|
}
|
|
}
|
|
if (equalsIndex >= 0) {
|
|
indices.add(equalsIndex);
|
|
} else {
|
|
// Have to be final "base64".
|
|
var lastSeparator = indices.last;
|
|
if (char != comma ||
|
|
i != lastSeparator + 7 /* "base64,".length */ ||
|
|
!text.startsWith("base64", lastSeparator + 1)) {
|
|
throw FormatException("Expecting '='", text, i);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
indices.add(i);
|
|
bool isBase64 = indices.length.isOdd;
|
|
if (isBase64) {
|
|
text = base64.normalize(text, i + 1, text.length);
|
|
} else {
|
|
// Validate "data" part, must only contain RFC 2396 'uric' characters
|
|
// (reserved, unreserved, or escape sequences).
|
|
// Normalize to this (throws on a fragment separator).
|
|
var data = _Uri._normalize(text, i + 1, text.length, _uricTable,
|
|
escapeDelimiters: true);
|
|
if (data != null) {
|
|
text = text.replaceRange(i + 1, text.length, data);
|
|
}
|
|
}
|
|
return UriData._(text, indices, sourceUri);
|
|
}
|
|
|
|
/// Like [Uri._uriEncode] but takes the input as bytes, not a string.
|
|
///
|
|
/// Encodes into [buffer] instead of creating its own buffer.
|
|
static void _uriEncodeBytes(
|
|
List<int> canonicalTable, List<int> bytes, StringSink buffer) {
|
|
// Encode the string into bytes then generate an ASCII only string
|
|
// by percent encoding selected bytes.
|
|
int byteOr = 0;
|
|
for (int i = 0; i < bytes.length; i++) {
|
|
int byte = bytes[i];
|
|
byteOr |= byte;
|
|
if (byte < 128 &&
|
|
((canonicalTable[byte >> 4] & (1 << (byte & 0x0f))) != 0)) {
|
|
buffer.writeCharCode(byte);
|
|
} else {
|
|
buffer.writeCharCode(_PERCENT);
|
|
buffer.writeCharCode(_hexDigits.codeUnitAt(byte >> 4));
|
|
buffer.writeCharCode(_hexDigits.codeUnitAt(byte & 0x0f));
|
|
}
|
|
}
|
|
if ((byteOr & ~0xFF) != 0) {
|
|
for (int i = 0; i < bytes.length; i++) {
|
|
var byte = bytes[i];
|
|
if (byte < 0 || byte > 255) {
|
|
throw ArgumentError.value(byte, "non-byte value");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
String toString() =>
|
|
(_separatorIndices[0] == _noScheme) ? "data:$_text" : _text;
|
|
|
|
// Table of the `token` characters of RFC 2045 in a URI.
|
|
//
|
|
// A token is any US-ASCII character except SPACE, control characters and
|
|
// `tspecial` characters. The `tspecial` category is:
|
|
// '(', ')', '<', '>', '@', ',', ';', ':', '\', '"', '/', '[, ']', '?', '='.
|
|
//
|
|
// In a data URI, we also need to escape '%' and '#' characters.
|
|
static const _tokenCharTable = [
|
|
// LSB MSB
|
|
// | |
|
|
0x0000, // 0x00 - 0x0f 00000000 00000000
|
|
0x0000, // 0x10 - 0x1f 00000000 00000000
|
|
// ! $ &' *+ -.
|
|
0x6cd2, // 0x20 - 0x2f 01001011 00110110
|
|
// 01234567 89
|
|
0x03ff, // 0x30 - 0x3f 11111111 11000000
|
|
// ABCDEFG HIJKLMNO
|
|
0xfffe, // 0x40 - 0x4f 01111111 11111111
|
|
// PQRSTUVW XYZ ^_
|
|
0xc7ff, // 0x50 - 0x5f 11111111 11100011
|
|
// `abcdefg hijklmno
|
|
0xffff, // 0x60 - 0x6f 11111111 11111111
|
|
// pqrstuvw xyz{|}~
|
|
0x7fff, // 0x70 - 0x7f 11111111 11111110
|
|
];
|
|
|
|
// All non-escape RFC-2396 uric characters.
|
|
//
|
|
// uric = reserved | unreserved | escaped
|
|
// reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | ","
|
|
// unreserved = alphanum | mark
|
|
// mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
|
|
//
|
|
// This is the same characters as in a URI query (which is URI pchar plus '?')
|
|
static const _uricTable = _Uri._queryCharTable;
|
|
}
|
|
|
|
// --------------------------------------------------------------------
|
|
// Constants used to read the scanner result.
|
|
// The indices points into the table filled by [_scan] 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 are 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 the 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 [Uri.parse].
|
|
///
|
|
/// 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.
|
|
///
|
|
/// The states (starting from [_uriStart]) write positions while scanning
|
|
/// a string from `start` to `end` as follows:
|
|
///
|
|
/// - [_schemeEndIndex]: Should be initialized to `start-1`.
|
|
/// If the URI has a scheme, it is set to the position of the `:` after
|
|
/// the scheme.
|
|
/// - [_hostStartIndex]: Should be initialized to `start - 1`.
|
|
/// If the URI has an authority, it is set to the character before the
|
|
/// host name - either the second `/` in the `//` leading the authority,
|
|
/// or the `@` after a user-info. Comparing this value to the scheme end
|
|
/// position can be used to detect that there is a user-info component.
|
|
/// - [_portStartIndex]: Should be initialized to `start`.
|
|
/// Set to the position of the last `:` in an authority, and unchanged
|
|
/// if there is no authority or no `:` in an authority.
|
|
/// If this position is after the host start, there is a port, otherwise it
|
|
/// is just marking a colon in the user-info component.
|
|
/// - [_pathStartIndex]: Should be initialized to `start`.
|
|
/// Is set to the first path character unless the path is empty.
|
|
/// If the path is empty, the position is either unchanged (`start`) or
|
|
/// the first slash of an authority. So, if the path start is before a
|
|
/// host start or scheme end, the path is empty.
|
|
/// - [_queryStartIndex]: Should be initialized to `end`.
|
|
/// The position of the `?` leading a query if the URI contains a query.
|
|
/// - [_fragmentStartIndex]: Should be initialized to `end`.
|
|
/// The position of the `#` leading a fragment if the URI contains a fragment.
|
|
/// - [_notSimpleIndex]: Should be initialized to `start - 1`.
|
|
/// Set to another value if the URI is considered "not simple".
|
|
/// This is elaborated below.
|
|
///
|
|
/// # 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 (the scheme is handled in a second scan using
|
|
/// a separate two-state table).
|
|
/// Further, paths containing `..` or `.` path segments are considered
|
|
/// non-simple except for pure relative paths (no scheme or authority) starting
|
|
/// with a sequence of "../" segments.
|
|
///
|
|
/// The transition tables cannot detect a trailing ".." in the path,
|
|
/// followed by a query or fragment, because the segment is not known to be
|
|
/// complete until we are past it, and we then need to store the query/fragment
|
|
/// start instead. This cast is checked manually post-scanning (such a path
|
|
/// needs to be normalized to end in "../", so the URI shouldn't be considered
|
|
/// simple).
|
|
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 = List<Uint8List>.generate(stateCount, (_) => 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, r"\", authOrPathSlash | notSimple);
|
|
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, r"/\", 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, r"\", relPathSeg | notSimple);
|
|
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, r"\", pathSeg | notSimple);
|
|
setChars(b, "?", query | queryStart);
|
|
setChars(b, "#", fragment | fragmentStart);
|
|
|
|
b = build(authOrPath, path | notSimple);
|
|
setChars(b, pchar, path | pathStart);
|
|
setChars(b, "/", authOrPathSlash | pathStart);
|
|
setChars(b, r"\", authOrPathSlash | pathStart); // This should be non-simple.
|
|
setChars(b, ".", pathSegDot | pathStart);
|
|
setChars(b, "?", query | queryStart);
|
|
setChars(b, "#", fragment | fragmentStart);
|
|
|
|
b = build(authOrPathSlash, path | notSimple);
|
|
setChars(b, pchar, path);
|
|
setChars(b, "/", uinfoOrHost0 | hostStart);
|
|
setChars(b, r"\", uinfoOrHost0 | hostStart); // This should be non-simple.
|
|
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, r"\", pathSeg | pathStart); // This should be non-simple.
|
|
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, r"\", pathSeg | pathStart); // This should be non-simple.
|
|
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, r"\", pathSeg | pathStart); // This should be non-simple.
|
|
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, r"\", pathSeg | pathStart); // This should be non-simple.
|
|
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, r"/\", pathSeg | notSimple);
|
|
setChars(b, "?", query | queryStart);
|
|
setChars(b, "#", fragment | fragmentStart);
|
|
|
|
b = build(relPathSegDot, path | notSimple);
|
|
setChars(b, pchar, path);
|
|
setChars(b, ".", relPathSegDot2);
|
|
setChars(b, r"/\", pathSeg | notSimple);
|
|
setChars(b, "?", query | queryStart);
|
|
setChars(b, "#", fragment | fragmentStart);
|
|
|
|
b = build(relPathSegDot2, path | notSimple);
|
|
setChars(b, pchar, path);
|
|
setChars(b, "/", relPathSeg);
|
|
setChars(b, r"\", relPathSeg | notSimple);
|
|
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);
|
|
setChars(b, r"\", pathSeg | notSimple);
|
|
setChars(b, "?", query | queryStart);
|
|
setChars(b, "#", fragment | fragmentStart);
|
|
|
|
b = build(pathSegDot, path | notSimple);
|
|
setChars(b, pchar, path);
|
|
setChars(b, ".", pathSegDot2);
|
|
setChars(b, r"/\", pathSeg | notSimple);
|
|
setChars(b, "?", query | queryStart);
|
|
setChars(b, "#", fragment | fragmentStart);
|
|
|
|
b = build(pathSegDot2, path | notSimple);
|
|
setChars(b, pchar, path);
|
|
setChars(b, r"/\", pathSeg | notSimple);
|
|
setChars(b, "?", query | queryStart);
|
|
setChars(b, "#", fragment | fragmentStart);
|
|
|
|
b = build(path, path | notSimple);
|
|
setChars(b, pchar, path);
|
|
setChars(b, "/", pathSeg);
|
|
setChars(b, r"\", pathSeg | notSimple);
|
|
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], starting 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;
|
|
}
|
|
|
|
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;
|
|
int? _hashCodeCache;
|
|
|
|
_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");
|
|
|
|
/// Like [isScheme] but expects argument to be case normalized.
|
|
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;
|
|
|
|
bool isScheme(String scheme) {
|
|
if (scheme == null || scheme.isEmpty) return _schemeEnd < 0;
|
|
if (scheme.length != _schemeEnd) return false;
|
|
return _caseInsensitiveStartsWith(scheme, _uri, 0);
|
|
}
|
|
|
|
String get scheme {
|
|
return _schemeCache ??= _computeScheme();
|
|
}
|
|
|
|
String _computeScheme() {
|
|
if (_schemeEnd <= 0) return "";
|
|
if (_isHttp) return "http";
|
|
if (_isHttps) return "https";
|
|
if (_isFile) return "file";
|
|
if (_isPackage) return "package";
|
|
return _uri.substring(0, _schemeEnd);
|
|
}
|
|
|
|
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) {
|
|
throw StateError("Cannot use origin without a scheme: $this");
|
|
}
|
|
if (!isHttp && !_isHttps) {
|
|
throw StateError(
|
|
"Origin is only applicable to schemes http and https: $this");
|
|
}
|
|
if (_hostStart == _portStart) {
|
|
throw StateError(
|
|
"A $scheme: URI should have a non-empty host name: $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 List<String>.unmodifiable(parts);
|
|
}
|
|
|
|
Map<String, String> get queryParameters {
|
|
if (!hasQuery) return const <String, String>{};
|
|
return UnmodifiableMapView<String, String>(Uri.splitQueryString(query));
|
|
}
|
|
|
|
Map<String, List<String>> get queryParametersAll {
|
|
if (!hasQuery) return const <String, List<String>>{};
|
|
Map<String, List<String>> queryParameterLists =
|
|
_Uri._splitQueryStringAll(query);
|
|
queryParameterLists.updateAll(_toUnmodifiableStringList);
|
|
return 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 _SimpleUri(_uri.substring(0, _fragmentStart), _schemeEnd, _hostStart,
|
|
_portStart, _pathStart, _queryStart, _fragmentStart, _schemeCache);
|
|
}
|
|
|
|
Uri replace(
|
|
{String? scheme,
|
|
String? userInfo,
|
|
String? host,
|
|
int? port,
|
|
String? path,
|
|
Iterable<String>? pathSegments,
|
|
String? query,
|
|
Map<String, dynamic /*String?|Iterable<String>*/ >? queryParameters,
|
|
String? fragment}) {
|
|
bool schemeChanged = false;
|
|
if (scheme != null) {
|
|
scheme = _Uri._makeScheme(scheme, 0, scheme.length);
|
|
schemeChanged = !_isScheme(scheme);
|
|
} else {
|
|
scheme = this.scheme;
|
|
}
|
|
bool isFile = (scheme == "file");
|
|
if (userInfo != null) {
|
|
userInfo = _Uri._makeUserInfo(userInfo, 0, userInfo.length);
|
|
} else if (_hostStart > 0) {
|
|
userInfo = _uri.substring(_schemeEnd + 3, _hostStart);
|
|
} else {
|
|
userInfo = "";
|
|
}
|
|
if (port != null) {
|
|
port = _Uri._makePort(port, scheme);
|
|
} else {
|
|
port = this.hasPort ? this.port : null;
|
|
if (schemeChanged) {
|
|
// The default port might have changed.
|
|
port = _Uri._makePort(port, scheme);
|
|
}
|
|
}
|
|
if (host != null) {
|
|
host = _Uri._makeHost(host, 0, host.length, false);
|
|
} else if (_hostStart > 0) {
|
|
host = _uri.substring(_hostStart, _portStart);
|
|
} else if (userInfo.isNotEmpty || port != null || isFile) {
|
|
host = "";
|
|
}
|
|
|
|
bool hasAuthority = host != null;
|
|
if (path != null || pathSegments != null) {
|
|
path = _Uri._makePath(path, 0, _stringOrNullLength(path), pathSegments,
|
|
scheme, hasAuthority);
|
|
} else {
|
|
path = _uri.substring(_pathStart, _queryStart);
|
|
if ((isFile || (hasAuthority && !path.isEmpty)) &&
|
|
!path.startsWith('/')) {
|
|
path = "/" + path;
|
|
}
|
|
}
|
|
|
|
if (query != null || queryParameters != null) {
|
|
query = _Uri._makeQuery(
|
|
query, 0, _stringOrNullLength(query), queryParameters);
|
|
} else if (_queryStart < _fragmentStart) {
|
|
query = _uri.substring(_queryStart + 1, _fragmentStart);
|
|
}
|
|
|
|
if (fragment != null) {
|
|
fragment = _Uri._makeFragment(fragment, 0, fragment.length);
|
|
} else if (_fragmentStart < _uri.length) {
|
|
fragment = _uri.substring(_fragmentStart + 1);
|
|
}
|
|
|
|
return _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);
|
|
}
|
|
|
|
// Returns the index of the `/` after the package name of a package URI.
|
|
//
|
|
// Returns negative if the URI is not a valid package URI:
|
|
// * Scheme must be "package".
|
|
// * No authority.
|
|
// * Path starts with "something"/
|
|
// * where "something" is not all "." characters,
|
|
// * and contains no escapes or colons.
|
|
//
|
|
// The characters are necessarily valid path characters.
|
|
static int _packageNameEnd(_SimpleUri uri) {
|
|
if (uri._isPackage && !uri.hasAuthority) {
|
|
// Becomes Non zero if seeing any non-dot character.
|
|
// Also guards against empty package names.
|
|
return _skipPackageNameChars(uri._uri, uri._pathStart, uri._queryStart);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Merge two simple URIs. This should always result in a prefix of
|
|
// one concatenated with a suffix of the other, possibly with a `/` in
|
|
// the middle of two merged paths, 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 _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 _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 _SimpleUri(
|
|
newUri,
|
|
base._schemeEnd,
|
|
base._hostStart,
|
|
base._portStart,
|
|
base._pathStart,
|
|
base._queryStart,
|
|
ref._fragmentStart + delta,
|
|
base._schemeCache);
|
|
}
|
|
return base.removeFragment();
|
|
}
|
|
if (ref.hasAbsolutePath) {
|
|
int basePathStart = base._pathStart;
|
|
int packageNameEnd = _packageNameEnd(this);
|
|
if (packageNameEnd > 0) basePathStart = packageNameEnd;
|
|
var delta = basePathStart - ref._pathStart;
|
|
var newUri = base._uri.substring(0, basePathStart) +
|
|
ref._uri.substring(ref._pathStart);
|
|
return _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.
|
|
// Add a "/" in front, then leading "/../" segments are folded to "/".
|
|
int refStart = ref._pathStart;
|
|
while (ref._uri.startsWith("../", refStart)) {
|
|
refStart += 3;
|
|
}
|
|
var delta = base._pathStart - refStart + 1;
|
|
var newUri = "${base._uri.substring(0, base._pathStart)}/"
|
|
"${ref._uri.substring(refStart)}";
|
|
return _SimpleUri(
|
|
newUri,
|
|
base._schemeEnd,
|
|
base._hostStart,
|
|
base._portStart,
|
|
base._pathStart,
|
|
ref._queryStart + delta,
|
|
ref._fragmentStart + delta,
|
|
base._schemeCache);
|
|
}
|
|
// Merge paths.
|
|
|
|
// 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 packageNameEnd = _packageNameEnd(this);
|
|
if (packageNameEnd >= 0) {
|
|
baseStart = packageNameEnd; // At the `/` after the first package name.
|
|
} else {
|
|
while (baseUri.startsWith("../", baseStart)) baseStart += 3;
|
|
}
|
|
int refStart = ref._pathStart;
|
|
int refEnd = ref._queryStart;
|
|
|
|
/// Count of leading ".." segments in reference path.
|
|
/// The count is decremented when the segment is matched with a
|
|
/// segment of the base path, and both are then omitted from the result.
|
|
int backCount = 0;
|
|
|
|
/// Count "../" segments and advance `refStart` to after the segments.
|
|
while (refStart + 3 <= refEnd && refUri.startsWith("../", refStart)) {
|
|
refStart += 3;
|
|
backCount += 1;
|
|
}
|
|
|
|
// Extra slash inserted between base and reference path parts if
|
|
// the base path contains any slashes, or empty string if none.
|
|
// (We could use a slash from the base path in most cases, but not if
|
|
// we remove the entire base path).
|
|
String insert = "";
|
|
|
|
/// Remove segments from the base path.
|
|
/// Start with the segment trailing the last slash,
|
|
/// then remove segments for each leading "../" segment
|
|
/// from the reference path, or as many of them as are available.
|
|
while (baseEnd > baseStart) {
|
|
baseEnd--;
|
|
int char = baseUri.codeUnitAt(baseEnd);
|
|
if (char == _SLASH) {
|
|
insert = "/";
|
|
if (backCount == 0) break;
|
|
backCount--;
|
|
}
|
|
}
|
|
|
|
if (baseEnd == baseStart && !base.hasScheme && !base.hasAbsolutePath) {
|
|
// If the base is *just* a relative path (no scheme or authority),
|
|
// then merging with another relative path doesn't follow the
|
|
// RFC-3986 behavior.
|
|
// Don't need to check `base.hasAuthority` since the base path is
|
|
// non-empty - if there is an authority, a non-empty path is absolute.
|
|
|
|
// We reached the start of the base path, and want to stay relative,
|
|
// so don't insert a slash.
|
|
insert = "";
|
|
// If we reached the start of the base path with more "../" left over
|
|
// in the reference path, include those segments in the result.
|
|
refStart -= backCount * 3;
|
|
}
|
|
|
|
var delta = baseEnd - refStart + insert.length;
|
|
var newUri = "${base._uri.substring(0, baseEnd)}$insert"
|
|
"${ref._uri.substring(refStart)}";
|
|
|
|
return _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 UnsupportedError("Cannot extract a file path from a $scheme URI");
|
|
}
|
|
if (_queryStart < _uri.length) {
|
|
if (_queryStart < _fragmentStart) {
|
|
throw UnsupportedError(
|
|
"Cannot extract a file path from a URI with a query component");
|
|
}
|
|
throw UnsupportedError(
|
|
"Cannot extract a file path from a URI with a fragment component");
|
|
}
|
|
return (windows ?? _Uri._isWindows)
|
|
? _Uri._toWindowsFilePath(this)
|
|
: _toFilePath();
|
|
}
|
|
|
|
String _toFilePath() {
|
|
if (_hostStart < _portStart) {
|
|
// Has authority and non-empty host.
|
|
throw 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 => _hashCodeCache ??= _uri.hashCode;
|
|
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) return true;
|
|
return other is Uri && _uri == other.toString();
|
|
}
|
|
|
|
Uri _toNonSimple() {
|
|
return _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;
|
|
}
|
|
|
|
/// Special [_Uri] created from an existing [UriData].
|
|
class _DataUri extends _Uri {
|
|
final UriData _data;
|
|
|
|
_DataUri(this._data, String path, String? query)
|
|
: super._internal("data", "", null, null, path, query, null);
|
|
|
|
UriData? get data => _data;
|
|
}
|
|
|
|
/// Checks whether [text] starts with "data:" at position [start].
|
|
///
|
|
/// The text must be long enough to allow reading five characters
|
|
/// 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) {
|
|
// 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;
|
|
|
|
List<String> _toUnmodifiableStringList(String key, List<String> list) =>
|
|
List<String>.unmodifiable(list);
|
|
|
|
/// Counts valid package name characters in [source].
|
|
///
|
|
/// If [source] starts at [start] with a valid package name,
|
|
/// followed by a `/`, no later than [end],
|
|
/// then the position of the `/` is returned.
|
|
/// If not, a negative value is returned.
|
|
/// (Assumes source characters are valid path characters.)
|
|
/// A name only consisting of `.` characters is not a valid
|
|
/// package name.
|
|
int _skipPackageNameChars(String source, int start, int end) {
|
|
// Becomes non-zero when seeing a non-dot character.
|
|
// Also guards against empty package names.
|
|
var dots = 0;
|
|
for (var i = start; i < end; i++) {
|
|
var char = source.codeUnitAt(i);
|
|
if (char == _SLASH) return (dots != 0) ? i : -1;
|
|
if (char == _PERCENT || char == _COLON) return -1;
|
|
dots |= char ^ _DOT;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// Whether [string] at [start] starts with [prefix], ignoring case.
|
|
///
|
|
/// Returns whether [string] at offset [start]
|
|
/// starts with the characters of [prefix],
|
|
/// but ignores differences in the cases of ASCII letters,
|
|
/// so `a` and `A` are considered equal.
|
|
///
|
|
/// The [string] must be at least as long as [prefix].
|
|
///
|
|
/// When used to checks the schemes of URIs,
|
|
/// this function doesn't check that the characters are valid URI scheme
|
|
/// characters. The [string] is assumed to be a valid URI,
|
|
/// so if [prefix] matches it, it has to be valid too.
|
|
bool _caseInsensitiveStartsWith(String prefix, String string, int start) =>
|
|
_caseInsensitiveCompareStart(prefix, string, start) >= 0;
|
|
|
|
/// Compares [string] at [start] with [prefix], ignoring case.
|
|
///
|
|
/// Returns 0 if [string] starts with [prefix] at offset [start].
|
|
/// Returns 0x20 if [string] starts with [prefix] at offset [start],
|
|
/// but some ASCII letters have different case.
|
|
/// Returns a negative value if [string] does not start with [prefix],
|
|
/// at offset [start] even ignoring case differences.
|
|
///
|
|
/// The [string] must be at least as long as `start + prefix.length`.
|
|
int _caseInsensitiveCompareStart(String prefix, String string, int start) {
|
|
int result = 0;
|
|
for (int i = 0; i < prefix.length; i++) {
|
|
int prefixChar = prefix.codeUnitAt(i);
|
|
int stringChar = string.codeUnitAt(start + i);
|
|
int delta = prefixChar ^ stringChar;
|
|
if (delta != 0) {
|
|
if (delta == 0x20) {
|
|
// Might be a case difference.
|
|
int lowerChar = stringChar | delta;
|
|
if (0x61 /*a*/ <= lowerChar && lowerChar <= 0x7a /*z*/) {
|
|
result = 0x20;
|
|
continue;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Checks whether two strings are equal ignoring case differences.
|
|
///
|
|
/// Returns whether if [string1] and [string2] has the same length
|
|
/// and same characters, but ignores the cases of ASCII letters,
|
|
/// so `a` and `A` are considered equal.
|
|
bool _caseInsensitiveEquals(String string1, String string2) =>
|
|
string1.length == string2.length &&
|
|
_caseInsensitiveStartsWith(string1, string2, 0);
|