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});