[dart:io] Fix HeaderValue parsing, toString(), and support null values.

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 <sortie@google.com>
Reviewed-by: Lasse R.H. Nielsen <lrn@google.com>
This commit is contained in:
Jonas Termansen 2020-03-18 11:36:36 +00:00 committed by commit-bot@chromium.org
parent 5f8802b82f
commit 96cf889e6b
9 changed files with 275 additions and 93 deletions

View file

@ -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`.

View file

@ -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'.

View file

@ -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.

View file

@ -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<String, String> parameters]) {
factory HeaderValue(
[String value = "", Map<String, String> 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<String, String> parameters}) {
{String charset, Map<String, String> parameters = const {}}) {
return new _ContentType(primaryType, subType, charset, parameters);
}

View file

@ -641,8 +641,8 @@ class _HeaderValue implements HeaderValue {
Map<String, String> _parameters;
Map<String, String> _unmodifiableParameters;
_HeaderValue([this._value = "", Map<String, String> parameters]) {
if (parameters != null) {
_HeaderValue([this._value = "", Map<String, String> parameters = const {}]) {
if (parameters != null && parameters.isNotEmpty) {
_parameters = new HashMap<String, String>.from(parameters);
}
}
@ -659,18 +659,25 @@ class _HeaderValue implements HeaderValue {
String get value => _value;
void _ensureParameters() {
if (_parameters == null) {
_parameters = new HashMap<String, String>();
}
}
Map<String, String> _ensureParameters() => _parameters ??= <String, String>{};
Map<String, String> get parameters {
_ensureParameters();
if (_unmodifiableParameters == null) {
_unmodifiableParameters = new UnmodifiableMapView(_parameters);
Map<String, String> 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;
});

View file

@ -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<String, String>? parameters]) {
factory HeaderValue(
[String value = "", Map<String, String?> parameters = const {}]) {
return new _HeaderValue(value, parameters);
}
@ -826,7 +832,7 @@ abstract class HeaderValue {
*
* This map cannot be modified.
*/
Map<String, String> get parameters;
Map<String, String?> 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<String, String>? parameters}) {
{String? charset, Map<String, String?> parameters = const {}}) {
return new _ContentType(primaryType, subType, charset, parameters);
}

View file

@ -648,12 +648,14 @@ class _HttpHeaders implements HttpHeaders {
class _HeaderValue implements HeaderValue {
String _value;
Map<String, String>? _parameters;
Map<String, String>? _unmodifiableParameters;
Map<String, String?>? _parameters;
Map<String, String?>? _unmodifiableParameters;
_HeaderValue([this._value = "", Map<String, String>? parameters]) {
if (parameters != null) {
_parameters = new HashMap<String, String>.from(parameters);
_HeaderValue([this._value = "", Map<String, String?> parameters = const {}]) {
// TODO(40614): Remove once non-nullability is sound.
Map<String, String?>? nullableParameters = parameters;
if (nullableParameters != null && nullableParameters.isNotEmpty) {
_parameters = new HashMap<String, String?>.from(nullableParameters);
}
}
@ -669,23 +671,55 @@ class _HeaderValue implements HeaderValue {
String get value => _value;
Map<String, String> _ensureParameters() =>
_parameters ??= HashMap<String, String>();
Map<String, String?> _ensureParameters() =>
_parameters ??= <String, String?>{};
Map<String, String> get parameters =>
Map<String, String?> 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<String, String>? parameters)
Map<String, String?> 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<String, String?>? 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;
});

View file

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

View file

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