mirror of
https://github.com/dart-lang/sdk
synced 2024-09-05 08:24:39 +00:00
[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:
parent
5f8802b82f
commit
96cf889e6b
31
CHANGELOG.md
31
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`.
|
||||
|
|
|
@ -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'.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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});
|
||||
|
||||
|
|
|
@ -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});
|
||||
|
||||
|
|
Loading…
Reference in a new issue