// 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 _DIGIT_0 = 0x30; 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_V = 0x76; 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 [Uri.new]: /// ```dart /// var httpsUri = Uri( /// scheme: 'https', /// host: 'dart.dev', /// path: 'language/built-in-types', /// fragment: 'numbers'); /// print(httpsUri); // https://dart.dev/language/built-in-types#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/libraries/dart-core#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); // libraries/dart-core /// print(uri.pathSegments); // [libraries, dart-core] /// print(uri.fragment); // utility-classes /// print(uri.hasQuery); // false /// print(uri.data); // null /// ``` /// /// **See also:** /// * [URIs][uris] in the [introduction to `dart:core`][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/libraries/dart-core#uris /// [libtour]: https://dart.dev/libraries/dart-core abstract interface 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: 'language/built-in-types', /// fragment: 'numbers'); /// print(httpsUri); // https://dart.dev/language/built-in-types#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? pathSegments, String? query, Map*/>? 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*/>? 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? 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: {'search': 'file', 'max': '10'}, /// ); /// print(uri); // data:;search=name;max=10,example%20content /// ``` factory Uri.dataFromString( String content, { String? mimeType, Encoding? encoding, Map? 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 bytes, { String mimeType = "application/octet-stream", Map? 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 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 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&limit=20&limit=30&max=100'); /// print(jsonEncode(uri.queryParametersAll)); // {"limit":["10","20","30"],"max":["100"]} /// ``` /// /// The map and the lists it contains are unmodifiable. Map> 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( /// 'https://dart.dev/libraries/dart-core#utility-classes'); /// /// final uri2 = uri1.replace( /// scheme: 'https', /// path: 'libraries/dart-core', /// fragment: 'uris'); /// print(uri2); // https://dart.dev/libraries/dart-core#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: 'language/built-in-types', /// query: uri1.query, /// fragment: null); /// print(uri3); // https://dart.dev/language/built-in-types /// ``` /// 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? pathSegments, String? query, Map*/>? 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 // // 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 / "/" / "?" ) // Pv6address = 6( h16 ":" ) ls32 // / "::" 5( h16 ":" ) ls32 // / [ h16 ] "::" 4( h16 ":" ) ls32 // / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 // / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 // / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 // / [ *4( h16 ":" ) h16 ] "::" ls32 // / [ *5( h16 ":" ) h16 ] "::" h16 // / [ *6( h16 ":" ) h16 ] "::" // ls32 = ( h16 ":" h16 ) / IPv4address // ; least-significant 32 bits of address // h16 = 1*4HEXDIG // ; 16 bits of address represented in hexadecimal 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.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/libraries/dart-core#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(_unreserved2396Mask, 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. * 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(_unreservedMask, 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(_encodeFullMask, 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 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). /// /// If [start] and [end] are provided, only that range, which must be a valid /// range of [host], is parsed. Defaults to all of [host]. /// /// Throws a [FormatException] if (the range of) [host] is not a valid /// IPv4 address representation, meaning something of the form /// `...` where each octet is a decimal /// numeral in the range 0..255 with no leading zeros. static List parseIPv4Address( String host, [ @Since('3.10') int start = 0, @Since('3.10') int? end, ]) { end = RangeError.checkValidRange(start, end, host.length); var list = Uint8List(4); _parseIPv4Address(host, start, end, list, 0); return list; } static Never _ipv4FormatError(String msg, String source, int position) { throw FormatException('Illegal IPv4 address, $msg', source, position); } /// Parses an IPv4 address of a substring into [result]. /// /// Returns when all characters from [start] to [end] have been parsed /// successfully. /// /// It's an error if [host] from [start] to [end] does not *start with* /// `...` where each octet is a sequence /// of decimal digits with no leading zeros (the only numeral that can /// start with zero is `0`) and no value above 255, which also limits /// it to three digits. /// It's also an error if such a dotted-quad of octets is followed by /// a `.`. If it's followed by a non-digit, non-`.` character, /// the parsing ends there, and that position is returned. /// The caller can decide whether they consider an end of parsing /// before [end] to be an error or not. /// /// The bytes are stored into four bytes of [target] /// starting at [targetOffset]. /// /// If an error is encountered, a [FormatException] is thrown, /// using [_ipv4FormatError]. static void _parseIPv4Address( String host, int start, int end, Uint8List target, int targetOffset, ) { assert(target.length >= targetOffset + 4); var cursor = start; var octetStart = cursor; var octetIndex = 0; var octetValue = 0; while (true) { // Any non-negative, non-digit, non-dot value works as sentinel. var char = cursor >= end ? 0 : host.codeUnitAt(cursor); var digit = char ^ _DIGIT_0; if (digit <= 9) { if (octetValue != 0 || cursor == octetStart) { octetValue = octetValue * 10 + digit; if (octetValue <= 255) { cursor++; continue; } _ipv4FormatError( "each part must be in the range 0..255", host, octetStart, ); } _ipv4FormatError("parts must not have leading zeros", host, octetStart); } if (cursor == octetStart) { // No digit where octet expected. if (cursor == end) break; // Report as missing part. _ipv4FormatError("invalid character", host, cursor); } target[targetOffset + octetIndex++] = octetValue; // Must have _DOT unless after fourth octet. if (char == _DOT) { if (octetIndex < 4) { octetValue = 0; octetStart = ++cursor; continue; } break; // Too many parts (fourth `.`). } // Ended an octet without following `.`. // Only valid at end with four octets. if (cursor == end) { if (octetIndex == 4) return; break; // Too few parts. } _ipv4FormatError("invalid character", host, cursor); } // More or less that 4 octets. // Loop is broken to here if the start-end slice is a prefix of a valid // IPv4 address, or it starts with a valid IPv4 address and // then has an extra `.` after it. _ipv4FormatError( 'IPv4 address should contain exactly 4 parts', host, cursor, ); } /// Checks if a (sub-)string is a valid IPv6 or IPvFuture address. /// /// See [parseIPv6Address] for IPv6 format. /// /// The format of IPvFuture is: /// * 'v' \+ '.' (\ | \ | ':')+ /// /// Since 'v' cannot start an IPv6 address, the input is either one /// or the other. /// No attempt is made to interpret an IPvFuture address, not even /// if the hex digit is `4` or `6`. /// /// Throws [FormatException] if the input is not a valid IPv6, /// IPv6+zone or IPvFuture address. /// /// Returns whether a valid input was an IPv6 address, /// rather than an IPvFuture address. static bool _validateIPvAddress(String host, int start, int end) { assert(0 <= start && start <= end && end <= host.length); if (start == end) throw FormatException("Empty IP address", host, start); var firstChar = host.codeUnitAt(start); if (firstChar == _LOWER_CASE_V) { var error = _validateIPvFutureAddress(host, start, end); if (error != null) throw error; return false; } // TODO: Have a validator that is not also parsing into bytes. parseIPv6Address(host, start, end); return true; } /// Validate that [start]..[end] of [host] is IPvFuture version and address. /// /// The [start] is at the leading 'v'. /// /// Returns a [FormatException] if input is not valid, and `null` if it /// is valid. The caller can then throw the exception. static FormatException? _validateIPvFutureAddress( String host, int start, int end, ) { assert(host.startsWith('v', start)); start++; var cursor = start; var char = 0; while (true) { if (cursor < end) { char = host.codeUnitAt(cursor++); // Continue if ASCII digit. if (char ^ _DIGIT_0 <= 9) continue; // Continue if a-f, A-F. var ucChar = char | 0x20; if (ucChar >= _LOWER_CASE_A && ucChar <= _LOWER_CASE_F) continue; if (char == _DOT) { // Done, check if any hex digits were seen. if (cursor - 1 == start) { return FormatException( "Missing hex-digit in IPvFuture address", host, cursor, ); } break; } return FormatException("Unexpected character", host, cursor - 1); } // Found non-`.` character after zero or more hex digits. if (cursor - 1 == start) { return FormatException( "Missing hex-digit in IPvFuture address", host, cursor, ); } return FormatException("Missing '.' in IPvFuture address", host, cursor); } if (cursor == end) { return FormatException( "Missing address in IPvFuture address, host, cursor", ); } while (true) { var char = host.codeUnitAt(cursor); if (_charTables.codeUnitAt(char) & _ipvFutureAddressCharsMask != 0) { if (++cursor < end) continue; return null; } return FormatException( "Invalid IPvFuture address character", host, cursor, ); } } /// 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` /// /// The grammar for IPv6 addresses are: /// ``` /// IPv6address ::= (h16 ":"){6} ls32 /// | "::" (h16 ":"){5} ls32 /// | ( h16) "::" (h16 ":"){4} ls32 /// | ((h16 ":"){0,1} h16) "::" (h16 ":"){3} ls32 /// | ((h16 ":"){0,2} h16) "::" (h16 ":"){2} ls32 /// | ((h16 ":"){0,3} h16) "::" h16 ":" ls32 /// | ((h16 ":"){0,4} h16) "::" ls32 /// | ((h16 ":"){0,5} h16) "::" h16 /// | ((h16 ":"){0,6} h16) "::" /// ls32 ::= (h16 ":" h16) | IPv4address /// ;; least-significant 32 bits of address /// h16 ::= HEXDIG{1,4} /// ;; 16 bits of address represented in hexadecimal /// ``` /// That is /// - eight 1-to-4-digit hexadecimal numerals separated by `:`, or /// - one to seven such `:`-separated numerals, with either one pair is /// separated by `::`, or a leading or trailing `::`. /// - either of the above with a trailing two `:`-separated numerals /// replaced by an IPv4 address. /// /// An IPv6 address with a zone ID (from RFC 6874) is an IPv6 address followed /// by `%25` (an escaped `%`) and valid zone characters. /// ``` /// IPv6addrz ::= IPv6address "%25" ZoneID /// ZoneID ::= (unreserved | pct-encoded)+ /// ```. static List parseIPv6Address(String host, [int start = 0, int? end]) { end ??= RangeError.checkValidRange(start, 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. Never error(String msg, int? position) { throw FormatException('Illegal IPv6 address, $msg', host, position); } if (end - start < 2) error('address is too short', null); Uint8List result = Uint8List(16); // Set if seeing a ".", suggesting that there is an IPv4 address. int partStart = start; int wildcardAt = -1; int partCount = 0; int hexValue = 0; bool decValue = true; int cursor = start; // Handle leading colons eagerly to make the loop simpler. // After this, any colon encountered will be after another colon or // a hex part. if (host.codeUnitAt(cursor) == _COLON) { if (host.codeUnitAt(cursor + 1) == _COLON) { wildcardAt = 0; partCount = 1; partStart = cursor += 2; } else { error('invalid start colon', cursor); } } // Parse all parts, except a potential last one. while (true) { var char = cursor >= end ? 0 : host.codeUnitAt(cursor); handleDigit: { int hexDigit; if (char ^ _DIGIT_0 case var digit when digit <= 9) { hexDigit = digit; } else if (char | 0x20 case var letter when letter >= _LOWER_CASE_A && letter <= _LOWER_CASE_F) { hexDigit = letter - (_LOWER_CASE_A - 10); decValue = false; } else { break handleDigit; // Not a digit. } if (cursor < partStart + 4) { hexValue = hexValue * 16 + hexDigit; cursor++; continue; } error('an IPv6 part can contain a maximum of 4 hex digits', partStart); } if (cursor > partStart) { // Non-empty part, value in hexValue. if (char == _DOT) { if (decValue) { if (partCount <= 6) { _parseIPv4Address(host, partStart, end, result, partCount * 2); cursor = end; // Throws if IPv4 does not extend to end. partCount += 2; break; // Must be last. } error('an address must contain at most 8 parts', partStart); } break; // Any other dot is just an invalid character. } // Non-empty hex part. result[partCount * 2] = hexValue >> 8; result[partCount * 2 + 1] = hexValue & 0xFF; partCount++; if (char == _COLON) { if (partCount < 8) { // Start new part. hexValue = 0; decValue = true; cursor++; partStart = cursor; continue; } error('an address must contain at most 8 parts', cursor); } break; // Unexpected character or the end sentinel. } // Empty part, after a colon. if (char == _COLON) { // Colon after empty part must be after colon. // (Leading colons were checked before loop.) if (wildcardAt < 0) { wildcardAt = partCount; partCount++; // A wildcard fills at least one part. partStart = ++cursor; continue; } error('only one wildcard `::` is allowed', cursor); } // Missing hex digits after colon. // OK to end right after wildcard, not after another colon. if (wildcardAt != partCount - 1) { // Missing part after a single `:`. // Error even at end. error('missing part', cursor); } break; // Error if not at end. } if (cursor < end) error('invalid character', cursor); if (partCount < 8) { if (wildcardAt < 0) { error( 'an address without a wildcard must contain exactly 8 parts', end, ); } // Move everything after the wildcard to the end of the buffer, and fill // the wildcard gap with zeros. var partAfterWildcard = wildcardAt + 1; int partsAfterWildcard = partCount - partAfterWildcard; if (partsAfterWildcard > 0) { var positionAfterWildcard = partAfterWildcard * 2; var bytesAfterWildcard = partsAfterWildcard * 2; var newPositionAfterWildcard = result.length - bytesAfterWildcard; result.setRange( newPositionAfterWildcard, result.length, result, positionAfterWildcard, ); result.fillRange(positionAfterWildcard, newPositionAfterWildcard, 0); } } return result; } } // Superclass of the two implementation types. sealed class _PlatformUri implements Uri {} final class _Uri implements _PlatformUri { // 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 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 queryParameters = UnmodifiableMapView(Uri.splitQueryString(this.query)); /// Cache the computed return value of [queryParametersAll]. late final Map> 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? pathSegments, String? query, Map*/>? 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? queryParameters, ]) { return _makeHttpUri("http", authority, unencodedPath, queryParameters); } /// Implementation of [Uri.https]. factory _Uri.https( String authority, [ String unencodedPath = '', Map? 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('[') && !host.startsWith('v', 1)) { 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? 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 or IPvFuture 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, ); } bool isIPv6 = Uri._validateIPvAddress( 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 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 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.value( path, "path", 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.value( path, "path", "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? pathSegments, String? query, Map*/>? 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 _computePathSegments(String pathToSplit) { if (pathToSplit.isNotEmpty && pathToSplit.codeUnitAt(0) == _SLASH) { pathToSplit = pathToSplit.substring(1); } return (pathToSplit.isEmpty) ? const [] : List.unmodifiable( pathToSplit.split("/").map(Uri.decodeComponent), ); } static Map> _computeQueryParametersAll(String? query) { if (query == null || query.isEmpty) return const >{}; Map> queryParameterLists = _splitQueryStringAll(query); queryParameterLists.updateAll(_toUnmodifiableStringList); return Map>.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 (with or without a zone) or an IPvFuture address. /// If [strictIPv6] is false, the address is also considered /// an IPv6 address if it contains any ':' character. /// /// If it is not an IPv6/IPvFUture 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 or IPvFuture address if it starts with '[', // or an IPv6 address if it 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 = end - 1; if (host.codeUnitAt(start + 1) != _LOWER_CASE_V) { 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"); } } bool isIPv6 = Uri._validateIPvAddress(host, start + 1, index); var hostChars = host.substring(start + 1, index); // RFC 5952 requires IPv6 hex digits to be lower case. if (isIPv6) hostChars = hostChars.toLowerCase(); return '[$hostChars$zoneID]'; } if (!strictIPv6) { // IPv6 addresses allowed without `[...]` brackets. // Used when called from `Uri` constructor. // 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 && (_charTables.codeUnitAt(char) & _zoneIDMask) != 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 && (_charTables.codeUnitAt(char) & _regNameMask) != 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 _regNameMask table 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, _userinfoMask); } static String _makePath( String? path, int start, int end, Iterable? 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(_pathCharMask, s, utf8, false)) .join("/"); } else if (pathSegments != null) { throw ArgumentError('Both path and pathSegments specified'); } else { result = _normalizeOrSubstring( path, start, end, _pathCharOrSlashMask, 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*/>? queryParameters, ) { if (query != null) { if (queryParameters != null) { throw ArgumentError('Both query and queryParameters specified'); } return _normalizeOrSubstring( query, start, end, _queryCharMask, escapeDelimiters: true, ); } if (queryParameters == null) return null; return _makeQueryFromParameters(queryParameters); } external static String _makeQueryFromParameters( Map*/> queryParameters, ); /// Default implementation of [_makeQueryFromParameters]. /// /// This implementation is used from the patch for [_makeQueryFromParameters] /// where there is not a more efficient native implementation available. static String _makeQueryFromParametersDefault( Map*/> queryParameters, ) { 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, _queryCharMask, 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 codeUnits; if (char <= 0x7f) { // 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 of [component] from [start] to [end]. static String _normalizeOrSubstring( String component, int start, int end, int charMask, { bool escapeDelimiters = false, bool replaceBackslash = false, }) { return _normalize( component, start, end, charMask, 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, int charMask, { 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 && (_charTables.codeUnitAt(char) & charMask) != 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 char) { return char < 128 && ((_charTables.codeUnitAt(char) & _schemeMask) != 0); } static bool _isGeneralDelimiter(int char) { return char <= _RIGHT_BRACKET && ((_charTables.codeUnitAt(char) & _genDelimitersMask) != 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 or _normalizeRelativePath 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 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. /// /// Returns a new path that behaves the same as the input [path] when /// used as a URI or URI Reference path. /// /// If [allowScheme] is `false` and the path starts with something that would /// parse as a scheme, valid scheme-characters followed by a colon, /// the colon is escaped to `%3A`. The `allowScheme` should be set to `true` /// when the result will become the path of a URI with no scheme or authority. /// /// The result will contain no non-leading `..` segments. There may be /// any number of leading `..` segments if the [path] contains more `..` /// segments than prior actual segments. For example `a/b/../../../../c` will /// normalize to `../../c`. /// /// The result will contain no non-leading `.` segment. /// It will only start with a `.` segment if necessary to preserve /// the behavior of the input [path]: /// * The result is the path `./` if the result would otherwise /// have been an empty path, and the input `path` is not empty, or /// * The result starts with `.//` to represent an otherwise leading /// empty segment without being an absolute path. /// /// The leading `.` segment /// is only used to prefix a path that could otherwise behave differently /// from the input [path]. An empty path means _no path_ in a URI, which /// behaves differently from any non-empty path in some cases, and /// URI paths cannot represent an empty leading path segment. /// /// The result will end with a `/` if the [path] does, or if the path /// ends with a `.` or `..` segment. static String _normalizeRelativePath(String path, bool allowScheme) { assert(!path.startsWith('/')); // Only get called with relative paths. if (!_mayContainDotSegments(path)) { if (!allowScheme) path = _escapeScheme(path); return path; } assert(path.isNotEmpty); // An empty path would not have dot segments. // Collects segments to be joined by `/` to produce the result. // Used as a stack where `..` can pop the last segment. List output = []; // Set to `true` after a `.` or `..` segment. // If the path ends after a `.` or `..` with no trailing `/`, // the result should have a trailing slash. // For example "a/b" normalizes to "a/b", and "a/b/." normalizes to "a/b/", // but the content of `output` will be the same in both cases. // If the input ends with `appendSlash` set to `true`, the result will // have an extra `/` added. bool appendSlash = false; // Split input into segments so `.` and `..` segments can be handled // and remaining segments combined into a normalized path. // The first segment will never be empty, since the path is relative // and non-empty, so the path start with a non-`/` character. // A final empty string does not semantically represent a path segment, // just that the path ends in `/`. // It's handled the same as any other empty segment. // There is no difference between an empty segment and "no segment" // unless followed by a `..` or `.`, which the final string isn't. for (String segment in path.split("/")) { if (".." == segment) { appendSlash = true; if (!output.isEmpty && output.last != "..") { output.removeLast(); } else { output.add(".."); // The result can have a number of _leading_ `..`s. } } else if ("." == segment) { appendSlash = true; } else { appendSlash = false; if (segment.isEmpty && output.isEmpty) { // Avoid a leading empty segment, which would become an absolute path. // Can occur for inputs like `.//etc` or `a/..//etc`. // // The returned path will be the `output` list joined with `/`s. // A leading empty segment in `output` would make the resulting join // start with `/` and be an absolute path, // which is not a correct normalization of a relative path. // Using `./` as "first segment" represents that empty segment as the // segment between the `./` and the following joining `/`. // // The `./` first segment combines correctly with any following // segments, whether `.`, `..` or empty/non-empty segments. // * A `.` leaves the empty segment and sets `appendSlash` to true. // * A `..` removes the `./` segment, effectively removing // the empty segment between the `./` and the joining `/` // and making the `output` empty. It sets `appendSlash` to true. // * Any other segment is just appended and preserves the empty // segment until an equal number of `..`s bring the `output` // back to just the leading `"./"`. // * If reaching the end of input with only the `./` in the `output` // then either: // * `appendSlash` is `true`, and the result becomes the path `.//`, // which contains exactly the empty segment. // * `appendSlash` is `false`, in which case the end of input // was right after the empty segment that added the `./`, // an empty segment that came from a trailing `/` in the input // `path`. In that case, the empty segment shouldn't count // as a real segment, which makes the result path be empty. // In that case the result should be `./` anyway. // // In all cases, just treating `./` as a normal segment gives // the desired behavior both when combined with later input segments // and in the returned result. // // (This case could also be handled by checking at the end whether // the first segment is empty, and if so, prepending the result with // `"./"`. Simply changing the content of a first empty segment at // this point is simple and cheap.) segment = './'; } output.add(segment); } } if (output.isEmpty) { // Can happen for, fx, `.` or `a/..`. // A URI Reference does not distinguish having an empty path from having // _no path_, and a URI Reference with no path behaves differently when // combined with another URI or URI reference than one with non-empty // path. To make this normalization not change the behavior, instead // return a minimal non-empty normalized path. return "./"; } assert(!output.first.isEmpty); // Would be `./` instead. if (appendSlash) output.add(""); if (!allowScheme) output[0] = _escapeScheme(output[0]); var result = output.join("/"); return result; } /// 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 || ((_charTables.codeUnitAt(char) & _schemeMask) == 0)) { break; } } } return path; } Uri resolve(String reference) { return resolveUri(Uri.parse(reference)); } // The index of the `/` after the package name of a package URI. // // Value is 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; // Position up to which values are known to already be normalized, // because the value is taken from this `_Uri` // If any part of the path is taken from a `reference` which is not // a platform URI, and therefore not known to be canonicalized to the // standard of platform URIs, the combined path counts as potentially // non-normalized. const int atStart = 0, // Nothing taken from this URI. afterScheme = 1, // Scheme comes from this URI. afterAuthority = 2, // Scheme and authority comes from this URI. afterPath = 3, // The path, and everything before, is from this URI. afterQuery = 4; // Everything except fragment is from this URI. int split = atStart; if (reference.scheme.isNotEmpty) { if (reference is _PlatformUri) return reference; 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) { if (reference is _PlatformUri) { return reference.replace(scheme: targetScheme); } targetUserInfo = reference.userInfo; targetHost = reference.host; targetPort = _makePort( reference.hasPort ? reference.port : null, targetScheme, ); targetPath = _removeDotSegments(reference.path); if (reference.hasQuery) targetQuery = reference.query; split = afterScheme; } else { targetUserInfo = this._userInfo; targetHost = this._host; targetPort = this._port; if (reference.hasEmptyPath) { targetPath = this.path; if (reference.hasQuery) { split = afterPath; targetQuery = reference.query; } else { targetQuery = this._query; split = afterQuery; } } else { String basePath = this.path; int packageNameEnd = _packageNameEnd(this, basePath); split = afterAuthority; 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; if (reference is! _PlatformUri) { // Don't trust values coming from `reference` to be normalized. if (split == atStart) { targetScheme = _makeScheme(targetScheme, 0, targetScheme.length); } if (split <= afterScheme) { if (targetUserInfo.isNotEmpty) { targetUserInfo = _makeUserInfo( targetUserInfo, 0, targetUserInfo.length, ); } if (targetPort != null) { targetPort = _makePort(targetPort, targetScheme); } if (targetHost != null && targetHost.isNotEmpty) { targetHost = _makeHost(targetHost, 0, targetHost.length, false); } } if (split <= afterPath) { targetPath = _makePath( targetPath, 0, targetPath.length, null, targetScheme, targetHost != null, ); } if (split <= afterPath && targetQuery != null) { targetQuery = _makeQuery(targetQuery, 0, targetQuery.length, null); } if (fragment != null) { fragment = _makeFragment(fragment, 0, fragment.length); } } 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 _createList() => []; static Map> _splitQueryStringAll( String query, { Encoding encoding = utf8, }) { var result = >{}; 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( int canonicalMask, 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 bytes; if (simple) { if (utf8 == encoding || latin1 == encoding || ascii == encoding) { return text.substring(start, end); } else { bytes = text.substring(start, end).codeUnits; } } else { bytes = []; 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 && ((_charTables.codeUnitAt(char) & _unreservedMask) != 0); } } // -------------------------------------------------------------------- // 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. final 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 _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? parameters, bool base64 = false, }) { StringBuffer buffer = StringBuffer(); List 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(_uricMask, 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 bytes, { String mimeType = "application/octet-stream", Map? parameters, bool percentEncoded = false, }) { StringBuffer buffer = StringBuffer(); List indices = [_noScheme]; _writeUri(mimeType, null, parameters, buffer, indices); indices.add(buffer.length); if (percentEncoded) { buffer.write(','); _uriEncodeBytes(_uricMask, 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? parameters, StringBuffer buffer, List? 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( _tokenCharMask, mimeType.substring(0, slashIndex), utf8, false, ), ); buffer.write("/"); buffer.write( _Uri._uriEncode( _tokenCharMask, mimeType.substring(slashIndex + 1), utf8, false, ), ); } if (charsetName != null) { indices ?..add(buffer.length) ..add(buffer.length + 8); buffer.write(";charset="); buffer.write(_Uri._uriEncode(_tokenCharMask, 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(_tokenCharMask, key, utf8, false)); indices?.add(buffer.length); buffer.write('='); buffer.write(_Uri._uriEncode(_tokenCharMask, 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, _queryCharMask, ); end = queryIndex; } path = _Uri._normalizeOrSubstring( _text, colonIndex + 1, end, _pathCharOrSlashMask, ); 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 get parameters { var result = {}; 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 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, _uricMask, 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( int canonicalMask, List 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 && ((_charTables.codeUnitAt(byte) & canonicalMask) != 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; } // --- URI PARSER TABLE --- start --- generated code, do not edit --- // Use tools/generate_uri_parser_tables.dart to generate this code // if necessary. // -------------------------------------------------------------------- // 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 = 0; /// 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 95 characters in the /// interesting range, and one entry for all values outside the range. /// The character entries are stored in one `String` of 96 characters 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 0x1f (`0x7f ^ 0x60`), which /// represents the transition for all remaining characters. const String _scannerTables = "\xE1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01" "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xE1\xE1\xE1" "\x01\xE1\xE1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01" "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xE1\xE3\xE1\xE1\x01\xE1\x01" "\xE1\xCD\x01\xE1\x01\x01\x01\x01\x01\x01\x01\x01\x0E\x03\x01\x01\x01\x01" "\x01\x01\x01\x01\x01\x01\x22\x01\xE1\x01\xE1\xAC\xE1\x01\x01\x01\x01\x01" "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01" "\x01\x01\x01\xE1\xE1\xE1\x01\xE1\xE1\x01\x01\x01\x01\x01\x01\x01\x01\x01" "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xE1" "\xEA\xE1\xE1\x01\xE1\x01\xE1\xCD\x01\xE1\x01\x01\x01\x01\x01\x01\x01\x01" "\x01\x0A\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x22\x01\xE1\x01\xE1\xAC" "\xEB\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B" "\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\xEB\xEB\xEB\x8B\xEB\xEB\x8B\x8B\x8B" "\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B" "\x8B\x8B\x8B\x8B\x8B\xEB\x83\xEB\xEB\x8B\xEB\x8B\xEB\xCD\x8B\xEB\x8B\x8B" "\x8B\x8B\x8B\x8B\x8B\x8B\x92\x83\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B\x8B" "\xEB\x8B\xEB\x8B\xEB\xAC\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB\xEB\xEB" "\x0B\xEB\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB\x44\xEB\xEB\x0B\xEB\x0B" "\xEB\xCD\x0B\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x12\x44\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\xEB\x0B\xEB\x0B\xEB\xAC\xE5\x05\x05\x05\x05\x05" "\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05" "\x05\x05\x05\xE5\xE5\xE5\x05\xE5\x44\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5" "\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE8" "\x8A\xE5\xE5\x05\xE5\x05\xE5\xCD\x05\xE5\x05\x05\x05\x05\x05\x05\x05\x05" "\x05\x8A\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x66\x05\xE5\x05\xE5\xAC" "\xE5\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05" "\x05\x05\x05\x05\x05\x05\x05\x05\x05\xE5\xE5\xE5\x05\xE5\x44\xE5\xE5\xE5" "\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5\xE5" "\xE5\xE5\xE5\xE5\xE5\xE5\x8A\xE5\xE5\x05\xE5\x05\xE5\xCD\x05\xE5\x05\x05" "\x05\x05\x05\x05\x05\x05\x05\x8A\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05" "\x66\x05\xE5\x05\xE5\xAC\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7" "\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7" "\xE7\xE7\x44\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7" "\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\x8A\xE7\xE7\xE7\xE7\xE7" "\xE7\xCD\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\x8A\xE7\x07\x07\x07" "\x07\x07\x07\x07\x07\x07\xE7\xE7\xE7\xE7\xE7\xAC\xE7\xE7\xE7\xE7\xE7\xE7" "\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7" "\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\x44\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7" "\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7" "\x8A\xE7\xE7\xE7\xE7\xE7\xE7\xCD\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7\xE7" "\xE7\x8A\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\xE7\xE7\xE7\xE7\xE7\xAC" "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" "\x08\x08\x08\x08\x08\x08\x08\x05\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" "\x08\x08\x08\x08\x08\x08\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB\xEB\xEB" "\x0B\xEB\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB\xEA\xEB\xEB\x0B\xEB\x0B" "\xEB\xCD\x0B\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x10\xEA\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\xEB\x0B\xEB\x0B\xEB\xAC\xEB\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\xEB\xEB\xEB\x0B\xEB\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB" "\xEA\xEB\xEB\x0B\xEB\x0B\xEB\xCD\x0B\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x12\x0A\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB\x0B\xEB\x0B\xEB\xAC" "\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB\xEB\xEB\x0B\xEB\xEB\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\xEB\xEA\xEB\xEB\x0B\xEB\x0B\xEB\xCD\x0B\xEB\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0A\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\xEB\x0B\xEB\x0B\xEB\xAC\xEC\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C" "\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\xEC\xEC\xEC" "\x0C\xEC\xEC\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C" "\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\xEC\xEC\xEC\xEC\x0C\xEC\x0C" "\xEC\xCD\x0C\xEC\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\x0C\xEC\x0C\x0C\x0C\x0C" "\x0C\x0C\x0C\x0C\x0C\x0C\xEC\x0C\xEC\x0C\xEC\x0C\xED\x0D\x0D\x0D\x0D\x0D" "\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D" "\x0D\x0D\x0D\xED\xED\xED\x0D\xED\xED\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D" "\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\xED" "\xED\xED\xED\x0D\xED\x0D\xED\xED\x0D\xED\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D" "\x0D\xED\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\x0D\xED\x0D\xED\x0D\xED\x0D" "\xE1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01" "\x01\x01\x01\x01\x01\x01\x01\x01\x01\xE1\xE1\xE1\x01\xE1\xE1\x01\x01\x01" "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01" "\x01\x01\x01\x01\x01\xE1\xEA\xE1\xE1\x01\xE1\x01\xE1\xCD\x01\xE1\x01\x01" "\x01\x01\x01\x01\x01\x01\x0F\xEA\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01" "\x22\x01\xE1\x01\xE1\xAC\xE1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01" "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xE1\xE1\xE1" "\x01\xE1\xE1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01" "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\xE1\xE9\xE1\xE1\x01\xE1\x01" "\xE1\xCD\x01\xE1\x01\x01\x01\x01\x01\x01\x01\x01\x01\x09\x01\x01\x01\x01" "\x01\x01\x01\x01\x01\x01\x22\x01\xE1\x01\xE1\xAC\xEB\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\xEB\xEB\xEB\x0B\xEB\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB" "\xEA\xEB\xEB\x0B\xEB\x0B\xEB\xCD\x0B\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x11\xEA\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB\x0B\xEB\x0B\xEB\xAC" "\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB\xEB\xEB\x0B\xEB\xEB\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\xEB\xE9\xEB\xEB\x0B\xEB\x0B\xEB\xCD\x0B\xEB\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x09\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\xEB\x0B\xEB\x0B\xEB\xAC\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB\xEB\xEB" "\x0B\xEB\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB\xEA\xEB\xEB\x0B\xEB\x0B" "\xEB\xCD\x0B\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x13\xEA\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\xEB\x0B\xEB\x0B\xEB\xAC\xEB\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\xEB\xEB\xEB\x0B\xEB\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB" "\xEA\xEB\xEB\x0B\xEB\x0B\xEB\xCD\x0B\xEB\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B" "\x0B\xEA\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\xEB\x0B\xEB\x0B\xEB\xAC" "\xF5\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15" "\x15\x15\x15\x15\x15\x15\x15\x15\x15\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5" "\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5" "\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5" "\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5" "\xF5\xF5\xF5\xF5\xF5\xF5\xF5\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15" "\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\x15\xF5\xF5\xF5" "\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5" "\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5" "\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\xF5\x15\xF5\x15\x15\xF5\x15\x15\x15\x15" "\x15\x15\x15\x15\x15\x15\xF5\xF5\xF5\xF5\xF5\xF5"; // -------------------------------------------------------------------- /// 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. If that state is greater than or equal to /// [_nonSimpleEndStates], the general URI scan should consider the /// result non-simple, even if no position has been written to /// [_notSimpleIndex] of [indices]. int _scan(String uri, int start, int end, int state, List indices) { // Number of characters in table for each state (range 0x20..0x60). const int stateTableSize = 0x60; // Value to xor input character with to make valid range start at zero. const int _charXor = 96; // Limit on valid values after doing xor. const int _xorCharLimit = 95; // Entry used for invalid input characters (not in the range 0x20-0x7f). const int _invalidChar = 0x7F ^ _charXor; // Shift to extract write position from transition table entry. const int _writeIndexShift = 5; // Mask for state part of transition table entry. const int _stateMask = 31; assert(end <= uri.length); for (int i = start; i < end; i++) { int char = uri.codeUnitAt(i) ^ _charXor; if (char > _xorCharLimit) char = _invalidChar; int transition = _scannerTables.codeUnitAt(state * stateTableSize + char); state = transition & _stateMask; indices[transition >> _writeIndexShift] = i; } return state; } // --- URI PARSER TABLE --- end --- generated code, do not edit --- final class _SimpleUri implements _PlatformUri { 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 get pathSegments { int start = _pathStart; int end = _queryStart; if (_uri.startsWith("/", start)) start++; if (start == end) return const []; List 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.unmodifiable(parts); } Map get queryParameters { if (!hasQuery) return const {}; return UnmodifiableMapView(Uri.splitQueryString(query)); } Map> get queryParametersAll { if (!hasQuery) return const >{}; Map> queryParameterLists = _Uri._splitQueryStringAll( query, ); queryParameterLists.updateAll(_toUnmodifiableStringList); return Map>.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? pathSegments, String? query, Map*/>? 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 _toUnmodifiableStringList(String key, List list) => List.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); // --- URI CHARSET TABLE --- start --- generated code, do not edit --- // Use tools/generate_uri_parser_tables.dart to generate this code // if necessary. // The unreserved characters of RFC 3986. // [A-Za-z0-9\-._~] const _unreservedMask = 0x0001; // The unreserved characters of RFC 2396. // [A-Za-z0-9!'()*\-._~] const _unreserved2396Mask = 0x0002; // Table of reserved characters specified by ECMAScript 5. // [A-Za-z0-9!#$&'()*+,\-./:;=?_~] const _encodeFullMask = 0x0004; // Characters allowed in the scheme. // [A-Za-z0-9+\-.] const _schemeMask = 0x0008; // Characters allowed in the userinfo as of RFC 3986. // RFC 3986 Appendix A // userinfo = *( unreserved / pct-encoded / sub-delims / ':') // [A-Za-z0-9!$&'()*+,\-.:;=_~] (without '%') const _userinfoMask = 0x0010; // Characters allowed in the reg-name as of RFC 3986. // RFC 3986 Appendix A // reg-name = *( unreserved / pct-encoded / sub-delims ) // Same as `_userinfoMask` without the `:`. // // [A-Za-z0-9!$%&'()*+,\-.;=_~] (including '%') const _regNameMask = 0x0020; // Characters allowed in the path as of RFC 3986. // RFC 3986 section 3.3. // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" // [A-Za-z0-9!$&'()*+,\-.:;=@_~] (without '%') const _pathCharMask = 0x0040; // Characters allowed in the path as of RFC 3986. // RFC 3986 section 3.3 *and* slash. // [A-Za-z0-9!$&'()*+,\-./:;=@_~] (without '%') const _pathCharOrSlashMask = 0x0080; // Characters allowed in the query as of RFC 3986. // RFC 3986 section 3.4. // query = *( pchar / "/" / "?" ) // [A-Za-z0-9!$&'()*+,\-./:;=?@_~] (without '%') const _queryCharMask = 0x0100; // Characters allowed in the ZoneID as of RFC 6874. // ZoneID = 1*( unreserved / pct-encoded ) // [A-Za-z0-9\-._~] + '%' const _zoneIDMask = _unreservedMask; // Table of the `token` characters of RFC 2045 in a `data:` 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. const _tokenCharMask = 0x0200; // All non-escape RFC-2396 "uric" characters. // // The "uric" character set is defined by: // ``` // uric = reserved | unreserved | escaped // reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | "$" | "," // unreserved = alphanum | mark // mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")" // ``` // This is the same characters as in a URI query (which is URI pchar plus '?') const _uricMask = _queryCharMask; // General delimiter characters, RFC 3986 section 2.2. // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" // [:/?#[]@] const _genDelimitersMask = 0x0400; // Characters valid in an IPvFuture address, RFC 3986 section 3.2.2. // 1*( unreserved / sub-delims / ":" ) // [A-Za-z0-9\-._~]|[!$&'()*+,;=]|: const _ipvFutureAddressCharsMask = _userinfoMask; const String _charTables = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" "\x00\x00\x00\u03f6\x00\u0404\u03f4\x20\u03f4\u03f6\u01f6\u01f6\u03f6\u03fc" "\u01f4\u03ff\u03ff\u0584\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff" "\u03ff\u03ff\u05d4\u01f4\x00\u01f4\x00\u0504\u05c4\u03ff\u03ff\u03ff\u03ff" "\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff" "\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u0400\x00" "\u0400\u0200\u03f7\u0200\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff" "\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff" "\u03ff\u03ff\u03ff\u03ff\u03ff\u03ff\u0200\u0200\u0200\u03f7\x00"; // --- URI CHARSET TABLE --- end --- generated code, do not edit ---