Fix fused UTF-8/JSON decoding.
The VM version's parser did not allow a leading BOM, and it failed to parse a top-level integer.
Added test to check this.
Fixes #33251
Bug: http://dartbug.com/33251
Change-Id: I51e429082f0e9baac81e20f73b0885922b40b0b8
Reviewed-on: https://dart-review.googlesource.com/56860
Commit-Queue: Lasse R.H. Nielsen <lrn@google.com>
Reviewed-by: Florian Loitsch <floitsch@google.com>
diff --git a/pkg/expect/lib/expect.dart b/pkg/expect/lib/expect.dart
index eea2ab4..4803de0 100644
--- a/pkg/expect/lib/expect.dart
+++ b/pkg/expect/lib/expect.dart
@@ -515,33 +515,35 @@
}
/**
- * Calls the function [f] and verifies that it throws an exception.
+ * Calls the function [f] and verifies that it throws a `T`.
* The optional [check] function can provide additional validation
- * that the correct exception is being thrown. For example, to check
- * the type of the exception you could write this:
+ * that the correct object is being thrown. For example, to check
+ * the content of the thrown boject you could write this:
*
- * Expect.throws(myThrowingFunction, (e) => e is MyException);
+ * Expect.throws<MyException>(myThrowingFunction,
+ * (e) => e.myMessage.contains("WARNING"));
+ *
+ * The type variable can be omitted and the type checked in [check]
+ * instead. This was traditionally done before Dart had generic methods.
*
* If `f` fails an expectation (i.e., throws an [ExpectException]), that
* exception is not caught by [Expect.throws]. The test is still considered
* failing.
*/
- static void throws(void f(), [bool check(Object error), String reason]) {
+ static void throws<T>(void f(), [bool check(T error), String reason]) {
String msg = reason == null ? "" : "($reason)";
- if (f is! _Nullary) {
+ if (f is! Function()) {
// Only throws from executing the function body should count as throwing.
// The failure to even call `f` should throw outside the try/catch.
_fail("Expect.throws$msg: Function f not callable with zero arguments");
}
try {
f();
- } catch (e, s) {
+ } on Object catch (e, s) {
// A test failure doesn't count as throwing.
if (e is ExpectException) rethrow;
- if (check != null && !check(e)) {
- _fail("Expect.throws$msg: Unexpected '$e'\n$s");
- }
- return;
+ if (e is T && (check == null || check(e))) return;
+ _fail("Expect.throws$msg: Unexpected '$e'\n$s");
}
_fail('Expect.throws$msg fails: Did not throw');
}
@@ -607,8 +609,9 @@
/// Used in [Expect] because [Expect.identical] shadows the real [identical].
bool _identical(a, b) => identical(a, b);
-typedef _Nullary(); // Expect.throws argument must be this type.
-
+/// Exception thrown on a failed expectation check.
+///
+/// Always recognized by [Expect.throws] as an unexpected error.
class ExpectException implements Exception {
final String message;
ExpectException(this.message);
diff --git a/runtime/lib/convert_patch.dart b/runtime/lib/convert_patch.dart
index b2e7ed2..a2c802d 100644
--- a/runtime/lib/convert_patch.dart
+++ b/runtime/lib/convert_patch.dart
@@ -63,6 +63,7 @@
parser.chunk = input;
parser.chunkEnd = input.length;
parser.parse(0);
+ parser.close();
return parser.result;
}
@@ -398,6 +399,7 @@
static const int KWD_NULL = 0; // Prefix of "null" seen.
static const int KWD_TRUE = 4; // Prefix of "true" seen.
static const int KWD_FALSE = 8; // Prefix of "false" seen.
+ static const int KWD_BOM = 12; // Prefix of BOM seen.
static const int KWD_COUNT_SHIFT = 4; // Prefix length in bits 4+.
// Mask used to mask off two lower bits.
@@ -439,8 +441,11 @@
* ..0ddd0011 : Partial 'null' keyword.
* ..0ddd0111 : Partial 'true' keyword.
* ..0ddd1011 : Partial 'false' keyword.
- * For all three keywords, the `ddd` bits encode the number
+ * ..0ddd1111 : Partial UTF-8 BOM byte seqeuence ("\xEF\xBB\xBF").
+ * For all keywords, the `ddd` bits encode the number
* of letters seen.
+ * The BOM byte sequence is only used by [_JsonUtf8Parser],
+ * and only at the very beginning of input.
*/
int partialState = NO_PARTIAL;
@@ -789,7 +794,8 @@
int keywordType = partialState & KWD_TYPE_MASK;
int count = partialState >> KWD_COUNT_SHIFT;
int keywordTypeIndex = keywordType >> KWD_TYPE_SHIFT;
- String keyword = const ["null", "true", "false"][keywordTypeIndex];
+ String keyword =
+ const ["null", "true", "false", "\xEF\xBB\xBF"][keywordTypeIndex];
assert(count < keyword.length);
do {
if (position == chunkEnd) {
@@ -798,13 +804,19 @@
return chunkEnd;
}
int expectedChar = keyword.codeUnitAt(count);
- if (getChar(position) != expectedChar) return fail(position);
+ if (getChar(position) != expectedChar) {
+ if (count == 0) {
+ assert(keywordType == KWD_BOM);
+ return position;
+ }
+ return fail(position);
+ }
position++;
count++;
} while (count < keyword.length);
if (keywordType == KWD_NULL) {
listener.handleNull();
- } else {
+ } else if (keywordType != KWD_BOM) {
listener.handleBool(keywordType == KWD_TRUE);
}
return position;
@@ -1255,7 +1267,7 @@
fail(position, "Missing expected digit");
} else {
// If it doesn't even start out as a numeral.
- fail(position, "Unexpected character");
+ fail(position);
}
}
if (digit == 0) {
@@ -1660,7 +1672,7 @@
position++;
} else {
throw new FormatException(
- "Unexepected UTF-8 continuation byte", utf8, position);
+ "Unexpected UTF-8 continuation byte", utf8, position);
}
} else if (char < 0xE0) {
// C0-DF
@@ -1726,7 +1738,11 @@
int chunkEnd;
_JsonUtf8Parser(_JsonListener listener, this.allowMalformed)
- : super(listener);
+ : super(listener) {
+ // Starts out checking for an optional BOM (KWD_BOM, count = 0).
+ partialState =
+ _ChunkedJsonParser.PARTIAL_KEYWORD | _ChunkedJsonParser.KWD_BOM;
+ }
int getChar(int position) => chunk[position];
diff --git a/tests/lib_2/convert/json_test.dart b/tests/lib_2/convert/json_test.dart
index d7be8a0..22e5f24 100644
--- a/tests/lib_2/convert/json_test.dart
+++ b/tests/lib_2/convert/json_test.dart
@@ -104,10 +104,16 @@
}
void testThrows(jsonText) {
+ var message = "json = '${escape(jsonText)}'";
Expect.throwsFormatException(() => json.decode(jsonText),
- "json = '${escape(jsonText)}'");
+ "json.decode, $message");
Expect.throwsFormatException(() => jsonDecode(jsonText),
- "json = '${escape(jsonText)}'");
+ "jsonDecode, $message");
+ Expect.throwsFormatException(() => json.decoder.convert(jsonText),
+ "json.decoder.convert, $message");
+ Expect.throwsFormatException(() =>
+ utf8.decoder.fuse(json.decoder).convert(utf8.encode(jsonText)),
+ "utf8.decoder.fuse(json.decoder) o utf.encode, $message");
}
testNumbers() {
@@ -187,6 +193,10 @@
testThrows("-2.2 e+2");
testThrows("-2.2e +2");
testThrows("-2.2e+ 2");
+ testThrows("01");
+ testThrows("0.");
+ testThrows(".0");
+ testThrows("0.e1");
testThrows("[2.,2]");
testThrows("{2.:2}");
diff --git a/tests/lib_2/convert/json_utf8_test.dart b/tests/lib_2/convert/json_utf8_test.dart
new file mode 100644
index 0000000..6afae4d
--- /dev/null
+++ b/tests/lib_2/convert/json_utf8_test.dart
@@ -0,0 +1,77 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Test that the fused UTF-8/JSON decoder accepts a leading BOM.
+library test;
+
+import "package:expect/expect.dart";
+import "dart:convert";
+
+void main() {
+ for (var parse in [parseFuse, parseSequence, parseChunked]) {
+ // Sanity checks.
+ Expect.isTrue(parse('true'.codeUnits.toList()));
+ Expect.isFalse(parse('false'.codeUnits.toList()));
+ Expect.equals(123, parse('123'.codeUnits.toList()));
+ Expect.listEquals([42], parse('[42]'.codeUnits.toList()));
+ Expect.mapEquals({"x": 42}, parse('{"x":42}'.codeUnits.toList()));
+
+ // String (0x22 = ") with UTF-8 encoded Unicode characters.
+ Expect.equals(
+ "A\xff\u1234\u{65555}",
+ parse([
+ 0x22,
+ 0x41,
+ 0xc3,
+ 0xbf,
+ 0xe1,
+ 0x88,
+ 0xb4,
+ 0xf1,
+ 0xa5,
+ 0x95,
+ 0x95,
+ 0x22
+ ]));
+
+ // BOM followed by true.
+ Expect.isTrue(parse([0xEF, 0xBB, 0xBF, 0x74, 0x72, 0x75, 0x65]));
+ }
+
+ // Do not accept BOM in non-UTF-8 decoder.
+ Expect.throws<FormatException>(
+ () => new JsonDecoder().convert("\xEF\xBB\xBFtrue"));
+ Expect.throws<FormatException>(() => new JsonDecoder().convert("\uFEFFtrue"));
+
+ // Only accept BOM first.
+ Expect.throws<FormatException>(
+ () => parseFuse(" \xEF\xBB\xBFtrue".codeUnits.toList()));
+ // Only accept BOM first.
+ Expect.throws<FormatException>(
+ () => parseFuse(" true\xEF\xBB\xBF".codeUnits.toList()));
+
+ Expect.throws<FormatException>(
+ () => parseFuse(" [\xEF\xBB\xBF]".codeUnits.toList()));
+}
+
+Object parseFuse(List<int> text) {
+ return new Utf8Decoder().fuse(new JsonDecoder()).convert(text);
+}
+
+Object parseSequence(List<int> text) {
+ return new JsonDecoder().convert(new Utf8Decoder().convert(text));
+}
+
+Object parseChunked(List<int> text) {
+ var result;
+ var sink = new Utf8Decoder().fuse(new JsonDecoder()).startChunkedConversion(
+ new ChunkedConversionSink.withCallback((List<Object> values) {
+ result = values[0];
+ }));
+ for (var i = 0; i < text.length; i++) {
+ sink.add([text[i]]);
+ }
+ sink.close();
+ return result;
+}