From 96cf889e6b7bd9e596cbd08849c5351a38eec097 Mon Sep 17 00:00:00 2001 From: Jonas Termansen Date: Wed, 18 Mar 2020 11:36:36 +0000 Subject: [PATCH] [dart:io] Fix HeaderValue parsing, toString(), and support null values. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a breaking change. https://github.com/dart-lang/sdk/issues/40709 This change makes the HeaderValue parsing more strict in two invalid edge cases, supports parameters with null values as a feature, and fixes toString() so it always produces tokens or quoted-strings valid per RFC 7230 3.2.6. The empty parameter value without double quotes (which is not allowed by the standards) is now parsed as the empty string rather than null. E.g. HeaderValue.parse("v;a=").parameters now gives {"a": ""} rather than {"a": null}. Invalid inputs with unbalanced double quotes are now rejected. E.g. HeaderValue.parse('v;a="b').parameters will now throw a HttpException instead of giving {"a": "b"}. The HeaderValue.toString() method now supports parameters with null values by omitting the value. E.g.: HeaderValue("v", {"a": null, "b": "c"}).toString() now gives v; a; b=c This behavior can be used to implement some features in the Accept and Sec-WebSocket-Extensions headers. Likewise the empty value and values using characters outside of RFC 7230 3.2.6 tokens are now correctly implemented by double quoting such values with escape sequences. E.g.: HeaderValue("v", {"a": "A", "b": "(B)", "c": "", "d": "ø", "e": "\\\""}).toString() now gives v;a=A;b="(B)";c="";d="ø";e="\\\"" The NNBD migration required making subtle changes to some dart:io semantics in order to provide a better API. This change backports one of these semantic changes to the unmigrated SDK so any issues can be discovered now instead of blocking the future SDK unfork. Change-Id: Iafc790e03b6290232cac71fe14f995ce0f0b036b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/136620 Commit-Queue: Jonas Termansen Reviewed-by: Lasse R.H. Nielsen --- CHANGELOG.md | 31 +++++ .../tool/dart2js_nnbd_sdk_error_golden.txt | 12 +- .../tool/dartdevc_nnbd_sdk_error_golden.txt | 12 +- sdk/lib/_http/http.dart | 10 +- sdk/lib/_http/http_headers.dart | 95 ++++++++------ sdk_nnbd/lib/_http/http.dart | 12 +- sdk_nnbd/lib/_http/http_headers.dart | 117 ++++++++++++------ tests/standalone/io/http_headers_test.dart | 39 +++++- tests/standalone_2/io/http_headers_test.dart | 40 +++++- 9 files changed, 275 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 107029d2174..84b59a1e0e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,37 @@ used (see Issue [39627][]). now contains Unix epoch timestamps instead of `null` for the `accessed`, `changed`, and `modified` getters. +* **Breaking change** [#40709](https://github.com/dart-lang/sdk/issues/40709): + The `HeaderValue` class now parses more strictly in two invalid edge cases. + This is the class used to parse the semicolon delimited parameters used in the + `Accept`, `Authorization`, `Content-Type`, and other such HTTP headers. + + The empty parameter value without double quotes (which is not allowed by the + standards) is now parsed as the empty string rather than `null`. E.g. + `HeaderValue.parse("v;a=").parameters` now gives `{"a": ""}` rather than + `{"a": null}`. + + Invalid inputs with unbalanced double quotes are now rejected. E.g. + `HeaderValue.parse('v;a="b').parameters` will now throw a `HttpException` + instead of giving `{"a": "b"}`. + +* The `HeaderValue.toString()` method now supports parameters with `null` values + by omitting the value. `HeaderValue("v", {"a": null, "b": "c"}).toString()` + now gives `v; a; b=c`. This behavior can be used to implement some features in + the `Accept` and `Sec-WebSocket-Extensions` headers. + + Likewise the empty value and values using characters outside of + [RFC 7230 tokens](https://tools.ietf.org/html/rfc7230#section-3.2.6) are now + correctly implemented by double quoting such values with escape sequences. + E.g: + + ```dart + HeaderValue("v", + {"a": "A", "b": "(B)", "c": "", "d": "ø", "e": "\\\""}).toString() + ``` + + now gives `v;a=A;b="(B)";c="";d="ø";e="\\\""`. + #### `dart:mirrors` * Added `MirrorSystem.neverType`. diff --git a/pkg/dev_compiler/tool/dart2js_nnbd_sdk_error_golden.txt b/pkg/dev_compiler/tool/dart2js_nnbd_sdk_error_golden.txt index 1a63f963c32..8c561732023 100644 --- a/pkg/dev_compiler/tool/dart2js_nnbd_sdk_error_golden.txt +++ b/pkg/dev_compiler/tool/dart2js_nnbd_sdk_error_golden.txt @@ -6,13 +6,13 @@ ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/i ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1637|7|5|Superinterfaces don't have a valid override for '>>': int.>> (int Function(int)), JSNumber.>> (num Function(num)). ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1637|7|5|Superinterfaces don't have a valid override for '\|': int.\| (int Function(int)), JSNumber.\| (num Function(num)). ERROR|COMPILE_TIME_ERROR|INCONSISTENT_INHERITANCE|lib/_internal/js_runtime/lib/interceptors.dart|1637|7|5|Superinterfaces don't have a valid override for '^': int.^ (int Function(int)), JSNumber.^ (num Function(num)). -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1004|10|5|Non-nullable instance field 'value' must be initialized. -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1030|8|6|Non-nullable instance field 'secure' must be initialized. -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1036|8|8|Non-nullable instance field 'httpOnly' must be initialized. -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1493|12|11|Non-nullable instance field 'idleTimeout' must be initialized. -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1543|8|14|Non-nullable instance field 'autoUncompress' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1010|10|5|Non-nullable instance field 'value' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1036|8|6|Non-nullable instance field 'secure' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1042|8|8|Non-nullable instance field 'httpOnly' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1499|12|11|Non-nullable instance field 'idleTimeout' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1549|8|14|Non-nullable instance field 'autoUncompress' must be initialized. ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|172|8|12|Non-nullable instance field 'autoCompress' must be initialized. -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|991|10|4|Non-nullable instance field 'name' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|997|10|4|Non-nullable instance field 'name' must be initialized. ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/io/io.dart|5589|12|8|Non-nullable instance field 'encoding' must be initialized. ERROR|STATIC_TYPE_WARNING|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1654|28|1|The operator '&' isn't defined for the type 'JSInt'. ERROR|STATIC_TYPE_WARNING|UNDEFINED_OPERATOR|lib/_internal/js_runtime/lib/interceptors.dart|1656|27|1|The operator '&' isn't defined for the type 'JSInt'. diff --git a/pkg/dev_compiler/tool/dartdevc_nnbd_sdk_error_golden.txt b/pkg/dev_compiler/tool/dartdevc_nnbd_sdk_error_golden.txt index 6dad26d707b..6aafce79c7b 100644 --- a/pkg/dev_compiler/tool/dartdevc_nnbd_sdk_error_golden.txt +++ b/pkg/dev_compiler/tool/dartdevc_nnbd_sdk_error_golden.txt @@ -15,11 +15,11 @@ ERROR|COMPILE_TIME_ERROR|BODY_MIGHT_COMPLETE_NORMALLY|lib/_internal/js_dev_runti ERROR|COMPILE_TIME_ERROR|BODY_MIGHT_COMPLETE_NORMALLY|lib/_internal/js_dev_runtime/private/foreign_helper.dart|221|8|37|The body might complete normally, which would cause 'null' to be returned, but the return type is a potentially non-nullable type. ERROR|COMPILE_TIME_ERROR|BODY_MIGHT_COMPLETE_NORMALLY|lib/_internal/js_dev_runtime/private/foreign_helper.dart|224|8|11|The body might complete normally, which would cause 'null' to be returned, but the return type is a potentially non-nullable type. ERROR|COMPILE_TIME_ERROR|BODY_MIGHT_COMPLETE_NORMALLY|lib/_internal/js_dev_runtime/private/foreign_helper.dart|228|6|11|The body might complete normally, which would cause 'null' to be returned, but the return type is a potentially non-nullable type. -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1004|10|5|Non-nullable instance field 'value' must be initialized. -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1030|8|6|Non-nullable instance field 'secure' must be initialized. -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1036|8|8|Non-nullable instance field 'httpOnly' must be initialized. -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1493|12|11|Non-nullable instance field 'idleTimeout' must be initialized. -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1543|8|14|Non-nullable instance field 'autoUncompress' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1010|10|5|Non-nullable instance field 'value' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1036|8|6|Non-nullable instance field 'secure' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1042|8|8|Non-nullable instance field 'httpOnly' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1499|12|11|Non-nullable instance field 'idleTimeout' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|1549|8|14|Non-nullable instance field 'autoUncompress' must be initialized. ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|172|8|12|Non-nullable instance field 'autoCompress' must be initialized. -ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|991|10|4|Non-nullable instance field 'name' must be initialized. +ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/_http/http.dart|997|10|4|Non-nullable instance field 'name' must be initialized. ERROR|COMPILE_TIME_ERROR|NOT_INITIALIZED_NON_NULLABLE_INSTANCE_FIELD|lib/io/io.dart|5589|12|8|Non-nullable instance field 'encoding' must be initialized. diff --git a/sdk/lib/_http/http.dart b/sdk/lib/_http/http.dart index f841bc58204..6883fbcc21c 100644 --- a/sdk/lib/_http/http.dart +++ b/sdk/lib/_http/http.dart @@ -753,6 +753,11 @@ abstract class HttpHeaders { * [HeaderValue] can be used to conveniently build and parse header * values on this form. * + * Parameter values can be omitted, in which case the value is parsed as `null`. + * Values can be doubled quoted to allow characters outside of the RFC 7230 + * token characters and backslash sequences can be used to represent the double + * quote and backslash characters themselves. + * * To build an [:accepts:] header with the value * * text/plain; q=0.3, text/html @@ -779,7 +784,8 @@ abstract class HeaderValue { /** * Creates a new header value object setting the value and parameters. */ - factory HeaderValue([String value = "", Map parameters]) { + factory HeaderValue( + [String value = "", Map parameters = const {}]) { return new _HeaderValue(value, parameters); } @@ -892,7 +898,7 @@ abstract class ContentType implements HeaderValue { * or in `parameters`, will have its value converted to lower-case. */ factory ContentType(String primaryType, String subType, - {String charset, Map parameters}) { + {String charset, Map parameters = const {}}) { return new _ContentType(primaryType, subType, charset, parameters); } diff --git a/sdk/lib/_http/http_headers.dart b/sdk/lib/_http/http_headers.dart index d1521533c29..e27193554d3 100644 --- a/sdk/lib/_http/http_headers.dart +++ b/sdk/lib/_http/http_headers.dart @@ -641,8 +641,8 @@ class _HeaderValue implements HeaderValue { Map _parameters; Map _unmodifiableParameters; - _HeaderValue([this._value = "", Map parameters]) { - if (parameters != null) { + _HeaderValue([this._value = "", Map parameters = const {}]) { + if (parameters != null && parameters.isNotEmpty) { _parameters = new HashMap.from(parameters); } } @@ -659,18 +659,25 @@ class _HeaderValue implements HeaderValue { String get value => _value; - void _ensureParameters() { - if (_parameters == null) { - _parameters = new HashMap(); - } - } + Map _ensureParameters() => _parameters ??= {}; - Map get parameters { - _ensureParameters(); - if (_unmodifiableParameters == null) { - _unmodifiableParameters = new UnmodifiableMapView(_parameters); + Map get parameters => + _unmodifiableParameters ??= UnmodifiableMapView(_ensureParameters()); + + static bool _isToken(String token) { + if (token.isEmpty) { + return false; } - return _unmodifiableParameters; + final delimiters = "\"(),/:;<=>?@[\]{}"; + for (int i = 0; i < token.length; i++) { + int codeUnit = token.codeUnitAt(i); + if (codeUnit <= 32 || + codeUnit >= 127 || + delimiters.indexOf(token[i]) >= 0) { + return false; + } + } + return true; } String toString() { @@ -678,7 +685,27 @@ class _HeaderValue implements HeaderValue { sb.write(_value); if (parameters != null && parameters.length > 0) { _parameters.forEach((String name, String value) { - sb..write("; ")..write(name)..write("=")..write(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(); @@ -716,8 +743,12 @@ class _HeaderValue implements HeaderValue { index++; } - void maybeExpect(String expected) { - if (s[index] == expected) index++; + bool maybeExpect(String expected) { + if (done() || !s.startsWith(expected, index)) { + return false; + } + index++; + return true; } void parseParameters() { @@ -753,16 +784,15 @@ class _HeaderValue implements HeaderValue { index++; } else if (s[index] == "\"") { index++; - break; + return sb.toString(); } sb.write(s[index]); index++; } - return sb.toString(); + throw new HttpException("Failed to parse header value"); } else { // Parse non-quoted value. - var val = parseValue(); - return val == "" ? null : val; + return parseValue(); } } @@ -771,23 +801,18 @@ class _HeaderValue implements HeaderValue { if (done()) return; String name = parseParameterName(); skipWS(); - if (done()) { + 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; - return; } - maybeExpect("="); - skipWS(); - if (done()) { - parameters[name] = null; - return; - } - String value = parseParameterValue(); - if (name == 'charset' && this is _ContentType && value != null) { - // Charset parameter of ContentTypes are always lower-case. - value = value.toLowerCase(); - } - parameters[name] = value; - skipWS(); if (done()) return; // TODO: Implement support for multi-valued parameters. if (s[index] == valueSeparator) return; @@ -821,7 +846,7 @@ class _ContentType extends _HeaderValue implements ContentType { parameters.forEach((String key, String value) { String lowerCaseKey = key.toLowerCase(); if (lowerCaseKey == "charset") { - value = value.toLowerCase(); + value = value?.toLowerCase(); } this._parameters[lowerCaseKey] = value; }); diff --git a/sdk_nnbd/lib/_http/http.dart b/sdk_nnbd/lib/_http/http.dart index fb662c6b5ed..0fd38cbf774 100644 --- a/sdk_nnbd/lib/_http/http.dart +++ b/sdk_nnbd/lib/_http/http.dart @@ -772,6 +772,11 @@ abstract class HttpHeaders { * [HeaderValue] can be used to conveniently build and parse header * values on this form. * + * Parameter values can be omitted, in which case the value is parsed as `null`. + * Values can be doubled quoted to allow characters outside of the RFC 7230 + * token characters and backslash sequences can be used to represent the double + * quote and backslash characters themselves. + * * To build an "accepts" header with the value * * text/plain; q=0.3, text/html @@ -798,7 +803,8 @@ abstract class HeaderValue { /** * Creates a new header value object setting the value and parameters. */ - factory HeaderValue([String value = "", Map? parameters]) { + factory HeaderValue( + [String value = "", Map parameters = const {}]) { return new _HeaderValue(value, parameters); } @@ -826,7 +832,7 @@ abstract class HeaderValue { * * This map cannot be modified. */ - Map get parameters; + Map get parameters; /** * Returns the formatted string representation in the form: @@ -916,7 +922,7 @@ abstract class ContentType implements HeaderValue { * or in `parameters`, will have its value converted to lower-case. */ factory ContentType(String primaryType, String subType, - {String? charset, Map? parameters}) { + {String? charset, Map parameters = const {}}) { return new _ContentType(primaryType, subType, charset, parameters); } diff --git a/sdk_nnbd/lib/_http/http_headers.dart b/sdk_nnbd/lib/_http/http_headers.dart index 3fcdbe77862..db3d7aa4f82 100644 --- a/sdk_nnbd/lib/_http/http_headers.dart +++ b/sdk_nnbd/lib/_http/http_headers.dart @@ -648,12 +648,14 @@ class _HttpHeaders implements HttpHeaders { class _HeaderValue implements HeaderValue { String _value; - Map? _parameters; - Map? _unmodifiableParameters; + Map? _parameters; + Map? _unmodifiableParameters; - _HeaderValue([this._value = "", Map? parameters]) { - if (parameters != null) { - _parameters = new HashMap.from(parameters); + _HeaderValue([this._value = "", Map parameters = const {}]) { + // TODO(40614): Remove once non-nullability is sound. + Map? nullableParameters = parameters; + if (nullableParameters != null && nullableParameters.isNotEmpty) { + _parameters = new HashMap.from(nullableParameters); } } @@ -669,23 +671,55 @@ class _HeaderValue implements HeaderValue { String get value => _value; - Map _ensureParameters() => - _parameters ??= HashMap(); + Map _ensureParameters() => + _parameters ??= {}; - Map get 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.indexOf(token[i]) >= 0) { + return false; + } + } + return true; + } + String toString() { StringBuffer sb = new StringBuffer(); sb.write(_value); var parameters = this._parameters; if (parameters != null && parameters.length > 0) { - parameters.forEach((String name, String value) { - sb - ..write("; ") - ..write(name) - ..write("=") - ..write(value == "" ? '""' : value); + 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(); @@ -724,8 +758,12 @@ class _HeaderValue implements HeaderValue { index++; } - void maybeExpect(String expected) { - if (s[index] == expected) index++; + bool maybeExpect(String expected) { + if (done() || !s.startsWith(expected, index)) { + return false; + } + index++; + return true; } void parseParameters() { @@ -762,13 +800,13 @@ class _HeaderValue implements HeaderValue { index++; } else if (char == "\"") { index++; - break; + return sb.toString(); } char = s[index]; sb.write(char); index++; } - return sb.toString(); + throw new HttpException("Failed to parse header value"); } else { // Parse non-quoted value. return parseValue(); @@ -780,23 +818,18 @@ class _HeaderValue implements HeaderValue { if (done()) return; String name = parseParameterName(); skipWS(); - if (done()) { - parameters[name] = ""; - return; + 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; } - maybeExpect("="); - skipWS(); - if (done()) { - parameters[name] = ""; - return; - } - String value = parseParameterValue(); - if (name == 'charset' && this is _ContentType) { - // Charset parameter of ContentTypes are always lower-case. - value = value.toLowerCase(); - } - parameters[name] = value; - skipWS(); if (done()) return; // TODO: Implement support for multi-valued parameters. if (s[index] == valueSeparator) return; @@ -805,7 +838,7 @@ class _HeaderValue implements HeaderValue { } skipWS(); - _value = parseValue(); // TODO(39784): No _validateValue? + _value = parseValue(); skipWS(); if (done()) return; maybeExpect(parameterSeparator); @@ -818,17 +851,23 @@ class _ContentType extends _HeaderValue implements ContentType { String _subType = ""; _ContentType(String primaryType, String subType, String? charset, - Map? parameters) + 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"; - if (parameters != null) { + // TODO(40614): Remove once non-nullability is sound. + Map? nullableParameters = parameters; + if (nullableParameters != null) { var parameterMap = _ensureParameters(); - parameters.forEach((String key, String value) { + nullableParameters.forEach((String key, String? value) { String lowerCaseKey = key.toLowerCase(); if (lowerCaseKey == "charset") { - value = value.toLowerCase(); + value = value?.toLowerCase(); } parameterMap[lowerCaseKey] = value; }); diff --git a/tests/standalone/io/http_headers_test.dart b/tests/standalone/io/http_headers_test.dart index d1d0b8c2c1b..56174989705 100644 --- a/tests/standalone/io/http_headers_test.dart +++ b/tests/standalone/io/http_headers_test.dart @@ -259,6 +259,25 @@ void testHeaderValue() { } HeaderValue headerValue; + headerValue = HeaderValue.parse(""); + check(headerValue, "", {}); + headerValue = HeaderValue.parse(";"); + check(headerValue, "", {}); + headerValue = HeaderValue.parse(";;"); + check(headerValue, "", {}); + headerValue = HeaderValue.parse("v;a"); + check(headerValue, "v", {"a": null}); + headerValue = HeaderValue.parse("v;a="); + check(headerValue, "v", {"a": ""}); + Expect.throws(() => HeaderValue.parse("v;a=\""), (e) => e is HttpException); + headerValue = HeaderValue.parse("v;a=\"\""); + check(headerValue, "v", {"a": ""}); + Expect.throws(() => HeaderValue.parse("v;a=\"\\"), (e) => e is HttpException); + Expect.throws( + () => HeaderValue.parse("v;a=\";b=\"c\""), (e) => e is HttpException); + Expect.throws(() => HeaderValue.parse("v;a=b c"), (e) => e is HttpException); + headerValue = HeaderValue.parse("æ;ø=å"); + check(headerValue, "æ", {"ø": "å"}); headerValue = HeaderValue.parse("xxx; aaa=bbb; ccc=\"\\\";\\a\"; ddd=\" \""); check(headerValue, "xxx", {"aaa": "bbb", "ccc": '\";a', "ddd": " "}); @@ -280,6 +299,24 @@ void testHeaderValue() { check(headerValue, "attachment", parameters); headerValue = HeaderValue.parse("xxx; aaa; bbb; ccc"); check(headerValue, "xxx", {"aaa": null, "bbb": null, "ccc": null}); + + Expect.equals("", HeaderValue().toString()); + Expect.equals("", HeaderValue("").toString()); + Expect.equals("v", HeaderValue("v").toString()); + Expect.equals("v", HeaderValue("v", {}).toString()); + Expect.equals("v; ", HeaderValue("v", {"": null}).toString()); + Expect.equals("v; a", HeaderValue("v", {"a": null}).toString()); + Expect.equals("v; a; b", HeaderValue("v", {"a": null, "b": null}).toString()); + Expect.equals( + "v; a; b=c", HeaderValue("v", {"a": null, "b": "c"}).toString()); + Expect.equals( + "v; a=c; b", HeaderValue("v", {"a": "c", "b": null}).toString()); + Expect.equals("v; a=\"\"", HeaderValue("v", {"a": ""}).toString()); + Expect.equals("v; a=\"b c\"", HeaderValue("v", {"a": "b c"}).toString()); + Expect.equals("v; a=\",\"", HeaderValue("v", {"a": ","}).toString()); + Expect.equals( + "v; a=\"\\\\\\\"\"", HeaderValue("v", {"a": "\\\""}).toString()); + Expect.equals("v; a=\"ø\"", HeaderValue("v", {"a": "ø"}).toString()); } void testContentType() { @@ -352,7 +389,7 @@ void testContentType() { check(contentType, "text", "html", {"charset": "utf-8", "xxx": "yyy"}); contentType = ContentType.parse("text/html; charset=;"); - check(contentType, "text", "html", {"charset": null}); + check(contentType, "text", "html", {"charset": ""}); contentType = ContentType.parse("text/html; charset;"); check(contentType, "text", "html", {"charset": null}); diff --git a/tests/standalone_2/io/http_headers_test.dart b/tests/standalone_2/io/http_headers_test.dart index d1d0b8c2c1b..1213e851c25 100644 --- a/tests/standalone_2/io/http_headers_test.dart +++ b/tests/standalone_2/io/http_headers_test.dart @@ -259,6 +259,25 @@ void testHeaderValue() { } HeaderValue headerValue; + headerValue = HeaderValue.parse(""); + check(headerValue, "", {}); + headerValue = HeaderValue.parse(";"); + check(headerValue, "", {}); + headerValue = HeaderValue.parse(";;"); + check(headerValue, "", {}); + headerValue = HeaderValue.parse("v;a"); + check(headerValue, "v", {"a": null}); + headerValue = HeaderValue.parse("v;a="); + check(headerValue, "v", {"a": ""}); + Expect.throws(() => HeaderValue.parse("v;a=\""), (e) => e is HttpException); + headerValue = HeaderValue.parse("v;a=\"\""); + check(headerValue, "v", {"a": ""}); + Expect.throws(() => HeaderValue.parse("v;a=\"\\"), (e) => e is HttpException); + Expect.throws( + () => HeaderValue.parse("v;a=\";b=\"c\""), (e) => e is HttpException); + Expect.throws(() => HeaderValue.parse("v;a=b c"), (e) => e is HttpException); + headerValue = HeaderValue.parse("æ;ø=å"); + check(headerValue, "æ", {"ø": "å"}); headerValue = HeaderValue.parse("xxx; aaa=bbb; ccc=\"\\\";\\a\"; ddd=\" \""); check(headerValue, "xxx", {"aaa": "bbb", "ccc": '\";a', "ddd": " "}); @@ -280,6 +299,25 @@ void testHeaderValue() { check(headerValue, "attachment", parameters); headerValue = HeaderValue.parse("xxx; aaa; bbb; ccc"); check(headerValue, "xxx", {"aaa": null, "bbb": null, "ccc": null}); + + Expect.equals("", HeaderValue().toString()); + Expect.equals("", HeaderValue("").toString()); + Expect.equals("v", HeaderValue("v").toString()); + Expect.equals("v", HeaderValue("v", null).toString()); + Expect.equals("v", HeaderValue("v", {}).toString()); + Expect.equals("v; ", HeaderValue("v", {"": null}).toString()); + Expect.equals("v; a", HeaderValue("v", {"a": null}).toString()); + Expect.equals("v; a; b", HeaderValue("v", {"a": null, "b": null}).toString()); + Expect.equals( + "v; a; b=c", HeaderValue("v", {"a": null, "b": "c"}).toString()); + Expect.equals( + "v; a=c; b", HeaderValue("v", {"a": "c", "b": null}).toString()); + Expect.equals("v; a=\"\"", HeaderValue("v", {"a": ""}).toString()); + Expect.equals("v; a=\"b c\"", HeaderValue("v", {"a": "b c"}).toString()); + Expect.equals("v; a=\",\"", HeaderValue("v", {"a": ","}).toString()); + Expect.equals( + "v; a=\"\\\\\\\"\"", HeaderValue("v", {"a": "\\\""}).toString()); + Expect.equals("v; a=\"ø\"", HeaderValue("v", {"a": "ø"}).toString()); } void testContentType() { @@ -352,7 +390,7 @@ void testContentType() { check(contentType, "text", "html", {"charset": "utf-8", "xxx": "yyy"}); contentType = ContentType.parse("text/html; charset=;"); - check(contentType, "text", "html", {"charset": null}); + check(contentType, "text", "html", {"charset": ""}); contentType = ContentType.parse("text/html; charset;"); check(contentType, "text", "html", {"charset": null});