// Copyright (c) 2013, 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._http; final _digitsValidator = RegExp(r"^\d+$"); class _HttpHeaders implements HttpHeaders { final Map> _headers; // The original header names keyed by the lowercase header names. Map? _originalHeaderNames; final String protocolVersion; bool _mutable = true; // Are the headers currently mutable? List? _noFoldingHeaders; int _contentLength = -1; bool _persistentConnection = true; bool _chunkedTransferEncoding = false; String? _host; int? _port; final int _defaultPortForScheme; _HttpHeaders( this.protocolVersion, { int defaultPortForScheme = HttpClient.defaultHttpPort, _HttpHeaders? initialHeaders, }) : _headers = HashMap>(), _defaultPortForScheme = defaultPortForScheme { if (initialHeaders != null) { initialHeaders._headers.forEach((name, value) => _headers[name] = value); _contentLength = initialHeaders._contentLength; _persistentConnection = initialHeaders._persistentConnection; _chunkedTransferEncoding = initialHeaders._chunkedTransferEncoding; _host = initialHeaders._host; _port = initialHeaders._port; } if (protocolVersion == "1.0") { _persistentConnection = false; _chunkedTransferEncoding = false; } } List? operator [](String name) => _headers[_validateField(name)]; String? value(String name) { name = _validateField(name); List? values = _headers[name]; if (values == null) return null; assert(values.isNotEmpty); if (values.length > 1) { throw HttpException("More than one value for header $name"); } return values[0]; } void add(String name, value, {bool preserveHeaderCase = false}) { _checkMutable(); String lowercaseName = _validateField(name); if (preserveHeaderCase && name != lowercaseName) { (_originalHeaderNames ??= {})[lowercaseName] = name; } else { _originalHeaderNames?.remove(lowercaseName); } _addAll(lowercaseName, value); } void _addAll(String name, value) { if (value is Iterable) { for (var v in value) { _add(name, _validateValue(v)); } } else { _add(name, _validateValue(value)); } } void set(String name, Object value, {bool preserveHeaderCase = false}) { _checkMutable(); String lowercaseName = _validateField(name); _headers.remove(lowercaseName); _originalHeaderNames?.remove(lowercaseName); if (lowercaseName == HttpHeaders.contentLengthHeader) { _contentLength = -1; } if (lowercaseName == HttpHeaders.transferEncodingHeader) { _chunkedTransferEncoding = false; } if (preserveHeaderCase && name != lowercaseName) { (_originalHeaderNames ??= {})[lowercaseName] = name; } _addAll(lowercaseName, value); } void remove(String name, Object value) { _checkMutable(); name = _validateField(name); value = _validateValue(value); List? values = _headers[name]; if (values != null) { values.remove(_valueToString(value)); if (values.isEmpty) { _headers.remove(name); _originalHeaderNames?.remove(name); } } if (name == HttpHeaders.transferEncodingHeader && value == "chunked") { _chunkedTransferEncoding = false; } } void removeAll(String name) { _checkMutable(); name = _validateField(name); _headers.remove(name); _originalHeaderNames?.remove(name); } void forEach(void Function(String name, List values) action) { _headers.forEach((String name, List values) { String originalName = _originalHeaderName(name); action(originalName, values); }); } void noFolding(String name) { name = _validateField(name); (_noFoldingHeaders ??= []).add(name); } bool get persistentConnection => _persistentConnection; void set persistentConnection(bool persistentConnection) { _checkMutable(); if (persistentConnection == _persistentConnection) return; final originalName = _originalHeaderName(HttpHeaders.connectionHeader); if (persistentConnection) { if (protocolVersion == "1.1") { remove(HttpHeaders.connectionHeader, "close"); } else { if (_contentLength < 0) { throw HttpException( "Trying to set 'Connection: Keep-Alive' on HTTP 1.0 headers with " "no ContentLength", ); } add(originalName, "keep-alive", preserveHeaderCase: true); } } else { if (protocolVersion == "1.1") { add(originalName, "close", preserveHeaderCase: true); } else { remove(HttpHeaders.connectionHeader, "keep-alive"); } } _persistentConnection = persistentConnection; } int get contentLength => _contentLength; void set contentLength(int contentLength) { _checkMutable(); if (protocolVersion == "1.0" && persistentConnection && contentLength == -1) { throw HttpException( "Trying to clear ContentLength on HTTP 1.0 headers with " "'Connection: Keep-Alive' set", ); } if (_contentLength == contentLength) return; _contentLength = contentLength; if (_contentLength >= 0) { if (chunkedTransferEncoding) chunkedTransferEncoding = false; _set(HttpHeaders.contentLengthHeader, contentLength.toString()); } else { _headers.remove(HttpHeaders.contentLengthHeader); if (protocolVersion == "1.1") { chunkedTransferEncoding = true; } } } bool get chunkedTransferEncoding => _chunkedTransferEncoding; void set chunkedTransferEncoding(bool chunkedTransferEncoding) { _checkMutable(); if (chunkedTransferEncoding && protocolVersion == "1.0") { throw HttpException( "Trying to set 'Transfer-Encoding: Chunked' on HTTP 1.0 headers", ); } if (chunkedTransferEncoding == _chunkedTransferEncoding) return; if (chunkedTransferEncoding) { List? values = _headers[HttpHeaders.transferEncodingHeader]; if (values == null || !values.contains("chunked")) { // Headers does not specify chunked encoding - add it if set. _addValue(HttpHeaders.transferEncodingHeader, "chunked"); } contentLength = -1; } else { // Headers does specify chunked encoding - remove it if not set. remove(HttpHeaders.transferEncodingHeader, "chunked"); } _chunkedTransferEncoding = chunkedTransferEncoding; } String? get host => _host; void set host(String? host) { _checkMutable(); _host = host; _updateHostHeader(); } int? get port => _port; void set port(int? port) { _checkMutable(); _port = port; _updateHostHeader(); } DateTime? get ifModifiedSince { List? values = _headers[HttpHeaders.ifModifiedSinceHeader]; if (values != null) { assert(values.isNotEmpty); try { return HttpDate.parse(values[0]); } on Exception { return null; } } return null; } void set ifModifiedSince(DateTime? ifModifiedSince) { _checkMutable(); if (ifModifiedSince == null) { _headers.remove(HttpHeaders.ifModifiedSinceHeader); } else { // Format "ifModifiedSince" header with date in Greenwich Mean Time (GMT). String formatted = HttpDate.format(ifModifiedSince.toUtc()); _set(HttpHeaders.ifModifiedSinceHeader, formatted); } } DateTime? get date { List? values = _headers[HttpHeaders.dateHeader]; if (values != null) { assert(values.isNotEmpty); try { return HttpDate.parse(values[0]); } on Exception { return null; } } return null; } void set date(DateTime? date) { _checkMutable(); if (date == null) { _headers.remove(HttpHeaders.dateHeader); } else { // Format "DateTime" header with date in Greenwich Mean Time (GMT). String formatted = HttpDate.format(date.toUtc()); _set(HttpHeaders.dateHeader, formatted); } } DateTime? get expires { List? values = _headers[HttpHeaders.expiresHeader]; if (values != null) { assert(values.isNotEmpty); try { return HttpDate.parse(values[0]); } on Exception { return null; } } return null; } void set expires(DateTime? expires) { _checkMutable(); if (expires == null) { _headers.remove(HttpHeaders.expiresHeader); } else { // Format "Expires" header with date in Greenwich Mean Time (GMT). String formatted = HttpDate.format(expires.toUtc()); _set(HttpHeaders.expiresHeader, formatted); } } ContentType? get contentType { var values = _headers[HttpHeaders.contentTypeHeader]; if (values != null) { return ContentType.parse(values[0]); } else { return null; } } void set contentType(ContentType? contentType) { _checkMutable(); if (contentType == null) { _headers.remove(HttpHeaders.contentTypeHeader); } else { _set(HttpHeaders.contentTypeHeader, contentType.toString()); } } void clear() { _checkMutable(); _headers.clear(); _contentLength = -1; _persistentConnection = true; _chunkedTransferEncoding = false; _host = null; _port = null; } // [name] must be a lower-case version of the name. void _add(String name, value) { assert(name == _validateField(name)); // Use the length as index on what method to call. This is notable // faster than computing hash and looking up in a hash-map. switch (name.length) { case 4: if (HttpHeaders.dateHeader == name) { _addDate(name, value); return; } if (HttpHeaders.hostHeader == name) { _addHost(name, value); return; } break; case 7: if (HttpHeaders.expiresHeader == name) { _addExpires(name, value); return; } break; case 10: if (HttpHeaders.connectionHeader == name) { _addConnection(name, value); return; } break; case 12: if (HttpHeaders.contentTypeHeader == name) { _addContentType(name, value); return; } break; case 14: if (HttpHeaders.contentLengthHeader == name) { _addContentLength(name, value); return; } break; case 17: if (HttpHeaders.transferEncodingHeader == name) { _addTransferEncoding(name, value); return; } if (HttpHeaders.ifModifiedSinceHeader == name) { _addIfModifiedSince(name, value); return; } } _addValue(name, value); } void _addContentLength(String name, value) { if (value is int) { if (value < 0) { throw HttpException("Content-Length must contain only digits"); } } else if (value is String) { if (!_digitsValidator.hasMatch(value)) { throw HttpException("Content-Length must contain only digits"); } value = int.parse(value); } else { throw HttpException("Unexpected type for header named $name"); } contentLength = value; } void _addTransferEncoding(String name, value) { if (value == "chunked") { chunkedTransferEncoding = true; } else { _addValue(HttpHeaders.transferEncodingHeader, value); } } void _addDate(String name, value) { if (value is DateTime) { date = value; } else if (value is String) { _set(HttpHeaders.dateHeader, value); } else { throw HttpException("Unexpected type for header named $name"); } } void _addExpires(String name, value) { if (value is DateTime) { expires = value; } else if (value is String) { _set(HttpHeaders.expiresHeader, value); } else { throw HttpException("Unexpected type for header named $name"); } } void _addIfModifiedSince(String name, value) { if (value is DateTime) { ifModifiedSince = value; } else if (value is String) { _set(HttpHeaders.ifModifiedSinceHeader, value); } else { throw HttpException("Unexpected type for header named $name"); } } void _addHost(String name, value) { if (value is String) { // value.indexOf will only work for ipv4, ipv6 which has multiple : in its // host part needs lastIndexOf int pos = value.lastIndexOf(":"); // According to RFC 3986, section 3.2.2, host part of ipv6 address must be // enclosed by square brackets. // https://serverfault.com/questions/205793/how-can-one-distinguish-the-host-and-the-port-in-an-ipv6-url if (pos == -1 || value.startsWith("[") && value.endsWith("]")) { _host = value; _port = HttpClient.defaultHttpPort; } else { if (pos > 0) { _host = value.substring(0, pos); } else { _host = null; } if (pos + 1 == value.length) { _port = HttpClient.defaultHttpPort; } else { try { _port = int.parse(value.substring(pos + 1)); } on FormatException { _port = null; } } } _set(HttpHeaders.hostHeader, value); } else { throw HttpException("Unexpected type for header named $name"); } } void _addConnection(String name, String value) { var lowerCaseValue = value.toLowerCase(); if (lowerCaseValue == 'close') { _persistentConnection = false; } else if (lowerCaseValue == 'keep-alive') { _persistentConnection = true; } _addValue(name, value); } void _addContentType(String name, value) { _set(HttpHeaders.contentTypeHeader, value); } void _addValue(String name, Object value) { List values = (_headers[name] ??= []); values.add(_valueToString(value)); } String _valueToString(Object value) { if (value is DateTime) { return HttpDate.format(value); } else if (value is String) { return value; // TODO(39784): no _validateValue? } else { return _validateValue(value.toString()) as String; } } void _set(String name, String value) { assert(name == _validateField(name)); _headers[name] = [value]; } void _checkMutable() { if (!_mutable) throw HttpException("HTTP headers are not mutable"); } void _updateHostHeader() { var host = _host; if (host != null) { bool defaultPort = _port == null || _port == _defaultPortForScheme; _set("host", defaultPort ? host : "$host:$_port"); } } bool _foldHeader(String name) { if (name == HttpHeaders.setCookieHeader) return false; var noFoldingHeaders = _noFoldingHeaders; return noFoldingHeaders == null || !noFoldingHeaders.contains(name); } void _finalize() { _mutable = false; } void _build(BytesBuilder builder, {bool skipZeroContentLength = false}) { // per https://tools.ietf.org/html/rfc7230#section-3.3.2 // A user agent SHOULD NOT send a // Content-Length header field when the request message does not // contain a payload body and the method semantics do not anticipate // such a body. String? ignoreHeader = _contentLength == 0 && skipZeroContentLength ? HttpHeaders.contentLengthHeader : null; _headers.forEach((String name, List values) { if (ignoreHeader == name) { return; } String originalName = _originalHeaderName(name); bool fold = _foldHeader(name); var nameData = originalName.codeUnits; builder.add(nameData); builder.addByte(_CharCode.COLON); builder.addByte(_CharCode.SP); for (int i = 0; i < values.length; i++) { if (i > 0) { if (fold) { builder.addByte(_CharCode.COMMA); builder.addByte(_CharCode.SP); } else { builder.addByte(_CharCode.CR); builder.addByte(_CharCode.LF); builder.add(nameData); builder.addByte(_CharCode.COLON); builder.addByte(_CharCode.SP); } } builder.add(values[i].codeUnits); } builder.addByte(_CharCode.CR); builder.addByte(_CharCode.LF); }); } String toString() { StringBuffer sb = StringBuffer(); _headers.forEach((String name, List values) { String originalName = _originalHeaderName(name); sb ..write(originalName) ..write(": "); bool fold = _foldHeader(name); for (int i = 0; i < values.length; i++) { if (i > 0) { if (fold) { sb.write(", "); } else { sb ..write("\n") ..write(originalName) ..write(": "); } } sb.write(values[i]); } sb.write("\n"); }); return sb.toString(); } List _parseCookies() { // Parse a Cookie header value according to the rules in RFC 6265. var cookies = []; void parseCookieString(String s) { int index = 0; bool done() => index == -1 || index == s.length; void skipWS() { while (!done()) { if (s[index] != " " && s[index] != "\t") return; index++; } } String parseName() { int start = index; while (!done()) { if (s[index] == " " || s[index] == "\t" || s[index] == "=") break; index++; } return s.substring(start, index); } String parseValue() { int start = index; while (!done()) { if (s[index] == " " || s[index] == "\t" || s[index] == ";") break; index++; } return s.substring(start, index); } bool expect(String expected) { if (done()) return false; if (s[index] != expected) return false; index++; return true; } while (!done()) { skipWS(); if (done()) return; String name = parseName(); skipWS(); if (!expect("=")) { index = s.indexOf(';', index); continue; } skipWS(); String value = parseValue(); try { cookies.add(_Cookie(name, value)); } catch (_) { // Skip it, invalid cookie data. } skipWS(); if (done()) return; if (!expect(";")) { index = s.indexOf(';', index); continue; } } } List? values = _headers[HttpHeaders.cookieHeader]; if (values != null) { for (var headerValue in values) { parseCookieString(headerValue); } } return cookies; } static String _validateField(String field) { for (var i = 0; i < field.length; i++) { if (!_HttpParser._isTokenChar(field.codeUnitAt(i))) { throw FormatException( "Invalid HTTP header field name: ${json.encode(field)}", field, i, ); } } return field.toLowerCase(); } static Object _validateValue(Object value) { if (value is! String) return value; for (var i = 0; i < (value).length; i++) { if (!_HttpParser._isValueChar((value).codeUnitAt(i))) { throw FormatException( "Invalid HTTP header field value: ${json.encode(value)}", value, i, ); } } return value; } String _originalHeaderName(String name) { return _originalHeaderNames?[name] ?? name; } } class _HeaderValue implements HeaderValue { String _value; Map? _parameters; Map? _unmodifiableParameters; _HeaderValue([this._value = "", Map parameters = const {}]) { // TODO(40614): Remove once non-nullability is sound. Map? nullableParameters = parameters; if (nullableParameters != null && nullableParameters.isNotEmpty) { _parameters = HashMap.from(nullableParameters); } } static _HeaderValue parse( String value, { String parameterSeparator = ";", String? valueSeparator, bool preserveBackslash = false, }) { // Parse the string. var result = _HeaderValue(); result._parse(value, parameterSeparator, valueSeparator, preserveBackslash); return result; } String get value => _value; Map _ensureParameters() => _parameters ??= {}; Map get parameters => _unmodifiableParameters ??= UnmodifiableMapView(_ensureParameters()); static bool _isToken(String token) { if (token.isEmpty) { return false; } final delimiters = "\"(),/:;<=>?@[]{}"; for (int i = 0; i < token.length; i++) { int codeUnit = token.codeUnitAt(i); if (codeUnit <= 32 || codeUnit >= 127 || delimiters.contains(token[i])) { return false; } } return true; } String toString() { StringBuffer sb = StringBuffer(); sb.write(_value); var parameters = _parameters; if (parameters != null && parameters.isNotEmpty) { parameters.forEach((String name, String? value) { sb ..write("; ") ..write(name); if (value != null) { sb.write("="); if (_isToken(value)) { sb.write(value); } else { sb.write('"'); int start = 0; for (int i = 0; i < value.length; i++) { // Can use codeUnitAt here instead. int codeUnit = value.codeUnitAt(i); if (codeUnit == 92 /* backslash */ || codeUnit == 34 /* double quote */ ) { sb.write(value.substring(start, i)); sb.write(r'\'); start = i; } } sb ..write(value.substring(start)) ..write('"'); } } }); } return sb.toString(); } void _parse( String s, String parameterSeparator, String? valueSeparator, bool preserveBackslash, ) { int index = 0; bool done() => index == s.length; void skipWS() { while (!done()) { if (s[index] != " " && s[index] != "\t") return; index++; } } String parseValue() { int start = index; while (!done()) { var char = s[index]; if (char == " " || char == "\t" || char == valueSeparator || char == parameterSeparator) break; index++; } return s.substring(start, index); } void expect(String expected) { if (done() || s[index] != expected) { throw HttpException("Failed to parse header value"); } index++; } bool maybeExpect(String expected) { if (done() || !s.startsWith(expected, index)) { return false; } index++; return true; } void parseParameters() { var parameters = _ensureParameters(); String parseParameterName() { int start = index; while (!done()) { var char = s[index]; if (char == " " || char == "\t" || char == "=" || char == parameterSeparator || char == valueSeparator) break; index++; } return s.substring(start, index).toLowerCase(); } String parseParameterValue() { if (!done() && s[index] == "\"") { // Parse quoted value. StringBuffer sb = StringBuffer(); index++; while (!done()) { var char = s[index]; if (char == "\\") { if (index + 1 == s.length) { throw HttpException("Failed to parse header value"); } if (preserveBackslash && s[index + 1] != "\"") { sb.write(char); } index++; } else if (char == "\"") { index++; return sb.toString(); } char = s[index]; sb.write(char); index++; } throw HttpException("Failed to parse header value"); } else { // Parse non-quoted value. return parseValue(); } } while (!done()) { skipWS(); if (done()) return; String name = parseParameterName(); skipWS(); if (maybeExpect("=")) { skipWS(); String value = parseParameterValue(); if (name == 'charset' && this is _ContentType) { // Charset parameter of ContentTypes are always lower-case. value = value.toLowerCase(); } parameters[name] = value; skipWS(); } else if (name.isNotEmpty) { parameters[name] = null; } if (done()) return; // TODO: Implement support for multi-valued parameters. if (s[index] == valueSeparator) return; expect(parameterSeparator); } } skipWS(); _value = parseValue(); skipWS(); if (done()) return; if (s[index] == valueSeparator) return; maybeExpect(parameterSeparator); parseParameters(); } } class _ContentType extends _HeaderValue implements ContentType { String _primaryType = ""; String _subType = ""; _ContentType( String primaryType, String subType, String? charset, Map parameters, ) : _primaryType = primaryType, _subType = subType, super("") { // TODO(40614): Remove once non-nullability is sound. String emptyIfNull(String? string) => string ?? ""; _primaryType = emptyIfNull(_primaryType); _subType = emptyIfNull(_subType); _value = "$_primaryType/$_subType"; // TODO(40614): Remove once non-nullability is sound. Map? nullableParameters = parameters; if (nullableParameters != null) { var parameterMap = _ensureParameters(); nullableParameters.forEach((String key, String? value) { String lowerCaseKey = key.toLowerCase(); if (lowerCaseKey == "charset") { value = value?.toLowerCase(); } parameterMap[lowerCaseKey] = value; }); } if (charset != null) { _ensureParameters()["charset"] = charset.toLowerCase(); } } _ContentType._(); static _ContentType parse(String value) { var result = _ContentType._(); result._parse(value, ";", null, false); int index = result._value.indexOf("/"); if (index == -1 || index == (result._value.length - 1)) { result._primaryType = result._value.trim().toLowerCase(); } else { result._primaryType = result._value.substring(0, index).trim().toLowerCase(); result._subType = result._value.substring(index + 1).trim().toLowerCase(); } return result; } String get mimeType => '$primaryType/$subType'; String get primaryType => _primaryType; String get subType => _subType; String? get charset => parameters["charset"]; } class _Cookie implements Cookie { String _name; String _value; DateTime? expires; int? maxAge; String? domain; String? _path; bool httpOnly = false; bool secure = false; SameSite? sameSite; _Cookie(String name, String value) : _name = _validateName(name), _value = _validateValue(value), httpOnly = true; String get name => _name; String get value => _value; String? get path => _path; set path(String? newPath) { _validatePath(newPath); _path = newPath; } set name(String newName) { _validateName(newName); _name = newName; } set value(String newValue) { _validateValue(newValue); _value = newValue; } _Cookie.fromSetCookieValue(String value) : _name = "", _value = "" { // Parse the 'set-cookie' header value. _parseSetCookieValue(value); } // Parse a 'set-cookie' header value according to the rules in RFC 6265. void _parseSetCookieValue(String s) { int index = 0; bool done() => index == s.length; String parseName() { int start = index; while (!done()) { if (s[index] == "=") break; index++; } return s.substring(start, index).trim(); } String parseValue() { int start = index; while (!done()) { if (s[index] == ";") break; index++; } return s.substring(start, index).trim(); } void parseAttributes() { String parseAttributeName() { int start = index; while (!done()) { if (s[index] == "=" || s[index] == ";") break; index++; } return s.substring(start, index).trim().toLowerCase(); } String parseAttributeValue() { int start = index; while (!done()) { if (s[index] == ";") break; index++; } return s.substring(start, index).trim().toLowerCase(); } while (!done()) { String name = parseAttributeName(); String value = ""; if (!done() && s[index] == "=") { index++; // Skip the = character. value = parseAttributeValue(); } if (name == "expires") { expires = HttpDate._parseCookieDate(value); } else if (name == "max-age") { maxAge = int.parse(value); } else if (name == "domain") { domain = value; } else if (name == "path") { path = value; } else if (name == "httponly") { httpOnly = true; } else if (name == "secure") { secure = true; } else if (name == "samesite") { sameSite = switch (value) { "lax" => SameSite.lax, "none" => SameSite.none, "strict" => SameSite.strict, _ => throw HttpException( 'SameSite value should be one of Lax, Strict or None.', ), }; } if (!done()) index++; // Skip the ; character } } _name = _validateName(parseName()); if (done() || _name.isEmpty) { throw HttpException("Failed to parse header value [$s]"); } index++; // Skip the = character. _value = _validateValue(parseValue()); if (done()) return; index++; // Skip the ; character. parseAttributes(); } String toString() { StringBuffer sb = StringBuffer(); sb ..write(_name) ..write("=") ..write(_value); var expires = this.expires; if (expires != null) { sb ..write("; Expires=") ..write(HttpDate.format(expires)); } if (maxAge != null) { sb ..write("; Max-Age=") ..write(maxAge); } if (domain != null) { sb ..write("; Domain=") ..write(domain); } if (path != null) { sb ..write("; Path=") ..write(path); } if (secure) sb.write("; Secure"); if (httpOnly) sb.write("; HttpOnly"); if (sameSite != null) sb.write("; $sameSite"); return sb.toString(); } static String _validateName(String newName) { const separators = [ "(", ")", "<", ">", "@", ",", ";", ":", "\\", '"', "/", "[", "]", "?", "=", "{", "}", ]; if (newName == null) throw ArgumentError.notNull("name"); for (int i = 0; i < newName.length; i++) { int codeUnit = newName.codeUnitAt(i); if (codeUnit <= 32 || codeUnit >= 127 || separators.contains(newName[i])) { throw FormatException( "Invalid character in cookie name, code unit: '$codeUnit'", newName, i, ); } } return newName; } static String _validateValue(String newValue) { if (newValue == null) throw ArgumentError.notNull("value"); // Per RFC 6265, consider surrounding "" as part of the value, but otherwise // double quotes are not allowed. int start = 0; int end = newValue.length; if (2 <= newValue.length && newValue.codeUnits[start] == 0x22 && newValue.codeUnits[end - 1] == 0x22) { start++; end--; } for (int i = start; i < end; i++) { int codeUnit = newValue.codeUnits[i]; if (!(codeUnit == 0x21 || (codeUnit >= 0x23 && codeUnit <= 0x2B) || (codeUnit >= 0x2D && codeUnit <= 0x3A) || (codeUnit >= 0x3C && codeUnit <= 0x5B) || (codeUnit >= 0x5D && codeUnit <= 0x7E))) { throw FormatException( "Invalid character in cookie value, code unit: '$codeUnit'", newValue, i, ); } } return newValue; } static void _validatePath(String? path) { if (path == null) return; for (int i = 0; i < path.length; i++) { int codeUnit = path.codeUnitAt(i); // According to RFC 6265, semicolon and controls should not occur in the // path. // path-value = // CTLs = %x00-1F / %x7F if (codeUnit < 0x20 || codeUnit >= 0x7f || codeUnit == 0x3b /*;*/ ) { throw FormatException( "Invalid character in cookie path, code unit: '$codeUnit'", ); } } } }