// 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<String>? pathSegments,
    String? query,
    Map<String, dynamic /*String?|Iterable<String>*/>? queryParameters,
    String? fragment,
  }) = _Uri;

  /// Creates a new `http` URI from authority, path and query.
  ///
  /// Example:
  /// ```dart
  /// var uri = Uri.http('example.org', '/path', { 'q' : 'dart' });
  /// print(uri); // http://example.org/path?q=dart
  ///
  /// uri = Uri.http('user:password@localhost:8080', '');
  /// print(uri); // http://user:password@localhost:8080
  ///
  /// uri = Uri.http('example.org', 'a b');
  /// print(uri); // http://example.org/a%20b
  ///
  /// uri = Uri.http('example.org', '/a%2F');
  /// print(uri); // http://example.org/a%252F
  /// ```
  ///
  /// The `scheme` is always set to `http`.
  ///
  /// The `userInfo`, `host` and `port` components are set from the
  /// [authority] argument. If `authority` is `null` or empty,
  /// the created `Uri` has no authority, and isn't directly usable
  /// as an HTTP URL, which must have a non-empty host.
  ///
  /// The `path` component is set from the [unencodedPath]
  /// argument. The path passed must not be encoded as this constructor
  /// encodes the path. Only `/` is recognized as path separator.
  /// If omitted, the path defaults to being empty.
  ///
  /// The `query` component is set from the optional [queryParameters]
  /// argument.
  factory Uri.http(
    String authority, [
    String unencodedPath,
    Map<String, dynamic /*String?|Iterable<String>*/>? queryParameters,
  ]) = _Uri.http;

  /// Creates a new `https` URI from authority, path and query.
  ///
  /// This constructor is the same as [Uri.http] except for the scheme
  /// which is set to `https`.
  ///
  /// Example:
  /// ```dart
  /// var uri = Uri.https('example.org', '/path', {'q': 'dart'});
  /// print(uri); // https://example.org/path?q=dart
  ///
  /// uri = Uri.https('user:password@localhost:8080', '');
  /// print(uri); // https://user:password@localhost:8080
  ///
  /// uri = Uri.https('example.org', 'a b');
  /// print(uri); // https://example.org/a%20b
  ///
  /// uri = Uri.https('example.org', '/a%2F');
  /// print(uri); // https://example.org/a%252F
  /// ```
  factory Uri.https(
    String authority, [
    String unencodedPath,
    Map<String, dynamic>? queryParameters,
  ]) = _Uri.https;

  /// Creates a new file URI from an absolute or relative file path.
  ///
  /// The file path is passed in [path].
  ///
  /// This path is interpreted using either Windows or non-Windows
  /// semantics.
  ///
  /// With non-Windows semantics, the slash (`/`) is used to separate
  /// path segments in the input [path].
  ///
  /// With Windows semantics, backslash (`\`) and forward-slash (`/`)
  /// are used to separate path segments in the input [path],
  /// except if the path starts with `\\?\` in which case
  /// only backslash (`\`) separates path segments in [path].
  ///
  /// If the path starts with a path separator, an absolute URI (with the
  /// `file` scheme and an empty authority) is created.
  /// Otherwise a relative URI reference with no scheme or authority is created.
  /// One exception to this rule is that when Windows semantics is used
  /// and the path starts with a drive letter followed by a colon (":") and a
  /// path separator, then an absolute URI is created.
  ///
  /// The default for whether to use Windows or non-Windows semantics
  /// is determined from the platform Dart is running on. When running in
  /// the standalone VM, this is detected by the VM based on the
  /// operating system. When running in a browser, non-Windows semantics
  /// is always used.
  ///
  /// To override the automatic detection of which semantics to use pass
  /// a value for [windows]. Passing `true` will use Windows
  /// semantics and passing `false` will use non-Windows semantics.
  ///
  /// Examples using non-Windows semantics:
  /// ```dart
  /// // xxx/yyy
  /// Uri.file('xxx/yyy', windows: false);
  ///
  /// // xxx/yyy/
  /// Uri.file('xxx/yyy/', windows: false);
  ///
  /// // file:///xxx/yyy
  /// Uri.file('/xxx/yyy', windows: false);
  ///
  /// // file:///xxx/yyy/
  /// Uri.file('/xxx/yyy/', windows: false);
  ///
  /// // C%3A
  /// Uri.file('C:', windows: false);
  /// ```
  ///
  /// Examples using Windows semantics:
  /// ```dart
  /// // xxx/yyy
  /// Uri.file(r'xxx\yyy', windows: true);
  ///
  /// // xxx/yyy/
  /// Uri.file(r'xxx\yyy\', windows: true);
  ///
  /// file:///xxx/yyy
  /// Uri.file(r'\xxx\yyy', windows: true);
  ///
  /// file:///xxx/yyy/
  /// Uri.file(r'\xxx\yyy/', windows: true);
  ///
  /// // file:///C:/xxx/yyy
  /// Uri.file(r'C:\xxx\yyy', windows: true);
  ///
  /// // This throws an error. A path with a drive letter, but no following
  /// // path, is not allowed.
  /// Uri.file(r'C:', windows: true);
  ///
  /// // This throws an error. A path with a drive letter is not absolute.
  /// Uri.file(r'C:xxx\yyy', windows: true);
  ///
  /// // file://server/share/file
  /// Uri.file(r'\\server\share\file', windows: true);
  /// ```
  ///
  /// If the path passed is not a valid file path, an error is thrown.
  factory Uri.file(String path, {bool? windows}) = _Uri.file;

  /// Like [Uri.file] except that a non-empty URI path ends in a slash.
  ///
  /// If [path] is not empty, and it doesn't end in a directory separator,
  /// then a slash is added to the returned URI's path.
  /// In all other cases, the result is the same as returned by `Uri.file`.
  ///
  /// Example:
  /// ```dart
  /// final fileDirectory = Uri.directory('data/images', windows: false);
  /// print(fileDirectory); // data/images/
  ///
  /// final fileDirectoryWindows =
  ///    Uri.directory(r'C:\data\images', windows: true);
  /// print(fileDirectoryWindows); // file:///C:/data/images/
  /// ```
  factory Uri.directory(String path, {bool? windows}) = _Uri.directory;

  /// Creates a `data:` URI containing the [content] string.
  ///
  /// Converts the content to bytes using [encoding] or the charset specified
  /// in [parameters] (defaulting to US-ASCII if not specified or unrecognized),
  /// then encodes the bytes into the resulting data URI.
  ///
  /// Defaults to encoding using percent-encoding (any non-ASCII or
  /// non-URI-valid bytes is replaced by a percent encoding). If [base64] is
  /// true, the bytes are instead encoded using [base64].
  ///
  /// If [encoding] is not provided and [parameters] has a `charset` entry,
  /// that name is looked up using [Encoding.getByName],
  /// and if the lookup returns an encoding, that encoding is used to convert
  /// [content] to bytes.
  /// If providing both an [encoding] and a charset in [parameters], they should
  /// agree, otherwise decoding won't be able to use the charset parameter
  /// to determine the encoding.
  ///
  /// If [mimeType] and/or [parameters] are supplied, they are added to the
  /// created URI. If any of these contain characters that are not allowed
  /// in the data URI, the character is percent-escaped. If the character is
  /// non-ASCII, it is first UTF-8 encoded and then the bytes are percent
  /// encoded. An omitted [mimeType] in a data URI means `text/plain`, just
  /// as an omitted `charset` parameter defaults to meaning `US-ASCII`.
  ///
  /// To read the content back, use [UriData.contentAsString].
  ///
  /// Example:
  /// ```dart
  /// final uri = Uri.dataFromString(
  ///   'example content',
  ///   mimeType: 'text/plain',
  ///   parameters: <String, String>{'search': 'file', 'max': '10'},
  /// );
  /// print(uri); // data:;search=name;max=10,example%20content
  /// ```
  factory Uri.dataFromString(
    String content, {
    String? mimeType,
    Encoding? encoding,
    Map<String, String>? parameters,
    bool base64 = false,
  }) {
    UriData data = UriData.fromString(
      content,
      mimeType: mimeType,
      encoding: encoding,
      parameters: parameters,
      base64: base64,
    );
    return data.uri;
  }

  /// Creates a `data:` URI containing an encoding of [bytes].
  ///
  /// Defaults to Base64 encoding the bytes, but if [percentEncoded]
  /// is `true`, the bytes will instead be percent encoded (any non-ASCII
  /// or non-valid-ASCII-character byte is replaced by a percent encoding).
  ///
  /// To read the bytes back, use [UriData.contentAsBytes].
  ///
  /// It defaults to having the mime-type `application/octet-stream`.
  /// The [mimeType] and [parameters] are added to the created URI.
  /// If any of these contain characters that are not allowed
  /// in the data URI, the character is percent-escaped. If the character is
  /// non-ASCII, it is first UTF-8 encoded and then the bytes are percent
  /// encoded.
  ///
  /// Example:
  /// ```dart
  /// final uri = Uri.dataFromBytes([68, 97, 114, 116]);
  /// print(uri); // data:application/octet-stream;base64,RGFydA==
  /// ```
  factory Uri.dataFromBytes(
    List<int> bytes, {
    String mimeType = "application/octet-stream",
    Map<String, String>? parameters,
    bool percentEncoded = false,
  }) {
    UriData data = UriData.fromBytes(
      bytes,
      mimeType: mimeType,
      parameters: parameters,
      percentEncoded: percentEncoded,
    );
    return data.uri;
  }

  /// The scheme component of the URI.
  ///
  /// The value is the empty string if there is no scheme component.
  ///
  /// A URI scheme is case insensitive.
  /// The returned scheme is canonicalized to lowercase letters.
  String get scheme;

  /// The authority component.
  ///
  /// The authority is formatted from the [userInfo], [host] and [port]
  /// parts.
  ///
  /// The value is the empty string if there is no authority component.
  String get authority;

  /// The user info part of the authority component.
  ///
  /// The value is the empty string if there is no user info in the
  /// authority component.
  String get userInfo;

  /// The host part of the authority component.
  ///
  /// The value is the empty string if there is no authority component and
  /// hence no host.
  ///
  /// If the host is an IP version 6 address, the surrounding `[` and `]` is
  /// removed.
  ///
  /// The host string is case-insensitive.
  /// The returned host name is canonicalized to lower-case
  /// with upper-case percent-escapes.
  String get host;

  /// The port part of the authority component.
  ///
  /// The value is the default port if there is no port number in the authority
  /// component. That's 80 for http, 443 for https, and 0 for everything else.
  int get port;

  /// The path component.
  ///
  /// The path is the actual substring of the URI representing the path,
  /// and it is encoded where necessary. To get direct access to the decoded
  /// path, use [pathSegments].
  ///
  /// The path value is the empty string if there is no path component.
  String get path;

  /// The query component.
  ///
  /// The value is the actual substring of the URI representing the query part,
  /// and it is encoded where necessary.
  /// To get direct access to the decoded query, use [queryParameters].
  ///
  /// The value is the empty string if there is no query component.
  String get query;

  /// The fragment identifier component.
  ///
  /// The value is the empty string if there is no fragment identifier
  /// component.
  String get fragment;

  /// The URI path split into its segments.
  ///
  /// Each of the segments in the list has been decoded.
  /// If the path is empty, the empty list will
  /// be returned. A leading slash `/` does not affect the segments returned.
  ///
  /// The list is unmodifiable and will throw [UnsupportedError] on any
  /// calls that would mutate it.
  List<String> get pathSegments;

  /// The URI query split into a map according to the rules
  /// specified for FORM post in the [HTML 4.01 specification section
  /// 17.13.4](https://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4
  /// "HTML 4.01 section 17.13.4").
  ///
  /// Each key and value in the resulting map has been decoded.
  /// If there is no query, the empty map is returned.
  ///
  /// Keys in the query string that have no value are mapped to the
  /// empty string.
  /// If a key occurs more than once in the query string, it is mapped to
  /// an arbitrary choice of possible value.
  /// The [queryParametersAll] getter can provide a map
  /// that maps keys to all of their values.
  ///
  /// Example:
  /// ```dart import:convert
  /// final uri =
  ///     Uri.parse('https://example.com/api/fetch?limit=10,20,30&max=100');
  /// print(jsonEncode(uri.queryParameters));
  /// // {"limit":"10,20,30","max":"100"}
  /// ```
  ///
  /// The map is unmodifiable.
  Map<String, String> get queryParameters;

  /// Returns the URI query split into a map according to the rules
  /// specified for FORM post in the [HTML 4.01 specification section
  /// 17.13.4](https://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4
  /// "HTML 4.01 section 17.13.4").
  ///
  /// Each key and value in the resulting map has been decoded. If there is no
  /// query, the map is empty.
  ///
  /// Keys are mapped to lists of their values. If a key occurs only once,
  /// its value is a singleton list. If a key occurs with no value, the
  /// empty string is used as the value for that occurrence.
  ///
  /// Example:
  /// ```dart import:convert
  /// final uri =
  ///     Uri.parse('https://example.com/api/fetch?limit=10&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<String, List<String>> get queryParametersAll;

  /// Whether the URI is absolute.
  ///
  /// A URI is an absolute URI in the sense of RFC 3986 if it has a scheme
  /// and no fragment.
  bool get isAbsolute;

  /// Whether the URI has a [scheme] component.
  bool get hasScheme => scheme.isNotEmpty;

  /// Whether the URI has an [authority] component.
  bool get hasAuthority;

  /// Whether the URI has an explicit port.
  ///
  /// If the port number is the default port number
  /// (zero for unrecognized schemes, with http (80) and https (443) being
  /// recognized),
  /// then the port is made implicit and omitted from the URI.
  bool get hasPort;

  /// Whether the URI has a query part.
  bool get hasQuery;

  /// Whether the URI has a fragment part.
  bool get hasFragment;

  /// Whether the URI has an empty path.
  bool get hasEmptyPath;

  /// Whether the URI has an absolute path (starting with '/').
  bool get hasAbsolutePath;

  /// Returns the origin of the URI in the form scheme://host:port for the
  /// schemes http and https.
  ///
  /// It is an error if the scheme is not "http" or "https", or if the host name
  /// is missing or empty.
  ///
  /// See: https://www.w3.org/TR/2011/WD-html5-20110405/origin-0.html#origin
  String get origin;

  /// Whether the scheme of this [Uri] is [scheme].
  ///
  /// The [scheme] should be the same as the one returned by [Uri.scheme],
  /// but doesn't have to be case-normalized to lower-case characters.
  ///
  /// Example:
  /// ```dart
  /// var uri = Uri.parse('http://example.com');
  /// print(uri.isScheme('HTTP')); // true
  ///
  /// final uriNoScheme = Uri(host: 'example.com');
  /// print(uriNoScheme.isScheme('HTTP')); // false
  /// ```
  ///
  /// An empty [scheme] string matches a URI with no scheme
  /// (one where [hasScheme] returns false).
  bool isScheme(String scheme);

  /// Creates a file path from a file URI.
  ///
  /// The returned path has either Windows or non-Windows
  /// semantics.
  ///
  /// For non-Windows semantics, the slash ("/") is used to separate
  /// path segments.
  ///
  /// For Windows semantics, the backslash ("\\") separator is used to
  /// separate path segments.
  ///
  /// If the URI is absolute, the path starts with a path separator
  /// unless Windows semantics is used and the first path segment is a
  /// drive letter. When Windows semantics is used, a host component in
  /// the uri in interpreted as a file server and a UNC path is
  /// returned.
  ///
  /// The default for whether to use Windows or non-Windows semantics
  /// is determined from the platform Dart is running on. When running in
  /// the standalone VM, this is detected by the VM based on the
  /// operating system. When running in a browser, non-Windows semantics
  /// is always used.
  ///
  /// To override the automatic detection of which semantics to use pass
  /// a value for [windows]. Passing `true` will use Windows
  /// semantics and passing `false` will use non-Windows semantics.
  ///
  /// If the URI ends with a slash (i.e. the last path component is
  /// empty), the returned file path will also end with a slash.
  ///
  /// With Windows semantics, URIs starting with a drive letter cannot
  /// be relative to the current drive on the designated drive. That is,
  /// for the URI `file:///c:abc` calling `toFilePath` will throw as a
  /// path segment cannot contain colon on Windows.
  ///
  /// Examples using non-Windows semantics (resulting of calling
  /// toFilePath in comment):
  /// ```dart
  /// Uri.parse("xxx/yyy");  // xxx/yyy
  /// Uri.parse("xxx/yyy/");  // xxx/yyy/
  /// Uri.parse("file:///xxx/yyy");  // /xxx/yyy
  /// Uri.parse("file:///xxx/yyy/");  // /xxx/yyy/
  /// Uri.parse("file:///C:");  // /C:
  /// Uri.parse("file:///C:a");  // /C:a
  /// ```
  /// Examples using Windows semantics (resulting URI in comment):
  /// ```dart
  /// Uri.parse("xxx/yyy");  // xxx\yyy
  /// Uri.parse("xxx/yyy/");  // xxx\yyy\
  /// Uri.parse("file:///xxx/yyy");  // \xxx\yyy
  /// Uri.parse("file:///xxx/yyy/");  // \xxx\yyy\
  /// Uri.parse("file:///C:/xxx/yyy");  // C:\xxx\yyy
  /// Uri.parse("file:C:xxx/yyy");  // Throws as a path segment
  ///                               // cannot contain colon on Windows.
  /// Uri.parse("file://server/share/file");  // \\server\share\file
  /// ```
  /// If the URI is not a file URI, calling this throws
  /// [UnsupportedError].
  ///
  /// If the URI cannot be converted to a file path, calling this throws
  /// [UnsupportedError].
  // TODO(lrn): Deprecate and move functionality to File class or similar.
  // The core libraries should not worry about the platform.
  String toFilePath({bool? windows});

  /// Access the structure of a `data:` URI.
  ///
  /// Returns a [UriData] object for `data:` URIs and `null` for all other
  /// URIs.
  /// The [UriData] object can be used to access the media type and data
  /// of a `data:` URI.
  UriData? get data;

  /// Returns a hash code computed as `toString().hashCode`.
  ///
  /// This guarantees that URIs with the same normalized string representation
  /// have the same hash code.
  int get hashCode;

  /// A URI is equal to another URI with the same normalized representation.
  bool operator ==(Object other);

  /// The normalized string representation of the URI.
  String toString();

  /// Creates a new `Uri` based on this one, but with some parts replaced.
  ///
  /// This method takes the same parameters as the [Uri] constructor,
  /// and they have the same meaning.
  ///
  /// At most one of [path] and [pathSegments] must be provided.
  /// Likewise, at most one of [query] and [queryParameters] must be provided.
  ///
  /// Each part that is not provided will default to the corresponding
  /// value from this `Uri` instead.
  ///
  /// This method is different from [Uri.resolve], which overrides in a
  /// hierarchical manner,
  /// and can instead replace each part of a `Uri` individually.
  ///
  /// Example:
  /// ```dart
  /// final uri1 = Uri.parse(
  ///     '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<String>? pathSegments,
    String? query,
    Map<String, dynamic /*String?|Iterable<String>*/>? queryParameters,
    String? fragment,
  });

  /// Creates a `Uri` that differs from this only in not having a fragment.
  ///
  /// If this `Uri` does not have a fragment, it is itself returned.
  ///
  /// Example:
  /// ```dart
  /// final uri =
  ///     Uri.parse('https://example.org:8080/foo/bar#frag').removeFragment();
  /// print(uri); // https://example.org:8080/foo/bar
  /// ```
  Uri removeFragment();

  /// Resolve [reference] as an URI relative to `this`.
  ///
  /// First turn [reference] into a URI using [Uri.parse]. Then resolve the
  /// resulting URI relative to `this`.
  ///
  /// Returns the resolved URI.
  ///
  /// See [resolveUri] for details.
  Uri resolve(String reference);

  /// Resolve [reference] as a URI relative to `this`.
  ///
  /// Returns the resolved URI.
  ///
  /// The algorithm "Transform Reference" for resolving a reference is described
  /// in [RFC-3986 Section 5](https://tools.ietf.org/html/rfc3986#section-5
  /// "RFC-1123").
  ///
  /// Updated to handle the case where the base URI is just a relative path -
  /// that is: when it has no scheme and no authority and the path does not
  /// start with a slash.
  /// In that case, the paths are combined without removing leading "..", and
  /// an empty path is not converted to "/".
  Uri resolveUri(Uri reference);

  /// Returns a URI where the path has been normalized.
  ///
  /// A normalized path does not contain `.` segments or non-leading `..`
  /// segments.
  /// Only a relative path with no scheme or authority may contain
  /// leading `..` segments;
  /// a path that starts with `/` will also drop any leading `..` segments.
  ///
  /// This uses the same normalization strategy as `Uri().resolve(this)`.
  ///
  /// Does not change any part of the URI except the path.
  ///
  /// The default implementation of `Uri` always normalizes paths, so calling
  /// this function has no effect.
  Uri normalizePath();

  /// Creates a new `Uri` object by parsing a URI string.
  ///
  /// If [start] and [end] are provided, they must specify a valid substring
  /// of [uri], and only the substring from `start` to `end` is parsed as a URI.
  ///
  /// If the [uri] string is not valid as a URI or URI reference,
  /// a [FormatException] is thrown.
  ///
  /// Example:
  /// ```dart
  /// final uri =
  ///     Uri.parse('https://example.com/api/fetch?limit=10,20,30&max=100');
  /// print(uri); // https://example.com/api/fetch?limit=10,20,30&max=100
  ///
  /// Uri.parse('::Not valid URI::'); // Throws FormatException.
  /// ```
  static Uri parse(String uri, [int start = 0, int? end]) {
    // This parsing will not validate percent-encoding, IPv6, etc.
    // When done splitting into parts, it will call, e.g., [_makeFragment]
    // to do the final parsing.
    //
    // Important parts of the RFC 3986 used here:
    // URI           = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
    //
    // hier-part     = "//" authority path-abempty
    //               / path-absolute
    //               / path-rootless
    //               / path-empty
    //
    // URI-reference = URI / relative-ref
    //
    // absolute-URI  = scheme ":" hier-part [ "?" query ]
    //
    // relative-ref  = relative-part [ "?" query ] [ "#" fragment ]
    //
    // relative-part = "//" authority path-abempty
    //               / path-absolute
    //               / path-noscheme
    //               / path-empty
    //
    // scheme        = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
    //
    // authority     = [ userinfo "@" ] host [ ":" port ]
    // userinfo      = *( unreserved / pct-encoded / sub-delims / ":" )
    // host          = IP-literal / IPv4address / reg-name
    // IP-literal    = "[" ( IPv6address / IPv6addrz / IPvFuture ) "]"
    // IPv6addrz     = IPv6address "%25" ZoneID
    // ZoneID        = 1*( unreserved / pct-encoded )
    // port          = *DIGIT
    // reg-name      = *( unreserved / pct-encoded / sub-delims )
    //
    // path          = path-abempty    ; begins with "/" or is empty
    //               / path-absolute   ; begins with "/" but not "//"
    //               / path-noscheme   ; begins with a non-colon segment
    //               / path-rootless   ; begins with a segment
    //               / path-empty      ; zero characters
    //
    // path-abempty  = *( "/" segment )
    // path-absolute = "/" [ segment-nz *( "/" segment ) ]
    // path-noscheme = segment-nz-nc *( "/" segment )
    // path-rootless = segment-nz *( "/" segment )
    // path-empty    = 0<pchar>
    //
    // segment       = *pchar
    // segment-nz    = 1*pchar
    // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" )
    //               ; non-zero-length segment without any colon ":"
    //
    // pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
    //
    // query         = *( pchar / "/" / "?" )
    //
    // fragment      = *( pchar / "/" / "?" )
    // 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<int>.filled(8, 0, growable: false);

    // Set default values for each position.
    // The value will either be correct in some cases where it isn't set
    // by the scanner, or it is clearly recognizable as an unset value.
    indices
      ..[0] = 0
      ..[_schemeEndIndex] = start - 1
      ..[_hostStartIndex] = start - 1
      ..[_notSimpleIndex] = start - 1
      ..[_portStartIndex] = start
      ..[_pathStartIndex] = start
      ..[_queryStartIndex] = end
      ..[_fragmentStartIndex] = end;
    var state = _scan(uri, start, end, _uriStart, indices);
    // Some states that should be non-simple, but the URI ended early.
    // Paths that end at a ".." must be normalized to end in "../".
    if (state >= _nonSimpleEndStates) {
      indices[_notSimpleIndex] = end;
    }
    int schemeEnd = indices[_schemeEndIndex];
    if (schemeEnd >= start) {
      // Rescan the scheme part now that we know it's not a path.
      state = _scan(uri, start, schemeEnd, _schemeStart, indices);
      if (state == _schemeStart) {
        // Empty scheme.
        indices[_notSimpleIndex] = schemeEnd;
      }
    }
    // The returned positions are limited by the scanners ability to write only
    // one position per character, and only the current position.
    // Scanning from left to right, we only know whether something is a scheme
    // or a path when we see a `:` or `/`, and likewise we only know if the first
    // `/` is part of the path or is leading an authority component when we see
    // the next character.

    int hostStart = indices[_hostStartIndex] + 1;
    int portStart = indices[_portStartIndex];
    int pathStart = indices[_pathStartIndex];
    int queryStart = indices[_queryStartIndex];
    int fragmentStart = indices[_fragmentStartIndex];

    // We may discover the scheme while handling special cases.
    String? scheme;

    // Derive some positions that weren't set to normalize the indices.
    if (fragmentStart < queryStart) queryStart = fragmentStart;
    // If pathStart isn't set (it's before scheme end or host start), then
    // the path is empty, or there is no authority part and the path
    // starts with a non-simple character.
    if (pathStart < hostStart) {
      // There is an authority, but no path. The path would start with `/`
      // if it was there.
      pathStart = queryStart;
    } else if (pathStart <= schemeEnd) {
      // There is a scheme, but no authority.
      pathStart = schemeEnd + 1;
    }
    // If there is an authority with no port, set the port position
    // to be at the end of the authority (equal to pathStart).
    // This also handles a ":" in a user-info component incorrectly setting
    // the port start position.
    if (portStart < hostStart) portStart = pathStart;

    assert(hostStart == start || schemeEnd <= hostStart);
    assert(hostStart <= portStart);
    assert(schemeEnd <= pathStart);
    assert(portStart <= pathStart);
    assert(pathStart <= queryStart);
    assert(queryStart <= fragmentStart);

    bool isSimple = indices[_notSimpleIndex] < start;

    if (isSimple) {
      // Check/do normalizations that weren't detected by the scanner.
      // This includes removal of empty port or userInfo,
      // or scheme specific port and path normalizations.
      if (hostStart > schemeEnd + 3) {
        // Always be non-simple if URI contains user-info.
        // The scanner doesn't set the not-simple position in this case because
        // it's setting the host-start position instead.
        isSimple = false;
      } else if (portStart > start && portStart + 1 == pathStart) {
        // If the port is empty, it should be omitted.
        // Pathological case, don't bother correcting it.
        isSimple = false;
      } else if (uri.startsWith(r"\", pathStart) ||
          hostStart > start &&
              (uri.startsWith(r"\", hostStart - 1) ||
                  uri.startsWith(r"\", hostStart - 2))) {
        // Seeing a `\` anywhere.
        // The scanner doesn't record when the first path character is a `\`
        // or when the last slash before the authority is a `\`.
        isSimple = false;
      } else if (queryStart < end &&
              (queryStart == pathStart + 2 &&
                  uri.startsWith("..", pathStart)) ||
          (queryStart > pathStart + 2 &&
              uri.startsWith("/..", queryStart - 3))) {
        // The path ends in a ".." segment. This should be normalized to "../".
        // We didn't detect this while scanning because a query or fragment was
        // detected at the same time (which is why we only need to check this
        // if there is something after the path).
        isSimple = false;
      } else {
        // There are a few scheme-based normalizations that
        // the scanner couldn't check.
        // That means that the input is very close to simple, so just do
        // the normalizations.
        if (schemeEnd == start + 4) {
          // Do scheme based normalizations for file, http.
          if (uri.startsWith("file", start)) {
            scheme = "file";
            if (hostStart <= start) {
              // File URIs should have an authority.
              // Paths after an authority should be absolute.
              String schemeAuth = "file://";
              int delta = 2;
              if (!uri.startsWith("/", pathStart)) {
                schemeAuth = "file:///";
                delta = 3;
              }
              uri = schemeAuth + uri.substring(pathStart, end);
              schemeEnd -= start;
              hostStart = 7;
              portStart = 7;
              pathStart = 7;
              queryStart += delta - start;
              fragmentStart += delta - start;
              start = 0;
              end = uri.length;
            } else if (pathStart == queryStart) {
              // Uri has authority and empty path. Add "/" as path.
              if (start == 0 && end == uri.length) {
                uri = uri.replaceRange(pathStart, queryStart, "/");
                queryStart += 1;
                fragmentStart += 1;
                end += 1;
              } else {
                uri =
                    "${uri.substring(start, pathStart)}/"
                    "${uri.substring(queryStart, end)}";
                schemeEnd -= start;
                hostStart -= start;
                portStart -= start;
                pathStart -= start;
                queryStart += 1 - start;
                fragmentStart += 1 - start;
                start = 0;
                end = uri.length;
              }
            }
          } else if (uri.startsWith("http", start)) {
            scheme = "http";
            // HTTP URIs should not have an explicit port of 80.
            if (portStart > start &&
                portStart + 3 == pathStart &&
                uri.startsWith("80", portStart + 1)) {
              if (start == 0 && end == uri.length) {
                uri = uri.replaceRange(portStart, pathStart, "");
                pathStart -= 3;
                queryStart -= 3;
                fragmentStart -= 3;
                end -= 3;
              } else {
                uri =
                    uri.substring(start, portStart) +
                    uri.substring(pathStart, end);
                schemeEnd -= start;
                hostStart -= start;
                portStart -= start;
                pathStart -= 3 + start;
                queryStart -= 3 + start;
                fragmentStart -= 3 + start;
                start = 0;
                end = uri.length;
              }
            }
          }
        } else if (schemeEnd == start + 5 && uri.startsWith("https", start)) {
          scheme = "https";
          // HTTPS URIs should not have an explicit port of 443.
          if (portStart > start &&
              portStart + 4 == pathStart &&
              uri.startsWith("443", portStart + 1)) {
            if (start == 0 && end == uri.length) {
              uri = uri.replaceRange(portStart, pathStart, "");
              pathStart -= 4;
              queryStart -= 4;
              fragmentStart -= 4;
              end -= 3;
            } else {
              uri =
                  uri.substring(start, portStart) +
                  uri.substring(pathStart, end);
              schemeEnd -= start;
              hostStart -= start;
              portStart -= start;
              pathStart -= 4 + start;
              queryStart -= 4 + start;
              fragmentStart -= 4 + start;
              start = 0;
              end = uri.length;
            }
          }
        }
      }
    }

    if (isSimple) {
      if (start > 0 || end < uri.length) {
        uri = uri.substring(start, end);
        schemeEnd -= start;
        hostStart -= start;
        portStart -= start;
        pathStart -= start;
        queryStart -= start;
        fragmentStart -= start;
      }
      return _SimpleUri(
        uri,
        schemeEnd,
        hostStart,
        portStart,
        pathStart,
        queryStart,
        fragmentStart,
        scheme,
      );
    }

    return _Uri.notSimple(
      uri,
      start,
      end,
      schemeEnd,
      hostStart,
      portStart,
      pathStart,
      queryStart,
      fragmentStart,
      scheme,
    );
  }

  /// Creates a new `Uri` object by parsing a URI string.
  ///
  /// If [start] and [end] are provided, they must specify a valid substring
  /// of [uri], and only the substring from `start` to `end` is parsed as a URI.
  ///
  /// Returns `null` if the [uri] string is not valid as a URI or URI reference.
  ///
  /// Example:
  /// ```dart
  /// final uri = Uri.tryParse(
  ///     'https://dart.dev/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<String, String> splitQueryString(
    String query, {
    Encoding encoding = utf8,
  }) {
    return query.split("&").fold({}, (map, element) {
      int index = element.indexOf("=");
      if (index == -1) {
        if (element != "") {
          map[decodeQueryComponent(element, encoding: encoding)] = "";
        }
      } else if (index != 0) {
        var key = element.substring(0, index);
        var value = element.substring(index + 1);
        map[decodeQueryComponent(key, encoding: encoding)] =
            decodeQueryComponent(value, encoding: encoding);
      }
      return map;
    });
  }

  /// Parses the [host] as an IP version 4 (IPv4) address, returning the address
  /// as a list of 4 bytes in network byte order (big endian).
  ///
  /// 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
  /// `<octet>.<octet>.<octet>.<octet>` where each octet is a decimal
  /// numeral in the range 0..255 with no leading zeros.
  static List<int> 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*
  /// `<octet>.<octet>.<octet>.<octet>` 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' \<hexDigit\>+ '.' (\<unreserved\> | \<sub-delims\> | ':')+
  ///
  /// 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<int> 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<String> pathSegments = _computePathSegments(this.path);

  /// Lazily computed and cached hashCode of [_text].
  late final int hashCode = this._text.hashCode;

  /// Cache the computed return value of [queryParameters].
  late final Map<String, String> queryParameters =
      UnmodifiableMapView<String, String>(Uri.splitQueryString(this.query));

  /// Cache the computed return value of [queryParametersAll].
  late final Map<String, List<String>> queryParametersAll =
      _computeQueryParametersAll(this.query);

  /// Internal non-verifying constructor. Only call with validated arguments.
  ///
  /// The components must be properly normalized.
  ///
  /// Use `null` for [_host] if there is no authority. In that case, always
  /// pass `null` for [_port] and an empty string for [_userInfo] as well.
  ///
  /// Use `null` for [_port], [_query] and [_fragment] if there is
  /// component of that type, and empty string for [_userInfo].
  ///
  /// The [path] and [scheme] are never empty.
  _Uri._internal(
    this.scheme,
    this._userInfo,
    this._host,
    this._port,
    this.path,
    this._query,
    this._fragment,
  );

  /// Create a [_Uri] from parts of [uri].
  ///
  /// The parameters specify the start/end of particular components of the URI.
  /// The [scheme] may contain a string representing a normalized scheme
  /// component if one has already been discovered.
  factory _Uri.notSimple(
    String uri,
    int start,
    int end,
    int schemeEnd,
    int hostStart,
    int portStart,
    int pathStart,
    int queryStart,
    int fragmentStart,
    String? scheme,
  ) {
    if (scheme == null) {
      scheme = "";
      if (schemeEnd > start) {
        scheme = _makeScheme(uri, start, schemeEnd);
      } else if (schemeEnd == start) {
        _fail(uri, start, "Invalid empty scheme");
      }
    }
    String userInfo = "";
    String? host;
    int? port;
    if (hostStart > start) {
      int userInfoStart = schemeEnd + 3;
      if (userInfoStart < hostStart) {
        userInfo = _makeUserInfo(uri, userInfoStart, hostStart - 1);
      }
      host = _makeHost(uri, hostStart, portStart, false);
      if (portStart + 1 < pathStart) {
        int portNumber =
            int.tryParse(uri.substring(portStart + 1, pathStart)) ??
            (throw FormatException("Invalid port", uri, portStart + 1));
        port = _makePort(portNumber, scheme);
      }
    }
    String path = _makePath(
      uri,
      pathStart,
      queryStart,
      null,
      scheme,
      host != null,
    );
    String? query;
    if (queryStart < fragmentStart) {
      query = _makeQuery(uri, queryStart + 1, fragmentStart, null);
    }
    String? fragment;
    if (fragmentStart < end) {
      fragment = _makeFragment(uri, fragmentStart + 1, end);
    }
    return _Uri._internal(scheme, userInfo, host, port, path, query, fragment);
  }

  /// Implementation of [Uri.Uri].
  factory _Uri({
    String? scheme,
    String? userInfo,
    String? host,
    int? port,
    String? path,
    Iterable<String>? pathSegments,
    String? query,
    Map<String, dynamic /*String?|Iterable<String>*/>? queryParameters,
    String? fragment,
  }) {
    if (scheme == null) {
      scheme = "";
    } else {
      scheme = _makeScheme(scheme, 0, scheme.length);
    }
    userInfo = _makeUserInfo(userInfo, 0, _stringOrNullLength(userInfo));
    if (userInfo == null) {
      // TODO(dart-lang/language#440): Remove when promotion works.
      throw "unreachable";
    }
    host = _makeHost(host, 0, _stringOrNullLength(host), false);
    // Special case this constructor for backwards compatibility.
    if (query == "") query = null;
    query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters);
    fragment = _makeFragment(fragment, 0, _stringOrNullLength(fragment));
    port = _makePort(port, scheme);
    bool isFile = (scheme == "file");
    if (host == null && (userInfo.isNotEmpty || port != null || isFile)) {
      host = "";
    }
    bool hasAuthority = (host != null);
    path = _makePath(
      path,
      0,
      _stringOrNullLength(path),
      pathSegments,
      scheme,
      hasAuthority,
    );
    if (path == null) {
      // TODO(dart-lang/language#440): Remove when promotion works.
      throw "unreachable";
    }
    if (scheme.isEmpty && host == null && !path.startsWith('/')) {
      bool allowScheme = scheme.isNotEmpty || host != null;
      path = _normalizeRelativePath(path, allowScheme);
    } else {
      path = _removeDotSegments(path);
    }
    if (host == null && path.startsWith("//")) {
      host = "";
    }
    return _Uri._internal(scheme, userInfo, host, port, path, query, fragment);
  }

  /// Implementation of [Uri.http].
  factory _Uri.http(
    String authority, [
    String unencodedPath = '',
    Map<String, dynamic>? queryParameters,
  ]) {
    return _makeHttpUri("http", authority, unencodedPath, queryParameters);
  }

  /// Implementation of [Uri.https].
  factory _Uri.https(
    String authority, [
    String unencodedPath = '',
    Map<String, dynamic>? queryParameters,
  ]) {
    return _makeHttpUri("https", authority, unencodedPath, queryParameters);
  }

  String get authority {
    if (!hasAuthority) return "";
    var sb = StringBuffer();
    _writeAuthority(sb);
    return sb.toString();
  }

  String get userInfo => _userInfo;

  String get host {
    String? host = _host;
    if (host == null) return "";
    if (host.startsWith('[') && !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<String, dynamic>? queryParameters,
  ) {
    var userInfo = "";
    String? host;
    int? port;

    if (authority != null && authority.isNotEmpty) {
      var hostStart = 0;
      // Split off the user info.
      for (int i = 0; i < authority.length; i++) {
        const int atSign = 0x40;
        if (authority.codeUnitAt(i) == atSign) {
          userInfo = authority.substring(0, i);
          hostStart = i + 1;
          break;
        }
      }
      var hostEnd = hostStart;
      if (hostStart < authority.length &&
          authority.codeUnitAt(hostStart) == _LEFT_BRACKET) {
        // IPv6 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<String> segments,
    bool argumentError,
  ) {
    for (var segment in segments) {
      if (segment.contains("/")) {
        if (argumentError) {
          throw ArgumentError("Illegal path character $segment");
        } else {
          throw UnsupportedError("Illegal path character $segment");
        }
      }
    }
  }

  static void _checkWindowsPathReservedCharacters(
    List<String> segments,
    bool argumentError, [
    int firstSegment = 0,
  ]) {
    for (var segment in segments.skip(firstSegment)) {
      if (segment.contains(RegExp(r'["*/:<>?\\|]'))) {
        if (argumentError) {
          throw ArgumentError("Illegal character in path");
        } else {
          throw UnsupportedError("Illegal character in path: $segment");
        }
      }
    }
  }

  static void _checkWindowsDriveLetter(int charCode, bool argumentError) {
    if ((_UPPER_CASE_A <= charCode && charCode <= _UPPER_CASE_Z) ||
        (_LOWER_CASE_A <= charCode && charCode <= _LOWER_CASE_Z)) {
      return;
    }
    if (argumentError) {
      throw ArgumentError(
        "Illegal drive letter " + String.fromCharCode(charCode),
      );
    } else {
      throw UnsupportedError(
        "Illegal drive letter " + String.fromCharCode(charCode),
      );
    }
  }

  static Uri _makeFileUri(String path, bool slashTerminated) {
    const String sep = "/";
    var segments = path.split(sep);
    if (slashTerminated && segments.isNotEmpty && segments.last.isNotEmpty) {
      segments.add(""); // Extra separator at end.
    }
    if (path.startsWith(sep)) {
      // Absolute file:// URI.
      return Uri(scheme: "file", pathSegments: segments);
    } else {
      // Relative URI.
      return Uri(pathSegments: segments);
    }
  }

  static _makeWindowsFileUrl(String path, bool slashTerminated) {
    if (path.startsWith(r"\\?\")) {
      if (path.startsWith(r"UNC\", 4)) {
        path = path.replaceRange(0, 7, r'\');
      } else {
        path = path.substring(4);
        if (path.length < 3 ||
            path.codeUnitAt(1) != _COLON ||
            path.codeUnitAt(2) != _BACKSLASH) {
          throw ArgumentError.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<String>? pathSegments,
    String? query,
    Map<String, dynamic /*String?|Iterable<String>*/>? queryParameters,
    String? fragment,
  }) {
    // Set to true if the scheme has (potentially) changed.
    // In that case, the default port may also have changed and we need
    // to check even the existing port.
    bool schemeChanged = false;
    if (scheme != null) {
      scheme = _makeScheme(scheme, 0, scheme.length);
      schemeChanged = (scheme != this.scheme);
    } else {
      scheme = this.scheme;
    }

    bool isFile = (scheme == "file");
    if (userInfo != null) {
      userInfo = _makeUserInfo(userInfo, 0, userInfo.length);
    } else {
      userInfo = this._userInfo;
    }

    if (port != null) {
      port = _makePort(port, scheme);
    } else {
      port = this._port;
      if (schemeChanged) {
        // The default port might have changed.
        port = _makePort(port, scheme);
      }
    }
    if (host != null) {
      host = _makeHost(host, 0, host.length, false);
    } else if (this.hasAuthority) {
      host = this._host;
    } else if (userInfo.isNotEmpty || port != null || isFile) {
      host = "";
    }

    bool hasAuthority = host != null;
    if (path != null || pathSegments != null) {
      path = _makePath(
        path,
        0,
        _stringOrNullLength(path),
        pathSegments,
        scheme,
        hasAuthority,
      );
    } else {
      var currentPath = this.path;
      if ((isFile || (hasAuthority && !currentPath.isEmpty)) &&
          !currentPath.startsWith('/')) {
        currentPath = "/" + currentPath;
      }
      path = currentPath;
    }

    if (query != null || queryParameters != null) {
      query = _makeQuery(query, 0, _stringOrNullLength(query), queryParameters);
    } else {
      query = this._query;
    }

    if (fragment != null) {
      fragment = _makeFragment(fragment, 0, fragment.length);
    } else {
      fragment = this._fragment;
    }

    return _Uri._internal(scheme, userInfo, host, port, path, query, fragment);
  }

  Uri removeFragment() {
    if (!this.hasFragment) return this;
    return _Uri._internal(scheme, _userInfo, _host, _port, path, _query, null);
  }

  static List<String> _computePathSegments(String pathToSplit) {
    if (pathToSplit.isNotEmpty && pathToSplit.codeUnitAt(0) == _SLASH) {
      pathToSplit = pathToSplit.substring(1);
    }
    return (pathToSplit.isEmpty)
        ? const <String>[]
        : List<String>.unmodifiable(
            pathToSplit.split("/").map(Uri.decodeComponent),
          );
  }

  static Map<String, List<String>> _computeQueryParametersAll(String? query) {
    if (query == null || query.isEmpty) return const <String, List<String>>{};
    Map<String, List<String>> queryParameterLists = _splitQueryStringAll(query);
    queryParameterLists.updateAll(_toUnmodifiableStringList);
    return Map<String, List<String>>.unmodifiable(queryParameterLists);
  }

  Uri normalizePath() {
    String path = _normalizePath(this.path, scheme, hasAuthority);
    if (identical(path, this.path)) return this;
    return this.replace(path: path);
  }

  static int? _makePort(int? port, String scheme) {
    // Perform scheme specific normalization.
    if (port != null && port == _defaultPort(scheme)) return null;
    return port;
  }

  /// Check and normalize a host name.
  ///
  /// If the host name starts and ends with '[' and ']', it is considered an
  /// IPv6 address (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<String>? pathSegments,
    String scheme,
    bool hasAuthority,
  ) {
    bool isFile = (scheme == "file");
    bool ensureLeadingSlash = isFile || hasAuthority;
    String result;
    if (path == null) {
      if (pathSegments == null) return isFile ? "/" : "";
      result = pathSegments
          .map((s) => _uriEncode(_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<String, dynamic /*String?|Iterable<String>*/>? 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<String, dynamic /*String?|Iterable<String>*/> 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<String, dynamic /*String?|Iterable<String>*/> 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<int> 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<String> output = [];
    bool appendSlash = false;
    for (String segment in path.split("/")) {
      appendSlash = false;
      if (segment == "..") {
        if (output.isNotEmpty) {
          output.removeLast();
          if (output.isEmpty) {
            output.add("");
          }
        }
        appendSlash = true;
      } else if ("." == segment) {
        appendSlash = true;
      } else {
        output.add(segment);
      }
    }
    if (appendSlash) output.add("");
    return output.join("/");
  }

  /// Removes all `.` segments and any non-leading `..` segments.
  ///
  /// 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<String> 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<String> _createList() => <String>[];

  static Map<String, List<String>> _splitQueryStringAll(
    String query, {
    Encoding encoding = utf8,
  }) {
    var result = <String, List<String>>{};
    int i = 0;
    int start = 0;
    int equalsIndex = -1;

    void parsePair(int start, int equalsIndex, int end) {
      String key;
      String value;
      if (start == end) return;
      if (equalsIndex < 0) {
        key = _uriDecode(query, start, end, encoding, true);
        value = "";
      } else {
        key = _uriDecode(query, start, equalsIndex, encoding, true);
        value = _uriDecode(query, equalsIndex + 1, end, encoding, true);
      }
      result.putIfAbsent(key, _createList).add(value);
    }

    while (i < query.length) {
      int char = query.codeUnitAt(i);
      if (char == _EQUALS) {
        if (equalsIndex < 0) equalsIndex = i;
      } else if (char == _AMPERSAND) {
        parsePair(start, equalsIndex, i);
        start = i + 1;
        equalsIndex = -1;
      }
      i++;
    }
    parsePair(start, equalsIndex, i);
    return result;
  }

  external static String _uriEncode(
    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<int> bytes;
    if (simple) {
      if (utf8 == encoding || latin1 == encoding || ascii == encoding) {
        return text.substring(start, end);
      } else {
        bytes = text.substring(start, end).codeUnits;
      }
    } else {
      bytes = <int>[];
      for (int i = start; i < end; i++) {
        var codeUnit = text.codeUnitAt(i);
        if (codeUnit > 127) {
          throw ArgumentError("Illegal percent encoding in URI");
        }
        if (codeUnit == _PERCENT) {
          if (i + 3 > text.length) {
            throw ArgumentError('Truncated URI');
          }
          bytes.add(_hexCharPairToByte(text, i + 1));
          i += 2;
        } else if (plusToSpace && codeUnit == _PLUS) {
          bytes.add(_SPACE);
        } else {
          bytes.add(codeUnit);
        }
      }
    }
    return encoding.decode(bytes);
  }

  static bool _isAlphabeticCharacter(int codeUnit) {
    var lowerCase = codeUnit | 0x20;
    return (_LOWER_CASE_A <= lowerCase && lowerCase <= _LOWER_CASE_Z);
  }

  static bool _isUnreservedChar(int char) {
    return char < 127 &&
        ((_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<int> _separatorIndices;

  /// Cache of the result returned by [uri].
  Uri? _uriCache;

  UriData._(this._text, this._separatorIndices, this._uriCache);

  // Avoid shadowing by argument.
  static const Base64Codec _base64 = base64;

  /// Creates a `data:` URI containing the [content] string.
  ///
  /// Equivalent to `Uri.dataFromString(...).data`, but may
  /// be more efficient if the [uri] itself isn't used.
  factory UriData.fromString(
    String content, {
    String? mimeType,
    Encoding? encoding,
    Map<String, String>? parameters,
    bool base64 = false,
  }) {
    StringBuffer buffer = StringBuffer();
    List<int> indices = [_noScheme];
    String? charsetName = parameters?["charset"];
    String? encodingName;
    if (encoding == null) {
      if (charsetName != null) {
        encoding = Encoding.getByName(charsetName);
      }
    } else if (charsetName == null) {
      // Non-null only if parameters does not contain "charset".
      encodingName = encoding.name;
    }
    encoding ??= ascii;
    _writeUri(mimeType, encodingName, parameters, buffer, indices);
    indices.add(buffer.length);
    if (base64) {
      buffer.write(';base64,');
      indices.add(buffer.length - 1);
      buffer.write(encoding.fuse(_base64).encode(content));
    } else {
      buffer.write(',');
      _uriEncodeBytes(_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<int> bytes, {
    String mimeType = "application/octet-stream",
    Map<String, String>? parameters,
    bool percentEncoded = false,
  }) {
    StringBuffer buffer = StringBuffer();
    List<int> indices = [_noScheme];
    _writeUri(mimeType, null, parameters, buffer, indices);
    indices.add(buffer.length);
    if (percentEncoded) {
      buffer.write(',');
      _uriEncodeBytes(_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<String, String>? parameters,
    StringBuffer buffer,
    List<int>? indices,
  ) {
    if (mimeType == null || _caseInsensitiveEquals("text/plain", mimeType)) {
      mimeType = "";
    }

    if (mimeType.isEmpty || identical(mimeType, "application/octet-stream")) {
      buffer.write(mimeType); // Common cases need no escaping.
    } else {
      int slashIndex = _validateMimeType(mimeType);
      if (slashIndex < 0) {
        throw ArgumentError.value(mimeType, "mimeType", "Invalid MIME type");
      }
      buffer.write(
        _Uri._uriEncode(
          _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<String, String> get parameters {
    var result = <String, String>{};
    for (int i = 3; i < _separatorIndices.length; i += 2) {
      var start = _separatorIndices[i - 2] + 1;
      var equals = _separatorIndices[i - 1];
      var end = _separatorIndices[i];
      String key = _Uri._uriDecode(_text, start, equals, utf8, false);
      String value = _Uri._uriDecode(_text, equals + 1, end, utf8, false);
      result[key] = value;
    }
    return result;
  }

  static UriData _parse(String text, int start, Uri? sourceUri) {
    assert(start == 0 || start == 5);
    assert((start == 5) == text.startsWith("data:"));

    /// Character codes.
    const int comma = 0x2c;
    const int slash = 0x2f;
    const int semicolon = 0x3b;
    const int equals = 0x3d;
    List<int> indices = [start - 1];
    int slashIndex = -1;
    var char;
    int i = start;
    for (; i < text.length; i++) {
      char = text.codeUnitAt(i);
      if (char == comma || char == semicolon) break;
      if (char == slash) {
        if (slashIndex < 0) {
          slashIndex = i;
          continue;
        }
        throw FormatException("Invalid MIME type", text, i);
      }
    }
    if (slashIndex < 0 && i > start) {
      // An empty MIME type is allowed, but if non-empty it must contain
      // exactly one slash.
      throw FormatException("Invalid MIME type", text, i);
    }
    while (char != comma) {
      // Parse parameters and/or "base64".
      indices.add(i);
      i++;
      int equalsIndex = -1;
      for (; i < text.length; i++) {
        char = text.codeUnitAt(i);
        if (char == equals) {
          if (equalsIndex < 0) equalsIndex = i;
        } else if (char == semicolon || char == comma) {
          break;
        }
      }
      if (equalsIndex >= 0) {
        indices.add(equalsIndex);
      } else {
        // Have to be final "base64".
        var lastSeparator = indices.last;
        if (char != comma ||
            i != lastSeparator + 7 /* "base64,".length */ ||
            !text.startsWith("base64", lastSeparator + 1)) {
          throw FormatException("Expecting '='", text, i);
        }
        break;
      }
    }
    indices.add(i);
    bool isBase64 = indices.length.isOdd;
    if (isBase64) {
      text = base64.normalize(text, i + 1, text.length);
    } else {
      // Validate "data" part, must only contain RFC 2396 'uric' characters
      // (reserved, unreserved, or escape sequences).
      // Normalize to this (throws on a fragment separator).
      var data = _Uri._normalize(
        text,
        i + 1,
        text.length,
        _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<int> bytes,
    StringSink buffer,
  ) {
    // Encode the string into bytes then generate an ASCII only string
    // by percent encoding selected bytes.
    int byteOr = 0;
    for (int i = 0; i < bytes.length; i++) {
      int byte = bytes[i];
      byteOr |= byte;
      if (byte < 128 && ((_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<int> 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<String> get pathSegments {
    int start = _pathStart;
    int end = _queryStart;
    if (_uri.startsWith("/", start)) start++;
    if (start == end) return const <String>[];
    List<String> parts = [];
    for (int i = start; i < end; i++) {
      var char = _uri.codeUnitAt(i);
      if (char == _SLASH) {
        parts.add(_uri.substring(start, i));
        start = i + 1;
      }
    }
    parts.add(_uri.substring(start, end));
    return List<String>.unmodifiable(parts);
  }

  Map<String, String> get queryParameters {
    if (!hasQuery) return const <String, String>{};
    return UnmodifiableMapView<String, String>(Uri.splitQueryString(query));
  }

  Map<String, List<String>> get queryParametersAll {
    if (!hasQuery) return const <String, List<String>>{};
    Map<String, List<String>> queryParameterLists = _Uri._splitQueryStringAll(
      query,
    );
    queryParameterLists.updateAll(_toUnmodifiableStringList);
    return Map<String, List<String>>.unmodifiable(queryParameterLists);
  }

  bool _isPort(String port) {
    int portDigitStart = _portStart + 1;
    return portDigitStart + port.length == _pathStart &&
        _uri.startsWith(port, portDigitStart);
  }

  Uri normalizePath() => this;

  Uri removeFragment() {
    if (!hasFragment) return this;
    return _SimpleUri(
      _uri.substring(0, _fragmentStart),
      _schemeEnd,
      _hostStart,
      _portStart,
      _pathStart,
      _queryStart,
      _fragmentStart,
      _schemeCache,
    );
  }

  Uri replace({
    String? scheme,
    String? userInfo,
    String? host,
    int? port,
    String? path,
    Iterable<String>? pathSegments,
    String? query,
    Map<String, dynamic /*String?|Iterable<String>*/>? queryParameters,
    String? fragment,
  }) {
    bool schemeChanged = false;
    if (scheme != null) {
      scheme = _Uri._makeScheme(scheme, 0, scheme.length);
      schemeChanged = !_isScheme(scheme);
    } else {
      scheme = this.scheme;
    }
    bool isFile = (scheme == "file");
    if (userInfo != null) {
      userInfo = _Uri._makeUserInfo(userInfo, 0, userInfo.length);
    } else if (_hostStart > 0) {
      userInfo = _uri.substring(_schemeEnd + 3, _hostStart);
    } else {
      userInfo = "";
    }
    if (port != null) {
      port = _Uri._makePort(port, scheme);
    } else {
      port = this.hasPort ? this.port : null;
      if (schemeChanged) {
        // The default port might have changed.
        port = _Uri._makePort(port, scheme);
      }
    }
    if (host != null) {
      host = _Uri._makeHost(host, 0, host.length, false);
    } else if (_hostStart > 0) {
      host = _uri.substring(_hostStart, _portStart);
    } else if (userInfo.isNotEmpty || port != null || isFile) {
      host = "";
    }

    bool hasAuthority = host != null;
    if (path != null || pathSegments != null) {
      path = _Uri._makePath(
        path,
        0,
        _stringOrNullLength(path),
        pathSegments,
        scheme,
        hasAuthority,
      );
    } else {
      path = _uri.substring(_pathStart, _queryStart);
      if ((isFile || (hasAuthority && !path.isEmpty)) &&
          !path.startsWith('/')) {
        path = "/" + path;
      }
    }

    if (query != null || queryParameters != null) {
      query = _Uri._makeQuery(
        query,
        0,
        _stringOrNullLength(query),
        queryParameters,
      );
    } else if (_queryStart < _fragmentStart) {
      query = _uri.substring(_queryStart + 1, _fragmentStart);
    }

    if (fragment != null) {
      fragment = _Uri._makeFragment(fragment, 0, fragment.length);
    } else if (_fragmentStart < _uri.length) {
      fragment = _uri.substring(_fragmentStart + 1);
    }

    return _Uri._internal(scheme, userInfo, host, port, path, query, fragment);
  }

  Uri resolve(String reference) {
    return resolveUri(Uri.parse(reference));
  }

  Uri resolveUri(Uri reference) {
    if (reference is _SimpleUri) {
      return _simpleMerge(this, reference);
    }
    return _toNonSimple().resolveUri(reference);
  }

  // Returns the index of the `/` after the package name of a package URI.
  //
  // Returns negative if the URI is not a valid package URI:
  // * Scheme must be "package".
  // * No authority.
  // * Path starts with "something"/
  // * where "something" is not all "." characters,
  // * and contains no escapes or colons.
  //
  // The characters are necessarily valid path characters.
  static int _packageNameEnd(_SimpleUri uri) {
    if (uri._isPackage && !uri.hasAuthority) {
      // Becomes Non zero if seeing any non-dot character.
      // Also guards against empty package names.
      return _skipPackageNameChars(uri._uri, uri._pathStart, uri._queryStart);
    }
    return -1;
  }

  // Merge two simple URIs. This should always result in a prefix of
  // one concatenated with a suffix of the other, possibly with a `/` in
  // the middle of two merged paths, which is again simple.
  // In a few cases, there might be a need for extra normalization, when
  // resolving on top of a known scheme.
  Uri _simpleMerge(_SimpleUri base, _SimpleUri ref) {
    if (ref.hasScheme) return ref;
    if (ref.hasAuthority) {
      if (!base.hasScheme) return ref;
      bool isSimple = true;
      if (base._isFile) {
        isSimple = !ref.hasEmptyPath;
      } else if (base._isHttp) {
        isSimple = !ref._isPort("80");
      } else if (base._isHttps) {
        isSimple = !ref._isPort("443");
      }
      if (isSimple) {
        var delta = base._schemeEnd + 1;
        var newUri =
            base._uri.substring(0, base._schemeEnd + 1) +
            ref._uri.substring(ref._schemeEnd + 1);
        return _SimpleUri(
          newUri,
          base._schemeEnd,
          ref._hostStart + delta,
          ref._portStart + delta,
          ref._pathStart + delta,
          ref._queryStart + delta,
          ref._fragmentStart + delta,
          base._schemeCache,
        );
      } else {
        // This will require normalization, so use the _Uri implementation.
        return _toNonSimple().resolveUri(ref);
      }
    }
    if (ref.hasEmptyPath) {
      if (ref.hasQuery) {
        int delta = base._queryStart - ref._queryStart;
        var newUri =
            base._uri.substring(0, base._queryStart) +
            ref._uri.substring(ref._queryStart);
        return _SimpleUri(
          newUri,
          base._schemeEnd,
          base._hostStart,
          base._portStart,
          base._pathStart,
          ref._queryStart + delta,
          ref._fragmentStart + delta,
          base._schemeCache,
        );
      }
      if (ref.hasFragment) {
        int delta = base._fragmentStart - ref._fragmentStart;
        var newUri =
            base._uri.substring(0, base._fragmentStart) +
            ref._uri.substring(ref._fragmentStart);
        return _SimpleUri(
          newUri,
          base._schemeEnd,
          base._hostStart,
          base._portStart,
          base._pathStart,
          base._queryStart,
          ref._fragmentStart + delta,
          base._schemeCache,
        );
      }
      return base.removeFragment();
    }
    if (ref.hasAbsolutePath) {
      int basePathStart = base._pathStart;
      int packageNameEnd = _packageNameEnd(this);
      if (packageNameEnd > 0) basePathStart = packageNameEnd;
      var delta = basePathStart - ref._pathStart;
      var newUri =
          base._uri.substring(0, basePathStart) +
          ref._uri.substring(ref._pathStart);
      return _SimpleUri(
        newUri,
        base._schemeEnd,
        base._hostStart,
        base._portStart,
        base._pathStart,
        ref._queryStart + delta,
        ref._fragmentStart + delta,
        base._schemeCache,
      );
    }
    if (base.hasEmptyPath && base.hasAuthority) {
      // ref has relative non-empty path.
      // Add a "/" in front, then leading "/../" segments are folded to "/".
      int refStart = ref._pathStart;
      while (ref._uri.startsWith("../", refStart)) {
        refStart += 3;
      }
      var delta = base._pathStart - refStart + 1;
      var newUri =
          "${base._uri.substring(0, base._pathStart)}/"
          "${ref._uri.substring(refStart)}";
      return _SimpleUri(
        newUri,
        base._schemeEnd,
        base._hostStart,
        base._portStart,
        base._pathStart,
        ref._queryStart + delta,
        ref._fragmentStart + delta,
        base._schemeCache,
      );
    }
    // Merge paths.

    // The RFC 3986 algorithm merges the base path without its final segment
    // (anything after the final "/", or everything if the base path doesn't
    // contain any "/"), and the reference path.
    // Then it removes "." and ".." segments using the remove-dot-segment
    // algorithm.
    // This code combines the two steps. It is simplified by knowing that
    // the base path contains no "." or ".." segments, and the reference
    // path can only contain leading ".." segments.

    String baseUri = base._uri;
    String refUri = ref._uri;
    int baseStart = base._pathStart;
    int baseEnd = base._queryStart;
    int packageNameEnd = _packageNameEnd(this);
    if (packageNameEnd >= 0) {
      baseStart = packageNameEnd; // At the `/` after the first package name.
    } else {
      while (baseUri.startsWith("../", baseStart)) baseStart += 3;
    }
    int refStart = ref._pathStart;
    int refEnd = ref._queryStart;

    /// Count of leading ".." segments in reference path.
    /// The count is decremented when the segment is matched with a
    /// segment of the base path, and both are then omitted from the result.
    int backCount = 0;

    /// Count "../" segments and advance `refStart` to after the segments.
    while (refStart + 3 <= refEnd && refUri.startsWith("../", refStart)) {
      refStart += 3;
      backCount += 1;
    }

    // Extra slash inserted between base and reference path parts if
    // the base path contains any slashes, or empty string if none.
    // (We could use a slash from the base path in most cases, but not if
    // we remove the entire base path).
    String insert = "";

    /// Remove segments from the base path.
    /// Start with the segment trailing the last slash,
    /// then remove segments for each leading "../" segment
    /// from the reference path, or as many of them as are available.
    while (baseEnd > baseStart) {
      baseEnd--;
      int char = baseUri.codeUnitAt(baseEnd);
      if (char == _SLASH) {
        insert = "/";
        if (backCount == 0) break;
        backCount--;
      }
    }

    if (baseEnd == baseStart && !base.hasScheme && !base.hasAbsolutePath) {
      // If the base is *just* a relative path (no scheme or authority),
      // then merging with another relative path doesn't follow the
      // RFC-3986 behavior.
      // Don't need to check `base.hasAuthority` since the base path is
      // non-empty - if there is an authority, a non-empty path is absolute.

      // We reached the start of the base path, and want to stay relative,
      // so don't insert a slash.
      insert = "";
      // If we reached the start of the base path with more "../" left over
      // in the reference path, include those segments in the result.
      refStart -= backCount * 3;
    }

    var delta = baseEnd - refStart + insert.length;
    var newUri =
        "${base._uri.substring(0, baseEnd)}$insert"
        "${ref._uri.substring(refStart)}";

    return _SimpleUri(
      newUri,
      base._schemeEnd,
      base._hostStart,
      base._portStart,
      base._pathStart,
      ref._queryStart + delta,
      ref._fragmentStart + delta,
      base._schemeCache,
    );
  }

  String toFilePath({bool? windows}) {
    if (_schemeEnd >= 0 && !_isFile) {
      throw UnsupportedError("Cannot extract a file path from a $scheme URI");
    }
    if (_queryStart < _uri.length) {
      if (_queryStart < _fragmentStart) {
        throw UnsupportedError(
          "Cannot extract a file path from a URI with a query component",
        );
      }
      throw UnsupportedError(
        "Cannot extract a file path from a URI with a fragment component",
      );
    }
    return (windows ?? _Uri._isWindows)
        ? _Uri._toWindowsFilePath(this)
        : _toFilePath();
  }

  String _toFilePath() {
    if (_hostStart < _portStart) {
      // Has authority and non-empty host.
      throw UnsupportedError(
        "Cannot extract a non-Windows file path from a file URI "
        "with an authority",
      );
    }
    return this.path;
  }

  UriData? get data {
    assert(scheme != "data");
    return null;
  }

  int get hashCode => _hashCodeCache ??= _uri.hashCode;

  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is Uri && _uri == other.toString();
  }

  Uri _toNonSimple() {
    return _Uri._internal(
      this.scheme,
      this.userInfo,
      this.hasAuthority ? this.host : null,
      this.hasPort ? this.port : null,
      this.path,
      this.hasQuery ? this.query : null,
      this.hasFragment ? this.fragment : null,
    );
  }

  String toString() => _uri;
}

/// Special [_Uri] created from an existing [UriData].
class _DataUri extends _Uri {
  final UriData _data;

  _DataUri(this._data, String path, String? query)
    : super._internal("data", "", null, null, path, query, null);

  UriData? get data => _data;
}

/// Checks whether [text] starts with "data:" at position [start].
///
/// The text must be long enough to allow reading five characters
/// from the [start] position.
///
/// Returns an integer value which is zero if text starts with all-lowercase
/// "data:" and 0x20 if the text starts with "data:" that isn't all lower-case.
/// All other values means the text starts with some other character.
int _startsWithData(String text, int start) {
  // Multiply by 3 to avoid a non-colon character making delta be 0x20.
  int delta = (text.codeUnitAt(start + 4) ^ _COLON) * 3;
  delta |= text.codeUnitAt(start) ^ 0x64 /*d*/;
  delta |= text.codeUnitAt(start + 1) ^ 0x61 /*a*/;
  delta |= text.codeUnitAt(start + 2) ^ 0x74 /*t*/;
  delta |= text.codeUnitAt(start + 3) ^ 0x61 /*a*/;
  return delta;
}

/// Helper function returning the length of a string, or `0` for `null`.
int _stringOrNullLength(String? s) => (s == null) ? 0 : s.length;

List<String> _toUnmodifiableStringList(String key, List<String> list) =>
    List<String>.unmodifiable(list);

/// Counts valid package name characters in [source].
///
/// If [source] starts at [start] with a valid package name,
/// followed by a `/`, no later than [end],
/// then the position of the `/` is returned.
/// If not, a negative value is returned.
/// (Assumes source characters are valid path characters.)
/// A name only consisting of `.` characters is not a valid
/// package name.
int _skipPackageNameChars(String source, int start, int end) {
  // Becomes non-zero when seeing a non-dot character.
  // Also guards against empty package names.
  var dots = 0;
  for (var i = start; i < end; i++) {
    var char = source.codeUnitAt(i);
    if (char == _SLASH) return (dots != 0) ? i : -1;
    if (char == _PERCENT || char == _COLON) return -1;
    dots |= char ^ _DOT;
  }
  return -1;
}

/// Whether [string] at [start] starts with  [prefix], ignoring case.
///
/// Returns whether [string] at offset [start]
/// starts with the characters of [prefix],
/// but ignores differences in the cases of ASCII letters,
/// so `a` and `A` are considered equal.
///
/// The [string] must be at least as long as [prefix].
///
/// When used to checks the schemes of URIs,
/// this function doesn't check that the characters are valid URI scheme
/// characters. The [string] is assumed to be a valid URI,
/// so if [prefix] matches it, it has to be valid too.
bool _caseInsensitiveStartsWith(String prefix, String string, int start) =>
    _caseInsensitiveCompareStart(prefix, string, start) >= 0;

/// Compares [string] at [start] with [prefix], ignoring case.
///
/// Returns 0 if [string] starts with [prefix] at offset [start].
/// Returns 0x20 if [string] starts with [prefix] at offset [start],
/// but some ASCII letters have different case.
/// Returns a negative value if [string] does not start with [prefix],
/// at offset [start] even ignoring case differences.
///
/// The [string] must be at least as long as `start + prefix.length`.
int _caseInsensitiveCompareStart(String prefix, String string, int start) {
  int result = 0;
  for (int i = 0; i < prefix.length; i++) {
    int prefixChar = prefix.codeUnitAt(i);
    int stringChar = string.codeUnitAt(start + i);
    int delta = prefixChar ^ stringChar;
    if (delta != 0) {
      if (delta == 0x20) {
        // Might be a case difference.
        int lowerChar = stringChar | delta;
        if (0x61 /*a*/ <= lowerChar && lowerChar <= 0x7a /*z*/ ) {
          result = 0x20;
          continue;
        }
      }
      return -1;
    }
  }
  return result;
}

/// Checks whether two strings are equal ignoring case differences.
///
/// Returns whether if [string1] and [string2] has the same length
/// and same characters, but ignores the cases of ASCII letters,
/// so `a` and `A` are considered equal.
bool _caseInsensitiveEquals(String string1, String string2) =>
    string1.length == string2.length &&
    _caseInsensitiveStartsWith(string1, string2, 0);

// --- 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 ---
