blob: 2f955dff65a7cd9863e3d341a1b1deb74fe7397a [file] [log] [blame]
// 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.
///
/// **See also:**
///
/// * [URIs][uris] in the [library tour][libtour]
/// * [RFC-3986](http://tools.ietf.org/html/rfc3986)
///
/// [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.
/// 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 a string, or an [Iterable] of strings,
/// where the latter corresponds to multiple values for the same 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.
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.
///
/// Examples:
///
/// ```dart
/// // http://example.org/path?q=dart.
/// Uri.http("example.org", "/path", { "q" : "dart" });
///
/// // http://user:pass@localhost:8080
/// Uri.http("user:pass@localhost:8080", "");
///
/// // http://example.org/a%20b
/// Uri.http("example.org", "a b");
///
/// // http://example.org/a%252F
/// Uri.http("example.org", "/a%2F");
/// ```
///
/// 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.
///
/// The `query` component is set from the optional [queryParameters]
/// argument.
factory Uri.http(String authority, String unencodedPath,
[Map<String, dynamic>? 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`.
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 from this rule is that when Windows semantics is used
/// and the path starts with a drive letter followed by a colon (":") and a
/// path separator, then an absolute URI is created.
///
/// The default for whether to use Windows or non-Windows semantics
/// determined from the platform Dart is running on. When running in
/// the standalone VM, this is detected by the VM based on the
/// operating system. When running in a browser non-Windows semantics
/// is always used.
///
/// To override the automatic detection of which semantics to use pass
/// a value for [windows]. Passing `true` will use Windows
/// semantics and passing `false` will use non-Windows semantics.
///
/// Examples using non-Windows semantics:
///
/// ```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`.
factory Uri.directory(String path, {bool? windows}) = _Uri.directory;
/// Creates a `data:` URI containing the [content] string.
///
/// Converts the content to a bytes using [encoding] or the charset specified
/// in [parameters] (defaulting to US-ASCII if not specified or unrecognized),
/// then encodes the bytes into the resulting data URI.
///
/// Defaults to encoding using percent-encoding (any non-ASCII or non-URI-valid
/// bytes is replaced by a percent encoding). If [base64] is true, the bytes
/// are instead encoded using [base64].
///
/// If [encoding] is not provided and [parameters] has a `charset` entry,
/// that name is looked up using [Encoding.getByName],
/// and if the lookup returns an encoding, that encoding is used to convert
/// [content] to bytes.
/// If providing both an [encoding] and a charset 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].
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.
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.
///
/// Th 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 have 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](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4").
///
/// Each key and value in the 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.
///
/// 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](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4").
///
/// Each key and value in the 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.
///
/// 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: http://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")); // Prints true.
/// ```
///
/// 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
/// 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
/// Uri uri1 = Uri.parse("a://b@c:4/d/e?f#g");
/// Uri uri2 = uri1.replace(scheme: "A", path: "D/E/E", fragment: "G");
/// print(uri2); // prints "a://b@c:4/D/E/E?f#G"
/// ```
/// This method acts similarly to using the `Uri` constructor with
/// some of the arguments taken from this `Uri`. Example:
/// ```dart
/// Uri uri3 = Uri(
/// scheme: "A",
/// userInfo: uri1.userInfo,
/// host: uri1.host,
/// port: uri1.port,
/// path: "D/E/E",
/// query: uri1.query,
/// fragment: "G");
/// print(uri3); // prints "a://b@c:4/D/E/E?f#G"
/// print(uri2 == uri3); // prints true.
/// ```
/// Using this method can be seen as a shorthand for the `Uri` constructor
/// call above, but may also be slightly faster because the parts taken
/// from this `Uri` need not be checked for validity again.
Uri replace(
{String? scheme,
String? userInfo,
String? host,
int? port,
String? path,
Iterable<String>? pathSegments,
String? query,
Map<String, dynamic /*String|Iterable<String>*/ >? queryParameters,
String? fragment});
/// 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.
Uri removeFragment();
/// Resolve [reference] as an URI relative to `this`.
///
/// First turn [reference] into a URI using [Uri.parse]. Then resolve the
/// resulting URI relative to `this`.
///
/// Returns the resolved URI.
///
/// See [resolveUri] for details.
Uri resolve(String reference);
/// Resolve [reference] as an URI relative to `this`.
///
/// Returns the resolved URI.
///
/// The algorithm "Transform Reference" for resolving a reference is described
/// in [RFC-3986 Section 5](http://tools.ietf.org/html/rfc3986#section-5 "RFC-1123").
///
/// Updated to handle the case where the base URI is just a relative path -
/// that is: when it has no scheme 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.
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 (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.
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 the which is
/// specified for the encodeUriComponent in ECMA-262 version 5.1.
///
/// When manually encoding path segments or query components remember
/// to encode each part separately before building the path or query
/// string.
///
/// For encoding the query part consider using
/// [encodeQueryComponent].
///
/// To avoid the need for explicitly encoding use the [pathSegments]
/// and [queryParameters] optional named arguments when constructing
/// a [Uri].
static String encodeComponent(String component) {
return _Uri._uriEncode(_Uri._unreserved2396Table, component, utf8, false);
}
/**
* 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 http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 for more
* details.
*/
static String encodeQueryComponent(String component,
{Encoding encoding = utf8}) {
return _Uri._uriEncode(_Uri._unreservedTable, component, encoding, true);
}
/// Decodes the percent-encoding in [encodedComponent].
///
/// Note that decoding a URI component might change its meaning as
/// some of the decoded characters could be characters with are
/// delimiters for a given URI 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.
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 in ECMA-262 version 5.1 for
/// the encodeURI function .
static String encodeFull(String uri) {
return _Uri._uriEncode(_Uri._encodeFullTable, uri, utf8, false);
}
/// Decodes the percent-encoding in [uri].
///
/// Note that decoding a full URI might change its meaning as some of
/// the decoded characters could be reserved characters. In most
/// cases an encoded URI should be parsed into components using
/// [Uri.parse] before decoding the separate components.
static String decodeFull(String uri) {
return _Uri._uriDecode(uri, 0, uri.length, utf8, false);
}
/// Splits the [query] into a map according to the rules
/// specified for FORM post in the [HTML 4.01 specification section
/// 17.13.4](http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4 "HTML 4.01 section 17.13.4").
///
/// Each key and value in the returned map has been decoded. If the [query]
/// is the empty string an empty map is returned.
///
/// Keys in the query string that have no value are mapped to the
/// empty string.
///
/// Each query component will be decoded using [encoding]. The default encoding
/// is UTF-8.
static Map<String, String> splitQueryString(String query,
{Encoding encoding = utf8}) {
return query.split("&").fold({}, (map, element) {
int index = element.indexOf("=");
if (index == -1) {
if (element != "") {
map[decodeQueryComponent(element, encoding: encoding)] = "";
}
} else if (index != 0) {
var key = element.substring(0, index);
var value = element.substring(index + 1);
map[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 ot the end of the string.
///
/// Some examples of IPv6 addresses:
/// * `::1`
/// * `FEDC:BA98:7654:3210:FEDC:BA98:7654:3210`
/// * `3ffe:2a00:100:7031::1`
/// * `::FFFF:129.144.52.38`
/// * `2010:836B:4179::836B:4179`
static List<int> parseIPv6Address(String host, [int start = 0, int? end]) {
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, [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');
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');
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');
}
} else if (parts.length != 8) {
error('an address without a wildcard must contain exactly 8 parts');
}
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 _compareScheme(scheme, thisScheme);
}
/// Compares scheme characters in [scheme] and at the start of [uri].
///
/// Returns `true` if [scheme] represents the same scheme as the start of
/// [uri]. That means having the same characters, but possibly different case
/// for letters.
///
/// This function doesn't check that the characters are valid URI scheme
/// characters. The [uri] is assumed to be valid, so if [scheme] matches
/// it, it has to be valid too.
///
/// The length should be tested before calling this function,
/// so the scheme part of [uri] is known to have the same length as [scheme].
static bool _compareScheme(String scheme, String uri) {
for (int i = 0; i < scheme.length; i++) {
int schemeChar = scheme.codeUnitAt(i);
int uriChar = uri.codeUnitAt(i);
int delta = schemeChar ^ uriChar;
if (delta != 0) {
if (delta == 0x20) {
// Might be a case difference.
int lowerChar = uriChar | delta;
if (0x61 /*a*/ <= lowerChar && lowerChar <= 0x7a /*z*/) {
continue;
}
}
return false;
}
}
return true;
}
/// 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);
}
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('/')) {
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 be 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}) {
return _normalize(component, start, end, charTable,
escapeDelimiters: escapeDelimiters) ??
component.substring(start, end);
}
/// Runs through component checking that each character is valid and
/// normalize 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 lower case letters, they are converted to
/// upper case.
///
/// Returns `null` if the original content was already normalized.
static String? _normalize(
String component, int start, int end, List<int> charTable,
{bool escapeDelimiters = 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 (!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 encodings 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