656ff46efd
The dart2wasm compiler can use three different SDK platforms depending on compiler options: The default one, a JS compatibility target and a standalone target. Code for these platforms used to be in `_internal/` subdirectories, but parts of `_internal/wasm/lib` were also used by the other targets. To make it clearer which patches/internal libraries belong to which target, this restructures `_internal/wasm` as follows: Files only relevant for one target are in `js`, `js_compatibility` and `standalone`. Files used by all targets are in `common`, files used in the default and the JS compatibility target are in `js_common`. TEST=Refactor, covered by existing tests Change-Id: I2c63c04a5fd5ca88cd640889aa295bd1fb8cfc95 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/505441 Reviewed-by: Slava Egorov <vegorov@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com> Commit-Queue: Martin Kustermann <kustermann@google.com> SLSA-Policy-Verified: SLSA Policy Verification Service <devtools-gerritcodereview-exitgate@google.com>
2922 lines
88 KiB
Dart
2922 lines
88 KiB
Dart
// Copyright (c) 2022, 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.
|
|
|
|
import "dart:_boxed_int";
|
|
import "dart:_compact_hash" show createMapFromStringKeyValueListUnsafe;
|
|
import "dart:_error_utils";
|
|
import "dart:_internal"
|
|
show patch, POWERS_OF_TEN, unsafeCast, pushWasmArray, popWasmArray;
|
|
import "dart:_list"
|
|
show GrowableList, WasmListBaseUnsafeExtensions, WasmListBase;
|
|
import "dart:_string";
|
|
import "dart:_string_helper";
|
|
import "dart:_typed_data";
|
|
import "dart:_object_helper";
|
|
import "dart:_wasm";
|
|
import "dart:typed_data" show Uint8List;
|
|
|
|
@patch
|
|
dynamic _parseJson(
|
|
String source,
|
|
Object? Function(Object? key, Object? value)? reviver,
|
|
) {
|
|
final listener = _JsonListener(reviver);
|
|
final parser = _StringParser(listener);
|
|
parser.setNewChunk(source, source.length);
|
|
parser.parse(0);
|
|
parser.close();
|
|
return listener.result;
|
|
}
|
|
|
|
@patch
|
|
class Utf8Decoder {
|
|
@patch
|
|
Converter<List<int>, T> fuse<T>(Converter<String, T> next) {
|
|
if (next is JsonDecoder) {
|
|
return _JsonUtf8Decoder(
|
|
(next as JsonDecoder)._reviver,
|
|
this._allowMalformed,
|
|
) as dynamic /*=Converter<List<int>, T>*/;
|
|
}
|
|
return super.fuse<T>(next);
|
|
}
|
|
}
|
|
|
|
class _JsonUtf8Decoder extends Converter<List<int>, Object?> {
|
|
final Object? Function(Object? key, Object? value)? _reviver;
|
|
final bool _allowMalformed;
|
|
|
|
_JsonUtf8Decoder(this._reviver, this._allowMalformed);
|
|
|
|
Object? convert(List<int> input) {
|
|
var parser = _JsonUtf8DecoderSink._createParser(_reviver, _allowMalformed);
|
|
parser.parseChunk(input, 0, input.length);
|
|
parser.close();
|
|
return parser.result;
|
|
}
|
|
|
|
ByteConversionSink startChunkedConversion(Sink<Object?> sink) =>
|
|
_JsonUtf8DecoderSink(_reviver, sink, _allowMalformed);
|
|
}
|
|
|
|
//// Implementation ///////////////////////////////////////////////////////////
|
|
|
|
// Simple API for JSON parsing.
|
|
|
|
/**
|
|
* A [_JsonListener] builds data objects from the parser events.
|
|
*
|
|
* This is a simple stack-based object builder. It keeps the most recently
|
|
* seen value in a variable, and uses it depending on the following event.
|
|
*/
|
|
class _JsonListener {
|
|
_JsonListener(this.reviver);
|
|
|
|
final Object? Function(Object? key, Object? value)? reviver;
|
|
|
|
/**
|
|
* Stack used to handle nested containers.
|
|
*
|
|
* The current container is pushed on the stack when a new one is started.
|
|
*/
|
|
WasmArray<GrowableList?> stack = WasmArray<GrowableList?>(0);
|
|
int stackLength = 0;
|
|
|
|
void stackPush(WasmArray<Object?>? value, int valueLength) {
|
|
final GrowableList<Object?>? valueAsList = value == null
|
|
? null
|
|
: GrowableList.withDataAndLength(value, valueLength);
|
|
|
|
// `GrowableList._nextCapacity` is copied here as the next capacity. We
|
|
// can't use `GrowableList._nextCapacity` as tear-off as it's difficult to
|
|
// inline tear-offs manually in the `pushWasmArray` compiler.
|
|
pushWasmArray<GrowableList<Object?>?>(
|
|
this.stack,
|
|
this.stackLength,
|
|
valueAsList,
|
|
(stackLength * 2) | 3,
|
|
);
|
|
}
|
|
|
|
GrowableList<dynamic>? stackPop() {
|
|
assert(stackLength != 0);
|
|
return popWasmArray<GrowableList<dynamic>?>(stack, stackLength);
|
|
}
|
|
|
|
/** Contents of the current container being built, or null if not building a
|
|
* container.
|
|
*
|
|
* When building a [Map] this will contain array of key-value pairs.
|
|
*/
|
|
WasmArray<Object?>? currentContainer = null;
|
|
int currentContainerLength = 0;
|
|
|
|
void currentContainerPush(Object? value) {
|
|
WasmArray<Object?> currentContainerNonNull = unsafeCast<WasmArray<Object?>>(
|
|
this.currentContainer,
|
|
);
|
|
// Per `_HashBase` invariant capacity needs to be a power of two.
|
|
pushWasmArray<Object?>(
|
|
currentContainerNonNull,
|
|
this.currentContainerLength,
|
|
value,
|
|
currentContainerLength == 0 ? 8 : (currentContainerLength * 2),
|
|
);
|
|
currentContainer = currentContainerNonNull;
|
|
}
|
|
|
|
/** The most recently read value. */
|
|
Object? value;
|
|
|
|
/** Pushes the currently active container. */
|
|
void beginContainer() {
|
|
stackPush(currentContainer, currentContainerLength);
|
|
currentContainer = const WasmArray<Object?>.literal([]);
|
|
currentContainerLength = 0;
|
|
}
|
|
|
|
/** Pops the top container from the [stack]. */
|
|
void popContainer() {
|
|
final GrowableList<dynamic>? currentContainerList = stackPop();
|
|
if (currentContainerList == null) {
|
|
currentContainer = null;
|
|
currentContainerLength = 0;
|
|
} else {
|
|
currentContainer = currentContainerList.data;
|
|
currentContainerLength = currentContainerList.length;
|
|
}
|
|
}
|
|
|
|
void handleString(String value) {
|
|
this.value = value;
|
|
}
|
|
|
|
@pragma('wasm:prefer-inline')
|
|
void handleIntegerNumber(int value) {
|
|
if (value.toWasmI64().leU(0xff.toWasmI64())) {
|
|
handleNumber(_intBoxes256[value]);
|
|
return;
|
|
}
|
|
handleNumber(value);
|
|
}
|
|
|
|
void handleNumber(num value) {
|
|
this.value = value;
|
|
}
|
|
|
|
void handleBool(bool value) {
|
|
this.value = value;
|
|
}
|
|
|
|
void handleNull() {
|
|
this.value = null;
|
|
}
|
|
|
|
void beginObject() {
|
|
beginContainer();
|
|
}
|
|
|
|
void propertyName() {
|
|
currentContainerPush(value);
|
|
value = null;
|
|
}
|
|
|
|
void propertyValue() {
|
|
if (reviver case final reviver?) {
|
|
final keyValuePairs = unsafeCast<WasmArray<Object?>>(
|
|
currentContainer,
|
|
); // null deref
|
|
final key = keyValuePairs[currentContainerLength - 1];
|
|
currentContainerPush(reviver(key, value));
|
|
} else {
|
|
currentContainerPush(value);
|
|
}
|
|
value = null;
|
|
}
|
|
|
|
void endObject() {
|
|
value = createMapFromStringKeyValueListUnsafe<String, dynamic>(
|
|
unsafeCast<WasmArray<Object?>>(currentContainer),
|
|
currentContainerLength,
|
|
);
|
|
popContainer();
|
|
}
|
|
|
|
void beginArray() {
|
|
beginContainer();
|
|
}
|
|
|
|
void arrayElement() {
|
|
var reviver = this.reviver;
|
|
if (reviver != null) {
|
|
value = reviver(currentContainerLength, value);
|
|
}
|
|
currentContainerPush(value);
|
|
value = null;
|
|
}
|
|
|
|
void endArray() {
|
|
value = GrowableList.withDataAndLength(
|
|
unsafeCast<WasmArray<Object?>>(currentContainer),
|
|
currentContainerLength,
|
|
);
|
|
popContainer();
|
|
}
|
|
|
|
/**
|
|
* Read out the final result of parsing a JSON string.
|
|
*
|
|
* Must only be called when the entire input has been parsed.
|
|
*/
|
|
dynamic get result {
|
|
assert(currentContainer == null);
|
|
var reviver = this.reviver;
|
|
if (reviver != null) {
|
|
return reviver(null, value);
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Buffer holding parts of a numeral.
|
|
*
|
|
* The buffer contains the characters of a JSON number.
|
|
* These are all ASCII, so an `array i8` is used as backing store.
|
|
*
|
|
* This buffer is used when a JSON number is split between separate chunks.
|
|
*/
|
|
class _NumberBuffer {
|
|
static const int minCapacity = 16;
|
|
static const int defaultOverhead = 5;
|
|
WasmArray<WasmI8> array;
|
|
int length = 0;
|
|
_NumberBuffer(int initialCapacity)
|
|
: array = WasmArray<WasmI8>(_initialCapacity(initialCapacity));
|
|
|
|
int get capacity => array.length;
|
|
|
|
void clear() {
|
|
length = 0;
|
|
}
|
|
|
|
// Pick an initial capacity greater than the first part's size.
|
|
// The typical use case has two parts, this is the attempt at
|
|
// guessing the size of the second part without overdoing it.
|
|
// The default estimate of the second part is [defaultOverhead],
|
|
// then round to multiplum of four, and return the result,
|
|
// or [minCapacity] if that is greater.
|
|
static int _initialCapacity(int minCapacity) {
|
|
minCapacity += defaultOverhead;
|
|
if (minCapacity < _NumberBuffer.minCapacity) {
|
|
return _NumberBuffer.minCapacity;
|
|
}
|
|
minCapacity = (minCapacity + 3) & ~3; // Round to multiple of four.
|
|
return minCapacity;
|
|
}
|
|
|
|
// Grows to the exact size asked for.
|
|
void ensureCapacity(int newCapacity) {
|
|
WasmArray<WasmI8> array = this.array;
|
|
if (newCapacity <= array.length) return;
|
|
WasmArray<WasmI8> newArray = WasmArray<WasmI8>(newCapacity);
|
|
newArray.copy(0, array, 0, array.length);
|
|
this.array = newArray;
|
|
}
|
|
|
|
String getString() {
|
|
return _stringFromAsciiBytes(array, 0, length);
|
|
}
|
|
|
|
// TODO(lrn): See if parsing of numbers can be abstracted to something
|
|
// not only working on strings, but also on char-code lists, without losing
|
|
// performance.
|
|
num parseNum() => num.parse(getString());
|
|
double parseDouble() => _parseValidFloat(getString());
|
|
}
|
|
|
|
/**
|
|
* Base class for the common monomorphic parts of all chunked JSON parsers.
|
|
*
|
|
* The members in this class should not be overridden and should not have
|
|
* polymorphism to make sure the generated code for the subclasses is optimal.
|
|
*/
|
|
abstract class _ChunkedJsonParserState {
|
|
final _JsonListener listener;
|
|
|
|
_ChunkedJsonParserState(this.listener);
|
|
|
|
// The current parsing state.
|
|
int state = _ChunkedJsonParser.STATE_INITIAL;
|
|
|
|
WasmArray<WasmI64> states = WasmArray<WasmI64>(0);
|
|
int statesLength = 0;
|
|
|
|
/**
|
|
* Stores tokenizer state between chunks.
|
|
*
|
|
* This state is stored when a chunk stops in the middle of a
|
|
* token (string, numeral, boolean or null).
|
|
*
|
|
* The partial state is used to continue parsing on the next chunk.
|
|
* The previous chunk is not retained, any data needed are stored in
|
|
* this integer, or in the [buffer] field as a string-building buffer
|
|
* or a [_NumberBuffer].
|
|
*
|
|
* Prefix state stored in [prefixState] as bits.
|
|
*
|
|
* ..00 : No partial value (NO_PARTIAL).
|
|
*
|
|
* ..00001 : Partial string, not inside escape.
|
|
* ..00101 : Partial string, after '\'.
|
|
* ..vvvv1dd01 : Partial \u escape.
|
|
* The 'dd' bits (2-3) encode the number of hex digits seen.
|
|
* Bits 5-16 encode the value of the hex digits seen so far.
|
|
*
|
|
* ..0ddd10 : Partial numeral.
|
|
* The `ddd` bits store the parts of in the numeral seen so
|
|
* far, as the constants `NUM_*` defined above.
|
|
* The characters of the numeral are stored in [buffer]
|
|
* as a [_NumberBuffer].
|
|
*
|
|
* ..0ddd0011 : Partial 'null' keyword.
|
|
* ..0ddd0111 : Partial 'true' keyword.
|
|
* ..0ddd1011 : Partial 'false' keyword.
|
|
* ..0ddd1111 : Partial UTF-8 BOM byte sequence ("\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 = _ChunkedJsonParser.NO_PARTIAL;
|
|
|
|
/**
|
|
* String parts stored while parsing a string.
|
|
*/
|
|
StringBuffer? _stringBuffer;
|
|
|
|
@pragma('wasm:prefer-inline')
|
|
StringBuffer get stringBuffer => _stringBuffer ??= StringBuffer();
|
|
|
|
/**
|
|
* Number parts stored while parsing a number.
|
|
*/
|
|
_NumberBuffer? _numberBuffer;
|
|
|
|
@pragma('wasm:prefer-inline')
|
|
_NumberBuffer get numberBuffer =>
|
|
_numberBuffer ??= _NumberBuffer(_NumberBuffer.minCapacity);
|
|
|
|
/**
|
|
* Push the current parse [state] on a stack.
|
|
*
|
|
* State is pushed when a new array or object literal starts,
|
|
* so the parser can go back to the correct value when the literal ends.
|
|
*/
|
|
void saveState(int state) {
|
|
pushWasmArray<WasmI64>(
|
|
states,
|
|
statesLength,
|
|
state.toWasmI64(),
|
|
(statesLength * 2) | 3,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Restore a state pushed with [saveState].
|
|
*/
|
|
int restoreState() {
|
|
assert(statesLength > 0);
|
|
return popWasmArray<WasmI64>(states, statesLength).toInt();
|
|
}
|
|
|
|
/**
|
|
* Read out the result after successfully closing the parser.
|
|
*
|
|
* The parser is closed by calling [close] or calling [addSourceChunk] with
|
|
* `true` as second (`isLast`) argument.
|
|
*/
|
|
dynamic get result => listener.result;
|
|
}
|
|
|
|
/**
|
|
* Chunked JSON parser implementation.
|
|
*
|
|
* This is a mixin instead of a base class to allow compilers to specialize
|
|
* applications otherwise accessing chunk characters becomes polymorphic.
|
|
*/
|
|
mixin _ChunkedJsonParser<T> on _ChunkedJsonParserState {
|
|
// A simple non-recursive state-based parser for JSON.
|
|
//
|
|
// Literal values accepted in states ARRAY_EMPTY, ARRAY_COMMA, OBJECT_COLON
|
|
// and strings also in OBJECT_EMPTY, OBJECT_COMMA.
|
|
// VALUE STRING : , } ] Transitions to
|
|
// EMPTY X X -> END
|
|
// ARRAY_EMPTY X X @ -> ARRAY_VALUE / pop
|
|
// ARRAY_VALUE @ @ -> ARRAY_COMMA / pop
|
|
// ARRAY_COMMA X X -> ARRAY_VALUE
|
|
// OBJECT_EMPTY X @ -> OBJECT_KEY / pop
|
|
// OBJECT_KEY @ -> OBJECT_COLON
|
|
// OBJECT_COLON X X -> OBJECT_VALUE
|
|
// OBJECT_VALUE @ @ -> OBJECT_COMMA / pop
|
|
// OBJECT_COMMA X -> OBJECT_KEY
|
|
// END
|
|
// Starting a new array or object will push the current state. The "pop"
|
|
// above means restoring this state and then marking it as an ended value.
|
|
// X means generic handling, @ means special handling for just that
|
|
// state - that is, values are handled generically, only punctuation
|
|
// cares about the current state.
|
|
// Values for states are chosen so bits 0 and 1 tell whether
|
|
// a string/value is allowed, and setting bits 0 through 2 after a value
|
|
// gets to the next state (not empty, doesn't allow a value).
|
|
|
|
// State building-block constants.
|
|
static const int TOP_LEVEL = 0;
|
|
static const int INSIDE_ARRAY = 1;
|
|
static const int INSIDE_OBJECT = 2;
|
|
static const int AFTER_COLON = 3; // Always inside object.
|
|
|
|
static const int ALLOW_STRING_MASK = 8; // Allowed if zero.
|
|
static const int ALLOW_VALUE_MASK = 4; // Allowed if zero.
|
|
static const int ALLOW_VALUE = 0;
|
|
static const int STRING_ONLY = 4;
|
|
static const int NO_VALUES = 12;
|
|
|
|
// Objects and arrays are "empty" until their first property/element.
|
|
// At this position, they may either have an entry or a close-bracket.
|
|
static const int EMPTY = 0;
|
|
static const int NON_EMPTY = 16;
|
|
static const int EMPTY_MASK = 16; // Empty if zero.
|
|
|
|
// Actual states : Context | Is empty? | Next?
|
|
static const int STATE_INITIAL = TOP_LEVEL | EMPTY | ALLOW_VALUE;
|
|
static const int STATE_END = TOP_LEVEL | NON_EMPTY | NO_VALUES;
|
|
|
|
static const int STATE_ARRAY_EMPTY = INSIDE_ARRAY | EMPTY | ALLOW_VALUE;
|
|
static const int STATE_ARRAY_VALUE = INSIDE_ARRAY | NON_EMPTY | NO_VALUES;
|
|
static const int STATE_ARRAY_COMMA = INSIDE_ARRAY | NON_EMPTY | ALLOW_VALUE;
|
|
|
|
static const int STATE_OBJECT_EMPTY = INSIDE_OBJECT | EMPTY | STRING_ONLY;
|
|
static const int STATE_OBJECT_KEY = INSIDE_OBJECT | NON_EMPTY | NO_VALUES;
|
|
static const int STATE_OBJECT_COLON = AFTER_COLON | NON_EMPTY | ALLOW_VALUE;
|
|
static const int STATE_OBJECT_VALUE = AFTER_COLON | NON_EMPTY | NO_VALUES;
|
|
static const int STATE_OBJECT_COMMA = INSIDE_OBJECT | NON_EMPTY | STRING_ONLY;
|
|
|
|
// Bits set in state after successfully reading a value.
|
|
// This transitions the state to expect the next punctuation.
|
|
static const int VALUE_READ_BITS = NON_EMPTY | NO_VALUES;
|
|
|
|
// Character code constants.
|
|
static const int BACKSPACE = 0x08;
|
|
static const int TAB = 0x09;
|
|
static const int NEWLINE = 0x0a;
|
|
static const int CARRIAGE_RETURN = 0x0d;
|
|
static const int FORM_FEED = 0x0c;
|
|
static const int SPACE = 0x20;
|
|
static const int QUOTE = 0x22;
|
|
static const int PLUS = 0x2b;
|
|
static const int COMMA = 0x2c;
|
|
static const int MINUS = 0x2d;
|
|
static const int DECIMALPOINT = 0x2e;
|
|
static const int SLASH = 0x2f;
|
|
static const int CHAR_0 = 0x30;
|
|
static const int CHAR_9 = 0x39;
|
|
static const int COLON = 0x3a;
|
|
static const int CHAR_E = 0x45;
|
|
static const int LBRACKET = 0x5b;
|
|
static const int BACKSLASH = 0x5c;
|
|
static const int RBRACKET = 0x5d;
|
|
static const int CHAR_a = 0x61;
|
|
static const int CHAR_b = 0x62;
|
|
static const int CHAR_e = 0x65;
|
|
static const int CHAR_f = 0x66;
|
|
static const int CHAR_l = 0x6c;
|
|
static const int CHAR_n = 0x6e;
|
|
static const int CHAR_r = 0x72;
|
|
static const int CHAR_s = 0x73;
|
|
static const int CHAR_t = 0x74;
|
|
static const int CHAR_u = 0x75;
|
|
static const int LBRACE = 0x7b;
|
|
static const int RBRACE = 0x7d;
|
|
|
|
// State of partial value at chunk split.
|
|
static const int NO_PARTIAL = 0;
|
|
static const int PARTIAL_STRING = 1;
|
|
static const int PARTIAL_NUMERAL = 2;
|
|
static const int PARTIAL_KEYWORD = 3;
|
|
static const int MASK_PARTIAL = 3;
|
|
|
|
// Partial states for numerals. Values can be |'ed with PARTIAL_NUMERAL.
|
|
static const int NUM_SIGN = 0; // After initial '-'.
|
|
static const int NUM_ZERO = 4; // After '0' as first digit.
|
|
static const int NUM_DIGIT = 8; // After digit, no '.' or 'e' seen.
|
|
static const int NUM_DOT = 12; // After '.'.
|
|
static const int NUM_DOT_DIGIT = 16; // After a decimal digit (after '.').
|
|
static const int NUM_E = 20; // After 'e' or 'E'.
|
|
static const int NUM_E_SIGN = 24; // After '-' or '+' after 'e' or 'E'.
|
|
static const int NUM_E_DIGIT = 28; // After exponent digit.
|
|
static const int NUM_SUCCESS = 32; // Never stored as partial state.
|
|
|
|
// Partial states for strings.
|
|
static const int STR_PLAIN = 0; // Inside string, but not escape.
|
|
static const int STR_ESCAPE = 4; // After '\'.
|
|
static const int STR_U = 16; // After '\u' and 0-3 hex digits.
|
|
static const int STR_U_COUNT_SHIFT = 2; // Hex digit count in bits 2-3.
|
|
static const int STR_U_VALUE_SHIFT = 5; // Hex digit value in bits 5+.
|
|
|
|
// Partial states for keywords.
|
|
static const int KWD_TYPE_MASK = 12;
|
|
static const int KWD_TYPE_SHIFT = 2;
|
|
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.
|
|
static const int TWO_BIT_MASK = 3;
|
|
|
|
/**
|
|
* Finalizes the parsing.
|
|
*
|
|
* Throws if the source read so far doesn't end up with a complete
|
|
* parsed value. That means it must not be inside a list or object
|
|
* literal, and any partial value read should also be a valid complete
|
|
* value.
|
|
*
|
|
* The only valid partial state is a number that ends in a digit, and
|
|
* only if the number is the entire JSON value being parsed
|
|
* (otherwise it would be inside a list or object).
|
|
* Such a number will be completed. Any other partial state is an error.
|
|
*/
|
|
void close() {
|
|
if (partialState != NO_PARTIAL) {
|
|
int partialType = partialState & MASK_PARTIAL;
|
|
if (partialType == PARTIAL_NUMERAL) {
|
|
int numState = partialState & ~MASK_PARTIAL;
|
|
// A partial number might be a valid number if we know it's done.
|
|
// There is an unnecessary overhead if input is a single number,
|
|
// but this is assumed to be rare.
|
|
finishChunkNumber(numState, 0, 0);
|
|
} else if (partialType == PARTIAL_STRING) {
|
|
fail(chunkEnd, "Unterminated string");
|
|
} else {
|
|
assert(partialType == PARTIAL_KEYWORD);
|
|
fail(chunkEnd); // Incomplete literal.
|
|
}
|
|
}
|
|
if (state != STATE_END) {
|
|
fail(chunkEnd);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Length of current chunk.
|
|
*
|
|
* The valid arguments to [getChar] are 0 .. `chunkEnd - 1`.
|
|
*/
|
|
int get chunkEnd;
|
|
|
|
/**
|
|
* Returns the chunk itself.
|
|
*
|
|
* Only used by [fail] to include the chunk in the thrown [FormatException].
|
|
*/
|
|
T get chunk;
|
|
|
|
/**
|
|
* Get character/code unit of current chunk.
|
|
*
|
|
* The [index] must be non-negative and less than `chunkEnd`.
|
|
* In practice, [index] will be no smaller than the `start` argument passed
|
|
* to [parse].
|
|
*/
|
|
int getChar(int index);
|
|
|
|
/**
|
|
* Returns [true] if [getChar] is returning UTF16 code units.
|
|
*
|
|
* Otherwise it is expected that [getChar] is returning UTF8 bytes.
|
|
*/
|
|
bool get isUtf16Input;
|
|
|
|
/**
|
|
* Copy ASCII characters from start to end of chunk into a list.
|
|
*
|
|
* Used for number buffer (always copies ASCII, so encoding is not important).
|
|
*/
|
|
void copyCharsToList(
|
|
int start,
|
|
int end,
|
|
WasmArray<WasmI8> target,
|
|
int offset,
|
|
);
|
|
|
|
/**
|
|
* Build a string using input code units.
|
|
*
|
|
* Creates a string buffer and enables adding characters and slices
|
|
* to that buffer.
|
|
* The buffer is stored in the [buffer] field. If the string is unterminated,
|
|
* the same buffer is used to continue parsing in the next chunk.
|
|
*/
|
|
void beginString();
|
|
/**
|
|
* Add single character code to string being built.
|
|
*
|
|
* Used for unparsed escape sequences.
|
|
*/
|
|
void addCharToString(int charCode);
|
|
|
|
/**
|
|
* Adds slice of current chunk to string being built.
|
|
*
|
|
* The [start] positions is inclusive, [end] is exclusive.
|
|
* The [bits] is a bit-wise OR of the range of bytes/charcodes.
|
|
* The [codeUnitsCache] is pre-populated from the range of bytes/charcodes.
|
|
*
|
|
* The [codeUnitsCache] may be shorter than the range of bytes/charcodes in
|
|
* which case it cannot be used, the implementation has to populate a separate
|
|
* wasm array for the range.
|
|
*
|
|
* The [codeUnitsCache] should only be used if the [bits] indicate the string
|
|
* is ASCII as in that case the bytes in `[start..end[` we stored in the array
|
|
* are also code units.
|
|
*/
|
|
void addSliceToString(
|
|
int start,
|
|
int end,
|
|
int bits,
|
|
WasmArray<WasmI16> codeUnitsCache,
|
|
);
|
|
|
|
/**
|
|
* Adds a string slice to the string being built.
|
|
*/
|
|
void addStringSliceToString(String string);
|
|
|
|
/** Finalizes the string being built and returns it as a String. */
|
|
String endString();
|
|
|
|
/**
|
|
* Extracts a literal string from a slice of the current chunk.
|
|
*
|
|
* No interpretation of the content is performed, except for converting
|
|
* the source format to string.
|
|
* This can be implemented more or less efficiently depending on the
|
|
* underlying source.
|
|
*
|
|
* This is used for string literals that contain no escapes.
|
|
*/
|
|
String getString(
|
|
int start,
|
|
int end,
|
|
int bits,
|
|
WasmArray<WasmI16> codeUnitsCache,
|
|
);
|
|
|
|
/**
|
|
* Same as [getString] but with [hash] containing the already computed string
|
|
* hash.
|
|
*
|
|
* Used to create string objects for keys in json objects.
|
|
*/
|
|
String getJsonObjectKeyString(int start, int end, int bits, int hash);
|
|
|
|
/**
|
|
* Parse a slice of the current chunk as a double.
|
|
*
|
|
* The format is expected to be correct.
|
|
* This is used by [parseNumber] when the double value cannot be
|
|
* built exactly during parsing.
|
|
*/
|
|
double parseDouble(int start, int end) {
|
|
return _parseValidFloat(getString(start, end, 0x7f, _emptyCodeUnitsCache));
|
|
}
|
|
|
|
/**
|
|
* Continues parsing a partial value.
|
|
*/
|
|
int parsePartial(int position) {
|
|
if (position == chunkEnd) return position;
|
|
int partialState = this.partialState;
|
|
assert(partialState != NO_PARTIAL);
|
|
int partialType = partialState & MASK_PARTIAL;
|
|
this.partialState = NO_PARTIAL;
|
|
partialState = partialState & ~MASK_PARTIAL;
|
|
assert(partialType != 0);
|
|
if (partialType == PARTIAL_STRING) {
|
|
position = parsePartialString(position, partialState);
|
|
} else if (partialType == PARTIAL_NUMERAL) {
|
|
position = parsePartialNumber(position, partialState);
|
|
} else if (partialType == PARTIAL_KEYWORD) {
|
|
position = parsePartialKeyword(position, partialState);
|
|
}
|
|
return position;
|
|
}
|
|
|
|
/**
|
|
* Parses the remainder of a number into the number buffer.
|
|
*
|
|
* Syntax is checked while pasing.
|
|
* Starts at position, which is expected to be the start of the chunk,
|
|
* and returns the index of the first non-number-literal character found,
|
|
* or chunkEnd if the entire chunk is a valid number continuation.
|
|
* Throws if a syntax error is detected.
|
|
*/
|
|
int parsePartialNumber(int position, int state) {
|
|
int start = position;
|
|
// Primitive implementation, can be optimized.
|
|
int end = chunkEnd;
|
|
toBailout:
|
|
{
|
|
if (position == end) break toBailout;
|
|
int char = getChar(position);
|
|
int digit = char ^ CHAR_0;
|
|
if (state == NUM_SIGN) {
|
|
if (digit <= 9) {
|
|
if (digit == 0) {
|
|
state = NUM_ZERO;
|
|
} else {
|
|
state = NUM_DIGIT;
|
|
}
|
|
position++;
|
|
if (position == end) break toBailout;
|
|
char = getChar(position);
|
|
digit = char ^ CHAR_0;
|
|
} else {
|
|
fail(position);
|
|
}
|
|
}
|
|
if (state == NUM_ZERO) {
|
|
// JSON does not allow insignificant leading zeros (e.g., "09").
|
|
if (digit <= 9) fail(position);
|
|
state = NUM_DIGIT;
|
|
}
|
|
while (state == NUM_DIGIT) {
|
|
if (digit > 9) {
|
|
if (char == DECIMALPOINT) {
|
|
state = NUM_DOT;
|
|
} else if ((char | 0x20) == CHAR_e) {
|
|
state = NUM_E;
|
|
} else {
|
|
finishChunkNumber(state, start, position);
|
|
return position;
|
|
}
|
|
}
|
|
position++;
|
|
if (position == end) break toBailout;
|
|
char = getChar(position);
|
|
digit = char ^ CHAR_0;
|
|
}
|
|
if (state == NUM_DOT) {
|
|
if (digit > 9) fail(position);
|
|
state = NUM_DOT_DIGIT;
|
|
}
|
|
while (state == NUM_DOT_DIGIT) {
|
|
if (digit > 9) {
|
|
if ((char | 0x20) == CHAR_e) {
|
|
state = NUM_E;
|
|
} else {
|
|
finishChunkNumber(state, start, position);
|
|
return position;
|
|
}
|
|
}
|
|
position++;
|
|
if (position == end) break toBailout;
|
|
char = getChar(position);
|
|
digit = char ^ CHAR_0;
|
|
}
|
|
if (state == NUM_E) {
|
|
if (char == PLUS || char == MINUS) {
|
|
state = NUM_E_SIGN;
|
|
position++;
|
|
if (position == end) break toBailout;
|
|
char = getChar(position);
|
|
digit = char ^ CHAR_0;
|
|
}
|
|
}
|
|
assert(state >= NUM_E);
|
|
while (digit <= 9) {
|
|
state = NUM_E_DIGIT;
|
|
position++;
|
|
if (position == end) break toBailout;
|
|
char = getChar(position);
|
|
digit = char ^ CHAR_0;
|
|
}
|
|
finishChunkNumber(state, start, position);
|
|
return position;
|
|
}
|
|
// Bailout code in case the current chunk ends while parsing the numeral.
|
|
assert(position == end);
|
|
continueChunkNumber(state, start);
|
|
return chunkEnd;
|
|
}
|
|
|
|
/**
|
|
* Continues parsing a partial string literal.
|
|
*
|
|
* Handles partial escapes and then hands the parsing off to
|
|
* [parseStringToBuffer].
|
|
*/
|
|
int parsePartialString(int position, int partialState) {
|
|
if (partialState == STR_PLAIN) {
|
|
return parseStringToBuffer(position);
|
|
}
|
|
if (partialState == STR_ESCAPE) {
|
|
position = parseStringEscape(position);
|
|
// parseStringEscape sets partialState if it sees the end.
|
|
if (position == chunkEnd) return position;
|
|
return parseStringToBuffer(position);
|
|
}
|
|
assert((partialState & STR_U) != 0);
|
|
int value = partialState >> STR_U_VALUE_SHIFT;
|
|
int count = (partialState >> STR_U_COUNT_SHIFT) & TWO_BIT_MASK;
|
|
for (int i = count; i < 4; i++, position++) {
|
|
if (position == chunkEnd) return chunkStringEscapeU(i, value);
|
|
int char = getChar(position);
|
|
int digit = parseHexDigit(char);
|
|
if (digit < 0) fail(position, "Invalid hex digit");
|
|
value = 16 * value + digit;
|
|
}
|
|
addCharToString(value);
|
|
return parseStringToBuffer(position);
|
|
}
|
|
|
|
/**
|
|
* Continues parsing a partial keyword.
|
|
*/
|
|
int parsePartialKeyword(int position, int partialState) {
|
|
int keywordType = partialState & KWD_TYPE_MASK;
|
|
int count = partialState >> KWD_COUNT_SHIFT;
|
|
int keywordTypeIndex = keywordType >> KWD_TYPE_SHIFT;
|
|
String keyword = const [
|
|
"null",
|
|
"true",
|
|
"false",
|
|
"\xEF\xBB\xBF",
|
|
][keywordTypeIndex];
|
|
assert(count < keyword.length);
|
|
do {
|
|
if (position == chunkEnd) {
|
|
this.partialState =
|
|
PARTIAL_KEYWORD | keywordType | (count << KWD_COUNT_SHIFT);
|
|
return chunkEnd;
|
|
}
|
|
int expectedChar = keyword.codeUnitAtUnchecked(count);
|
|
if (getChar(position) != expectedChar) {
|
|
if (count == 0) {
|
|
assert(keywordType == KWD_BOM);
|
|
return position;
|
|
}
|
|
fail(position);
|
|
}
|
|
position++;
|
|
count++;
|
|
} while (count < keyword.length);
|
|
if (keywordType == KWD_NULL) {
|
|
listener.handleNull();
|
|
} else if (keywordType != KWD_BOM) {
|
|
listener.handleBool(keywordType == KWD_TRUE);
|
|
}
|
|
return position;
|
|
}
|
|
|
|
/** Convert hex-digit to its value. Returns -1 if char is not a hex digit. */
|
|
int parseHexDigit(int char) {
|
|
int digit = char ^ 0x30;
|
|
if (digit <= 9) return digit;
|
|
int letter = (char | 0x20) ^ 0x60;
|
|
// values 1 .. 6 are 'a' through 'f'
|
|
if (letter <= 6 && letter > 0) return letter + 9;
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Parses the current chunk as a chunk of JSON.
|
|
*
|
|
* Starts parsing at [position] and continues until [chunkEnd].
|
|
* Continues parsing where the previous chunk (if any) ended.
|
|
*/
|
|
void parse(int position) {
|
|
int length = chunkEnd;
|
|
if (partialState != NO_PARTIAL) {
|
|
position = parsePartial(position);
|
|
if (position == length) return;
|
|
}
|
|
|
|
int state = this.state;
|
|
outer:
|
|
while (position < length) {
|
|
int char = 0;
|
|
do {
|
|
char = getChar(position);
|
|
if (isUtf16Input && char > 0xFF) {
|
|
break;
|
|
}
|
|
if ((_characterAttributes.readUnsigned(char) & CHAR_WHITESPACE) == 0) {
|
|
break;
|
|
}
|
|
position++;
|
|
if (position >= length) {
|
|
break outer;
|
|
}
|
|
} while (true);
|
|
|
|
switch (char) {
|
|
case QUOTE:
|
|
if ((state & ALLOW_STRING_MASK) != 0) fail(position);
|
|
final calculateHash =
|
|
state == STATE_OBJECT_EMPTY || state == STATE_OBJECT_COMMA;
|
|
state |= VALUE_READ_BITS;
|
|
if (calculateHash) {
|
|
position = parseStringUsedAsKey(position + 1);
|
|
} else {
|
|
position = parseString(position + 1);
|
|
}
|
|
break;
|
|
case LBRACKET:
|
|
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
|
listener.beginArray();
|
|
saveState(state);
|
|
state = STATE_ARRAY_EMPTY;
|
|
position++;
|
|
break;
|
|
case LBRACE:
|
|
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
|
listener.beginObject();
|
|
saveState(state);
|
|
state = STATE_OBJECT_EMPTY;
|
|
position++;
|
|
break;
|
|
case CHAR_n:
|
|
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
|
state |= VALUE_READ_BITS;
|
|
position = parseNull(position);
|
|
break;
|
|
case CHAR_f:
|
|
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
|
state |= VALUE_READ_BITS;
|
|
position = parseFalse(position);
|
|
break;
|
|
case CHAR_t:
|
|
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
|
state |= VALUE_READ_BITS;
|
|
position = parseTrue(position);
|
|
break;
|
|
case COLON:
|
|
if (state != STATE_OBJECT_KEY) fail(position);
|
|
listener.propertyName();
|
|
state = STATE_OBJECT_COLON;
|
|
position++;
|
|
break;
|
|
case COMMA:
|
|
if (state == STATE_OBJECT_VALUE) {
|
|
listener.propertyValue();
|
|
state = STATE_OBJECT_COMMA;
|
|
position++;
|
|
} else if (state == STATE_ARRAY_VALUE) {
|
|
listener.arrayElement();
|
|
state = STATE_ARRAY_COMMA;
|
|
position++;
|
|
} else {
|
|
fail(position);
|
|
}
|
|
break;
|
|
case RBRACKET:
|
|
if (state == STATE_ARRAY_EMPTY) {
|
|
listener.endArray();
|
|
} else if (state == STATE_ARRAY_VALUE) {
|
|
listener.arrayElement();
|
|
listener.endArray();
|
|
} else {
|
|
fail(position);
|
|
}
|
|
state = restoreState() | VALUE_READ_BITS;
|
|
position++;
|
|
break;
|
|
case RBRACE:
|
|
if (state == STATE_OBJECT_EMPTY) {
|
|
listener.endObject();
|
|
} else if (state == STATE_OBJECT_VALUE) {
|
|
listener.propertyValue();
|
|
listener.endObject();
|
|
} else {
|
|
fail(position);
|
|
}
|
|
state = restoreState() | VALUE_READ_BITS;
|
|
position++;
|
|
break;
|
|
default:
|
|
if ((state & ALLOW_VALUE_MASK) != 0) fail(position);
|
|
state |= VALUE_READ_BITS;
|
|
position = parseNumber(char, position);
|
|
break;
|
|
}
|
|
}
|
|
this.state = state;
|
|
}
|
|
|
|
/**
|
|
* Parses a "true" literal starting at [position].
|
|
*
|
|
* The character `source[position]` must be "t".
|
|
*/
|
|
int parseTrue(int position) {
|
|
assert(getChar(position) == CHAR_t);
|
|
if (chunkEnd < position + 4) {
|
|
return parseKeywordPrefix(position, "true", KWD_TRUE);
|
|
}
|
|
if (getChar(position + 1) != CHAR_r ||
|
|
getChar(position + 2) != CHAR_u ||
|
|
getChar(position + 3) != CHAR_e) {
|
|
fail(position);
|
|
}
|
|
listener.handleBool(true);
|
|
return position + 4;
|
|
}
|
|
|
|
/**
|
|
* Parses a "false" literal starting at [position].
|
|
*
|
|
* The character `source[position]` must be "f".
|
|
*/
|
|
int parseFalse(int position) {
|
|
assert(getChar(position) == CHAR_f);
|
|
if (chunkEnd < position + 5) {
|
|
return parseKeywordPrefix(position, "false", KWD_FALSE);
|
|
}
|
|
if (getChar(position + 1) != CHAR_a ||
|
|
getChar(position + 2) != CHAR_l ||
|
|
getChar(position + 3) != CHAR_s ||
|
|
getChar(position + 4) != CHAR_e) {
|
|
fail(position);
|
|
}
|
|
listener.handleBool(false);
|
|
return position + 5;
|
|
}
|
|
|
|
/**
|
|
* Parses a "null" literal starting at [position].
|
|
*
|
|
* The character `source[position]` must be "n".
|
|
*/
|
|
int parseNull(int position) {
|
|
assert(getChar(position) == CHAR_n);
|
|
if (chunkEnd < position + 4) {
|
|
return parseKeywordPrefix(position, "null", KWD_NULL);
|
|
}
|
|
if (getChar(position + 1) != CHAR_u ||
|
|
getChar(position + 2) != CHAR_l ||
|
|
getChar(position + 3) != CHAR_l) {
|
|
fail(position);
|
|
}
|
|
listener.handleNull();
|
|
return position + 4;
|
|
}
|
|
|
|
int parseKeywordPrefix(int position, String chars, int type) {
|
|
assert(getChar(position) == chars.codeUnitAt(0));
|
|
int length = chunkEnd;
|
|
int start = position;
|
|
int count = 1;
|
|
while (++position < length) {
|
|
int char = getChar(position);
|
|
if (char != chars.codeUnitAtUnchecked(count)) fail(start);
|
|
count++;
|
|
}
|
|
this.partialState = PARTIAL_KEYWORD | type | (count << KWD_COUNT_SHIFT);
|
|
return length;
|
|
}
|
|
|
|
static const int CHAR_SIMPLE_STRING_END = 1;
|
|
static const int CHAR_WHITESPACE = 2;
|
|
|
|
/**
|
|
* [_characterAttributes] string was generated using the following code:
|
|
*
|
|
* ```
|
|
* int $(String ch) => ch.codeUnitAt(0);
|
|
* final list = Uint8List(256);
|
|
* for (var i = 0; i < $(' '); i++) {
|
|
* list[i] |= CHAR_SIMPLE_STRING_END;
|
|
* }
|
|
* list[$('"')] |= CHAR_SIMPLE_STRING_END;
|
|
* list[$('\\')] |= CHAR_SIMPLE_STRING_END;
|
|
* list[$(' ')] |= CHAR_WHITESPACE;
|
|
* list[$('\r')] |= CHAR_WHITESPACE;
|
|
* list[$('\n')] |= CHAR_WHITESPACE;
|
|
* list[$('\t')] |= CHAR_WHITESPACE;
|
|
* for (var i = 0; i < 256; i += 64) {
|
|
* for (var v in list.skip(i).take(64)) {
|
|
* print('${v + $(' ')},');
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
static const _characterAttributes = ImmutableWasmArray<WasmI8>.literal([
|
|
33, 33, 33, 33, 33, 33, 33, 33, 33, 35, 35, 33, 33, 35, 33, 33, 33, 33, //
|
|
33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 32, 33, 32, //
|
|
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, //
|
|
32, 32, 32, 32,
|
|
]);
|
|
|
|
/**
|
|
* Parses a string value.
|
|
*
|
|
* Initial [position] is right after the initial quote.
|
|
* Returned position right after the final quote.
|
|
*/
|
|
int parseString(int position) {
|
|
return _parseStringInternal(position, false);
|
|
}
|
|
|
|
/**
|
|
* Same as [parseString] but the caller knows this string will be used as a
|
|
* key in a json object.
|
|
*/
|
|
int parseStringUsedAsKey(int position) {
|
|
return _parseStringInternal(position, true);
|
|
}
|
|
|
|
@pragma('wasm:prefer-inline')
|
|
int _parseStringInternal(int position, bool isJsonObjectKey) {
|
|
// Format: '"'([^\x00-\x1f\\\"]|'\\'[bfnrt/\\"])*'"'
|
|
// Initial position is right after first '"'.
|
|
int start = position;
|
|
int end = chunkEnd;
|
|
int char = 0;
|
|
int bits = 0;
|
|
int hash = 0;
|
|
|
|
// If our input is a JSString then we can obtain substrings using the
|
|
// `js-string:substring` function for it. That means we never have to go
|
|
// via an intermediary `WasmArray<WasmI16>` to create the JS string.
|
|
//
|
|
// If instead our intput is bytes (i.e., fused json+utf8 decoder) then we
|
|
// create JS strings using the `js-string:fromCharCodeArray` API. That
|
|
// requires copying bytes to an intermediary `WasmArray<WasmI16>`. To avoid
|
|
// doing two O(n) traversals over the input (first here in this function and
|
|
// later on when creating the array) - already when we consume the bytes
|
|
// here we opportunistically (i.e., assuming it's a shorter string,
|
|
// assuming it's ASCII) store the bytes into a pre-allocated
|
|
// `WasmArray<WasmI16>`. In the common case this will then avoid the
|
|
// additional O(n) pass.
|
|
//
|
|
// The `!isUtf16Input` condition below checks whether input is bytes (and
|
|
// will be compile-time evaluted to a constant due each parser returning a
|
|
// constant and the compiler copying the mixin for each implementation).
|
|
//
|
|
// Though if the string we currently parse is used as a key in a json
|
|
// object we'll with high likelyhood not need to allocate a JS String. This
|
|
// is due to the fact that we first try to look whether we have a matching
|
|
// string in the string intern cache. We therefore do not need (in the
|
|
// common case) go via an intermediary `WasmArray<WasmI16>`, so we'll
|
|
// not popualte it for json keys.
|
|
//
|
|
// The `!isJsonObjectKey` condition below checks for that case.
|
|
final bool useCharCodeCache = !isUtf16Input && !isJsonObjectKey;
|
|
final WasmArray<WasmI16> codeUnitsCache;
|
|
final int codeUnitsCacheSize;
|
|
if (useCharCodeCache) {
|
|
codeUnitsCache = _codeUnitsCache;
|
|
codeUnitsCacheSize = _codeUnitsCacheSize;
|
|
} else {
|
|
codeUnitsCache = _emptyCodeUnitsCache;
|
|
codeUnitsCacheSize = _emptyCodeUnitsCacheSize;
|
|
}
|
|
int cacheIndex = 0;
|
|
|
|
if (position < end) {
|
|
do {
|
|
// Caveat: do not combine the following two lines together. It helps
|
|
// compiler to generate better code (it currently can't reorder operations
|
|
// to reduce register pressure).
|
|
char = getChar(position);
|
|
bits |= char; // Includes final '"', but that never matters.
|
|
position++;
|
|
if (!isUtf16Input || char <= 0xFF) {
|
|
if ((_characterAttributes.readUnsigned(char) &
|
|
CHAR_SIMPLE_STRING_END) !=
|
|
0) {
|
|
break;
|
|
}
|
|
}
|
|
if (isJsonObjectKey) {
|
|
hash = stringCombineHashes(hash, char);
|
|
}
|
|
if (codeUnitsCacheSize > 0 && cacheIndex < codeUnitsCacheSize) {
|
|
codeUnitsCache.write(cacheIndex++, char);
|
|
}
|
|
} while (position < end);
|
|
if (char == QUOTE) {
|
|
int sliceEnd = position - 1;
|
|
listener.handleString(
|
|
isJsonObjectKey
|
|
? getJsonObjectKeyString(
|
|
start,
|
|
sliceEnd,
|
|
bits,
|
|
stringFinalizeHash(hash),
|
|
)
|
|
: getString(start, sliceEnd, bits, codeUnitsCache),
|
|
);
|
|
return sliceEnd + 1;
|
|
}
|
|
if (char == BACKSLASH) {
|
|
int sliceEnd = position - 1;
|
|
if (start < sliceEnd) {
|
|
final s = getString(start, sliceEnd, bits, codeUnitsCache);
|
|
beginString();
|
|
addStringSliceToString(s);
|
|
} else {
|
|
beginString();
|
|
}
|
|
return parseStringToBuffer(sliceEnd);
|
|
}
|
|
if (char < SPACE) {
|
|
fail(position - 1, "Control character in string");
|
|
}
|
|
}
|
|
beginString();
|
|
if (start < end) {
|
|
addSliceToString(start, end, bits, codeUnitsCache);
|
|
}
|
|
return chunkString(STR_PLAIN);
|
|
}
|
|
|
|
/**
|
|
* Sets up a partial string state.
|
|
*
|
|
* The state is either not inside an escape, or right after a backslash.
|
|
* For partial strings ending inside a Unicode escape, use
|
|
* [chunkStringEscapeU].
|
|
*/
|
|
int chunkString(int stringState) {
|
|
partialState = PARTIAL_STRING | stringState;
|
|
return chunkEnd;
|
|
}
|
|
|
|
/**
|
|
* Sets up a partial string state for a partially parsed Unicode escape.
|
|
*
|
|
* The partial string state includes the current [buffer] and the
|
|
* number of hex digits of the Unicode seen so far (e.g., for `"\u30')
|
|
* the state knows that two digits have been seen, and what their value is.
|
|
*
|
|
* Returns [chunkEnd] so it can be used as part of a return statement.
|
|
*/
|
|
int chunkStringEscapeU(int count, int value) {
|
|
partialState =
|
|
PARTIAL_STRING |
|
|
STR_U |
|
|
(count << STR_U_COUNT_SHIFT) |
|
|
(value << STR_U_VALUE_SHIFT);
|
|
return chunkEnd;
|
|
}
|
|
|
|
/**
|
|
* Parses the remainder of a string literal into a buffer.
|
|
*
|
|
* The buffer is stored in [buffer] and its underlying format depends on
|
|
* the input chunk type. For example UTF-8 decoding happens in the
|
|
* buffer, not in the parser, since all significant JSON characters are ASCII.
|
|
*
|
|
* This function scans through the string literal for escapes, and copies
|
|
* slices of non-escape characters using [addSliceToString].
|
|
*/
|
|
int parseStringToBuffer(int position) {
|
|
int end = chunkEnd;
|
|
int start = position;
|
|
int bits = 0;
|
|
|
|
final bool useCharCodeCache = !isUtf16Input;
|
|
|
|
final WasmArray<WasmI16> codeUnitsCache;
|
|
final int codeUnitsCacheSize;
|
|
if (useCharCodeCache) {
|
|
codeUnitsCache = _codeUnitsCache;
|
|
codeUnitsCacheSize = _codeUnitsCacheSize;
|
|
} else {
|
|
codeUnitsCache = _emptyCodeUnitsCache;
|
|
codeUnitsCacheSize = _emptyCodeUnitsCacheSize;
|
|
}
|
|
|
|
int cacheIndex = 0;
|
|
|
|
while (true) {
|
|
if (position == end) {
|
|
if (position > start) {
|
|
addSliceToString(start, position, bits, codeUnitsCache);
|
|
}
|
|
return chunkString(STR_PLAIN);
|
|
}
|
|
|
|
int char = 0;
|
|
do {
|
|
char = getChar(position);
|
|
bits |= char; // Includes final '"', but that never matters.
|
|
position++;
|
|
if (!isUtf16Input || char <= 0xFF) {
|
|
if ((_characterAttributes.readUnsigned(char) &
|
|
CHAR_SIMPLE_STRING_END) !=
|
|
0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (cacheIndex < codeUnitsCacheSize) {
|
|
codeUnitsCache.write(cacheIndex++, char);
|
|
}
|
|
} while (position < end);
|
|
|
|
if (char < SPACE) {
|
|
fail(position - 1); // Control character in string.
|
|
}
|
|
|
|
if (char == QUOTE) {
|
|
int quotePosition = position - 1;
|
|
if (quotePosition > start) {
|
|
addSliceToString(start, quotePosition, bits, codeUnitsCache);
|
|
}
|
|
listener.handleString(endString());
|
|
return position;
|
|
}
|
|
|
|
if (char != BACKSLASH) {
|
|
continue;
|
|
}
|
|
|
|
// Handle escape.
|
|
if (position - 1 > start) {
|
|
addSliceToString(start, position - 1, bits, codeUnitsCache);
|
|
}
|
|
|
|
if (position == end) return chunkString(STR_ESCAPE);
|
|
position = parseStringEscape(position);
|
|
if (position == end) return position;
|
|
start = position;
|
|
cacheIndex = 0;
|
|
bits = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse a string escape.
|
|
*
|
|
* Position is right after the initial backslash.
|
|
* The following escape is parsed into a character code which is added to
|
|
* the current string buffer using [addCharToString].
|
|
*
|
|
* Returns position after the last character of the escape.
|
|
*/
|
|
int parseStringEscape(int position) {
|
|
int char = getChar(position++);
|
|
int length = chunkEnd;
|
|
switch (char) {
|
|
case CHAR_b:
|
|
char = BACKSPACE;
|
|
break;
|
|
case CHAR_f:
|
|
char = FORM_FEED;
|
|
break;
|
|
case CHAR_n:
|
|
char = NEWLINE;
|
|
break;
|
|
case CHAR_r:
|
|
char = CARRIAGE_RETURN;
|
|
break;
|
|
case CHAR_t:
|
|
char = TAB;
|
|
break;
|
|
case SLASH:
|
|
case BACKSLASH:
|
|
case QUOTE:
|
|
break;
|
|
case CHAR_u:
|
|
int hexStart = position - 1;
|
|
int value = 0;
|
|
for (int i = 0; i < 4; i++) {
|
|
if (position == length) return chunkStringEscapeU(i, value);
|
|
char = getChar(position++);
|
|
int digit = char ^ 0x30;
|
|
value *= 16;
|
|
if (digit <= 9) {
|
|
value += digit;
|
|
} else {
|
|
digit = (char | 0x20) - CHAR_a;
|
|
// digit < 0 || digit > 5
|
|
if (digit.gtU(5)) {
|
|
fail(hexStart, "Invalid unicode escape");
|
|
}
|
|
value += digit + 10;
|
|
}
|
|
}
|
|
char = value;
|
|
break;
|
|
default:
|
|
if (char < SPACE) fail(position, "Control character in string");
|
|
fail(position, "Unrecognized string escape");
|
|
}
|
|
addCharToString(char);
|
|
if (position == length) return chunkString(STR_PLAIN);
|
|
return position;
|
|
}
|
|
|
|
/// Sets up a partial numeral state.
|
|
/// Returns chunkEnd to allow easy one-line bailout tests.
|
|
int beginChunkNumber(int state, int start) {
|
|
int end = chunkEnd;
|
|
int length = end - start;
|
|
numberBuffer.ensureCapacity(length);
|
|
copyCharsToList(start, end, numberBuffer.array, 0);
|
|
numberBuffer.length = length;
|
|
this.partialState = PARTIAL_NUMERAL | state;
|
|
return end;
|
|
}
|
|
|
|
void addNumberChunk(int start, int end, int overhead) {
|
|
int length = end - start;
|
|
int count = numberBuffer.length;
|
|
int newCount = count + length;
|
|
int newCapacity = newCount + overhead;
|
|
numberBuffer.ensureCapacity(newCapacity);
|
|
copyCharsToList(start, end, numberBuffer.array, count);
|
|
numberBuffer.length = newCount;
|
|
}
|
|
|
|
// Continues an already chunked number across an entire chunk.
|
|
int continueChunkNumber(int state, int start) {
|
|
int end = chunkEnd;
|
|
addNumberChunk(start, end, _NumberBuffer.defaultOverhead);
|
|
this.partialState = PARTIAL_NUMERAL | state;
|
|
return end;
|
|
}
|
|
|
|
void finishChunkNumber(int state, int start, int end) {
|
|
if (state == NUM_ZERO) {
|
|
listener.handleIntegerNumber(0);
|
|
numberBuffer.clear();
|
|
return;
|
|
}
|
|
if (end > start) {
|
|
addNumberChunk(start, end, 0);
|
|
}
|
|
if (state == NUM_DIGIT) {
|
|
num value = numberBuffer.parseNum();
|
|
listener.handleNumber(value);
|
|
} else if (state == NUM_DOT_DIGIT || state == NUM_E_DIGIT) {
|
|
listener.handleNumber(numberBuffer.parseDouble());
|
|
} else {
|
|
fail(chunkEnd, "Unterminated number literal");
|
|
}
|
|
numberBuffer.clear();
|
|
}
|
|
|
|
int parseNumber(int char, int position) {
|
|
// Also called on any unexpected character.
|
|
// Format:
|
|
// '-'?('0'|[1-9][0-9]*)('.'[0-9]+)?([eE][+-]?[0-9]+)?
|
|
int start = position;
|
|
int length = chunkEnd;
|
|
// Collects an int value while parsing. Used for both an integer literal,
|
|
// and the exponent part of a double literal.
|
|
// Stored as negative to ensure we can represent -2^63.
|
|
int intValue = 0;
|
|
double doubleValue = 0.0; // Collect double value while parsing.
|
|
// 1 if there is no leading -, -1 if there is.
|
|
int sign = 1;
|
|
bool isDouble = false;
|
|
// Break this block when the end of the number literal is reached.
|
|
// At that time, position points to the next character, and isDouble
|
|
// is set if the literal contains a decimal point or an exponential.
|
|
if (char == MINUS) {
|
|
sign = -1;
|
|
position++;
|
|
if (position == length) return beginChunkNumber(NUM_SIGN, start);
|
|
char = getChar(position);
|
|
}
|
|
int digit = char ^ CHAR_0;
|
|
if (digit > 9) {
|
|
if (sign < 0) {
|
|
fail(position, "Missing expected digit");
|
|
} else {
|
|
// If it doesn't even start out as a numeral.
|
|
fail(position);
|
|
}
|
|
}
|
|
if (digit == 0) {
|
|
position++;
|
|
if (position == length) return beginChunkNumber(NUM_ZERO, start);
|
|
char = getChar(position);
|
|
digit = char ^ CHAR_0;
|
|
// If starting with zero, next character must not be digit.
|
|
if (digit <= 9) fail(position);
|
|
} else {
|
|
int digitCount = 0;
|
|
do {
|
|
if (digitCount >= 18) {
|
|
// Check for overflow.
|
|
// Is 1 if digit is 8 or 9 and sign == 0, or digit is 9 and sign < 0;
|
|
int highDigit = digit >> 3;
|
|
if (sign < 0) highDigit &= digit;
|
|
if (digitCount == 19 || intValue - highDigit < -922337203685477580) {
|
|
isDouble = true;
|
|
// Big value that we know is not trusted to be exact later,
|
|
// forcing reparsing using `double.parse`.
|
|
doubleValue = 9223372036854775808.0;
|
|
}
|
|
}
|
|
intValue = 10 * intValue - digit;
|
|
digitCount++;
|
|
position++;
|
|
if (position == length) return beginChunkNumber(NUM_DIGIT, start);
|
|
char = getChar(position);
|
|
digit = char ^ CHAR_0;
|
|
} while (digit <= 9);
|
|
}
|
|
if (char == DECIMALPOINT) {
|
|
if (!isDouble) {
|
|
isDouble = true;
|
|
doubleValue = (intValue == 0) ? 0.0 : -intValue.toDouble();
|
|
}
|
|
intValue = 0;
|
|
position++;
|
|
if (position == length) return beginChunkNumber(NUM_DOT, start);
|
|
char = getChar(position);
|
|
digit = char ^ CHAR_0;
|
|
if (digit > 9) fail(position);
|
|
do {
|
|
doubleValue = 10.0 * doubleValue + digit;
|
|
intValue -= 1;
|
|
position++;
|
|
if (position == length) return beginChunkNumber(NUM_DOT_DIGIT, start);
|
|
char = getChar(position);
|
|
digit = char ^ CHAR_0;
|
|
} while (digit <= 9);
|
|
}
|
|
if ((char | 0x20) == CHAR_e) {
|
|
if (!isDouble) {
|
|
isDouble = true;
|
|
doubleValue = (intValue == 0) ? 0.0 : -intValue.toDouble();
|
|
intValue = 0;
|
|
}
|
|
position++;
|
|
if (position == length) return beginChunkNumber(NUM_E, start);
|
|
char = getChar(position);
|
|
int expSign = 1;
|
|
int exponent = 0;
|
|
if (((char + 1) | 2) == 0x2e /*+ or -*/ ) {
|
|
expSign = 0x2C - char; // -1 for MINUS, +1 for PLUS
|
|
position++;
|
|
if (position == length) return beginChunkNumber(NUM_E_SIGN, start);
|
|
char = getChar(position);
|
|
}
|
|
digit = char ^ CHAR_0;
|
|
if (digit > 9) {
|
|
fail(position, "Missing expected digit");
|
|
}
|
|
bool exponentOverflow = false;
|
|
do {
|
|
exponent = 10 * exponent + digit;
|
|
if (exponent > 400) exponentOverflow = true;
|
|
position++;
|
|
if (position == length) return beginChunkNumber(NUM_E_DIGIT, start);
|
|
char = getChar(position);
|
|
digit = char ^ CHAR_0;
|
|
} while (digit <= 9);
|
|
if (exponentOverflow) {
|
|
if (doubleValue == 0.0 || expSign < 0) {
|
|
listener.handleNumber(sign < 0 ? -0.0 : 0.0);
|
|
} else {
|
|
listener.handleNumber(
|
|
sign < 0 ? double.negativeInfinity : double.infinity,
|
|
);
|
|
}
|
|
return position;
|
|
}
|
|
intValue += expSign * exponent;
|
|
}
|
|
if (!isDouble) {
|
|
int bitFlag = -(sign + 1) >> 1; // 0 if sign == -1, -1 if sign == 1
|
|
// Negate if bitFlag is -1 by doing ~intValue + 1
|
|
listener.handleIntegerNumber((intValue ^ bitFlag) - bitFlag);
|
|
return position;
|
|
}
|
|
// Double values at or above this value (2 ** 53) may have lost precision.
|
|
// Only trust results that are below this value.
|
|
const double maxExactDouble = 9007199254740992.0;
|
|
if (doubleValue < maxExactDouble) {
|
|
int exponent = intValue;
|
|
double signedMantissa = doubleValue * sign;
|
|
if (exponent >= -22) {
|
|
if (exponent < 0) {
|
|
listener.handleNumber(signedMantissa / POWERS_OF_TEN[-exponent]);
|
|
return position;
|
|
}
|
|
if (exponent == 0) {
|
|
listener.handleNumber(signedMantissa);
|
|
return position;
|
|
}
|
|
if (exponent <= 22) {
|
|
listener.handleNumber(signedMantissa * POWERS_OF_TEN[exponent]);
|
|
return position;
|
|
}
|
|
}
|
|
}
|
|
// If the value is outside the range +/-maxExactDouble or
|
|
// exponent is outside the range +/-22, then we can't trust simple double
|
|
// arithmetic to get the exact result, so we use the system double parsing.
|
|
listener.handleNumber(parseDouble(start, position));
|
|
return position;
|
|
}
|
|
|
|
Never fail(int position, [String? message]) {
|
|
if (message == null) {
|
|
message = "Unexpected character";
|
|
if (position == chunkEnd) message = "Unexpected end of input";
|
|
}
|
|
throw FormatException(message, chunk, position);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Chunked JSON parser that parses [String] chunks.
|
|
*/
|
|
class _StringParser extends _ChunkedJsonParserState
|
|
with _ChunkedJsonParser<String> {
|
|
@pragma('wasm:initialize-at-startup')
|
|
static WasmArray<WasmI16> _emptyChunk = WasmArray<WasmI16>(0);
|
|
|
|
external static WasmArray<WasmI16> stringToCharCodeArray(
|
|
String string,
|
|
int end,
|
|
);
|
|
|
|
String _chunkAsString = '';
|
|
WasmArray<WasmI16> _chunkAsArray = _emptyChunk;
|
|
int chunkEnd = 0;
|
|
|
|
_StringParser(_JsonListener listener) : super(listener);
|
|
|
|
void setNewChunk(String string, int end) {
|
|
_chunkAsString = string;
|
|
_chunkAsArray = stringToCharCodeArray(string, end);
|
|
chunkEnd = end;
|
|
}
|
|
|
|
@override
|
|
String get chunk => _chunkAsString;
|
|
|
|
@pragma('wasm:prefer-inline')
|
|
bool get isUtf16Input => true;
|
|
|
|
@pragma('wasm:prefer-inline')
|
|
int getChar(int position) => _chunkAsArray.readUnsigned(position);
|
|
|
|
@pragma('wasm:prefer-inline')
|
|
String getString(
|
|
int start,
|
|
int end,
|
|
int bits,
|
|
WasmArray<WasmI16> codeUnitsCache,
|
|
) {
|
|
// NOTE: We don't use [codeUnitsCache]. TFA's signature tree shaker should
|
|
// remove its usage and the caller shouldn't even populate anything (due
|
|
// to using [_emptyCodeUnitsCache]).
|
|
return _chunkAsString.substringUnchecked(start, end);
|
|
}
|
|
|
|
String getJsonObjectKeyString(int start, int end, int bits, int stringHash) {
|
|
return _internString(_chunkAsString, _chunkAsArray, stringHash, start, end);
|
|
}
|
|
|
|
void beginString() {
|
|
assert(stringBuffer.isEmpty);
|
|
}
|
|
|
|
void addSliceToString(
|
|
int start,
|
|
int end,
|
|
int bits,
|
|
WasmArray<WasmI16> codeUnitsCache,
|
|
) {
|
|
addStringSliceToString(_chunkAsString.substringUnchecked(start, end));
|
|
}
|
|
|
|
void addStringSliceToString(String string) {
|
|
stringBuffer.write(string);
|
|
}
|
|
|
|
void addCharToString(int charCode) {
|
|
stringBuffer.writeCharCode(charCode);
|
|
}
|
|
|
|
String endString() {
|
|
final string = stringBuffer.toString();
|
|
stringBuffer.clear();
|
|
return string;
|
|
}
|
|
|
|
void copyCharsToList(
|
|
int start,
|
|
int end,
|
|
WasmArray<WasmI8> target,
|
|
int offset,
|
|
) {
|
|
int length = end - start;
|
|
for (int i = 0; i < length; i++) {
|
|
target.write(offset + i, _chunkAsArray.readUnsigned(start + i));
|
|
}
|
|
}
|
|
|
|
double parseDouble(int start, int end) {
|
|
return _parseValidFloat(getString(start, end, 0x7f, _emptyCodeUnitsCache));
|
|
}
|
|
|
|
void close() {
|
|
super.close();
|
|
_stringInternCache.fill(0, null, _stringInternCacheSize);
|
|
}
|
|
}
|
|
|
|
const _emptyCodeUnitsCacheSize = 0;
|
|
@pragma('wasm:initialize-at-startup')
|
|
final _emptyCodeUnitsCache = WasmArray<WasmI16>(_emptyCodeUnitsCacheSize);
|
|
|
|
const int _codeUnitsCacheSize = 512;
|
|
@pragma("wasm:initialize-at-startup")
|
|
final _codeUnitsCache = WasmArray<WasmI16>(_codeUnitsCacheSize);
|
|
|
|
const int _stringInternCacheSize = 512;
|
|
@pragma("wasm:initialize-at-startup")
|
|
final WasmArray<String?> _stringInternCache = WasmArray<String?>(
|
|
_stringInternCacheSize,
|
|
);
|
|
String _internString(
|
|
String source,
|
|
WasmArray<WasmI16> sourceAsArray,
|
|
int stringHash,
|
|
int start,
|
|
int end,
|
|
) {
|
|
final length = end - start;
|
|
final int index = stringHash & (_stringInternCacheSize - 1);
|
|
final existing = _stringInternCache[index];
|
|
|
|
insert:
|
|
{
|
|
if (existing != null) {
|
|
if (existing.length == length) {
|
|
if (getIdentityHashField(existing).toWasmI32() ==
|
|
stringHash.toWasmI32()) {
|
|
for (int j = start, i = 0; i < length; ++i, ++j) {
|
|
if (existing.codeUnitAtUnchecked(i) !=
|
|
sourceAsArray.readUnsigned(j)) {
|
|
break insert;
|
|
}
|
|
}
|
|
return existing;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final result = source.substringUnchecked(start, end);
|
|
setIdentityHashField(result, stringHash);
|
|
_stringInternCache[index] = result;
|
|
return result;
|
|
}
|
|
|
|
String _internJSStringFromAsciiSlice(
|
|
U8List source,
|
|
int stringHash,
|
|
int start,
|
|
int end,
|
|
) {
|
|
final length = end - start;
|
|
final int index = stringHash & (_stringInternCacheSize - 1);
|
|
final existing = _stringInternCache[index];
|
|
|
|
insert:
|
|
{
|
|
if (existing != null) {
|
|
if (getIdentityHashField(existing).toWasmI32() ==
|
|
stringHash.toWasmI32()) {
|
|
if (existing.length == length) {
|
|
for (int j = start, i = 0; i < length; ++i, ++j) {
|
|
if (existing.codeUnitAtUnchecked(i) != source[j]) {
|
|
break insert;
|
|
}
|
|
}
|
|
return existing;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final result = _stringFromAsciiList(source, start, end);
|
|
setIdentityHashField(result, stringHash);
|
|
_stringInternCache[index] = result;
|
|
return result;
|
|
}
|
|
|
|
external String _stringFromCharCodeArray(
|
|
WasmArray<WasmI16> array,
|
|
int start,
|
|
int end,
|
|
);
|
|
|
|
external String _stringFromAsciiBytes(
|
|
WasmArray<WasmI8> source,
|
|
int start,
|
|
int end,
|
|
);
|
|
|
|
String _stringFromAsciiList(U8List source, int start, int end) {
|
|
return _stringFromAsciiBytes(
|
|
source.data,
|
|
source.offsetInElements + start,
|
|
source.offsetInElements + end,
|
|
);
|
|
}
|
|
|
|
@patch
|
|
class JsonDecoder {
|
|
@patch
|
|
StringConversionSink startChunkedConversion(Sink<Object?> sink) =>
|
|
_JsonStringDecoderSink(this._reviver, sink);
|
|
}
|
|
|
|
/**
|
|
* Implements the chunked conversion from a JSON string to its corresponding
|
|
* object.
|
|
*
|
|
* The sink only creates one object, but its input can be chunked.
|
|
*/
|
|
class _JsonStringDecoderSink extends StringConversionSinkBase {
|
|
final _JsonListener _listener;
|
|
|
|
late final _StringParser _stringParser;
|
|
|
|
final Object? Function(Object? key, Object? value)? _reviver;
|
|
|
|
final Sink<Object?> _sink;
|
|
|
|
_JsonStringDecoderSink(this._reviver, this._sink)
|
|
: _listener = _JsonListener(_reviver) {
|
|
_stringParser = _StringParser(_listener);
|
|
}
|
|
|
|
void addSlice(String chunk, int start, int end, bool isLast) {
|
|
final parser = _stringParser;
|
|
parser.setNewChunk(chunk, end);
|
|
parser.parse(start);
|
|
if (isLast) parser.close();
|
|
}
|
|
|
|
void add(String chunk) {
|
|
addSlice(chunk, 0, chunk.length, false);
|
|
}
|
|
|
|
void close() {
|
|
_stringParser.close();
|
|
_sink.add(_listener.result);
|
|
_sink.close();
|
|
}
|
|
|
|
ByteConversionSink asUtf8Sink(bool allowMalformed) =>
|
|
_JsonUtf8DecoderSink(_reviver, _sink, allowMalformed);
|
|
}
|
|
|
|
/**
|
|
* Chunked JSON parser that parses UTF-8 chunks.
|
|
*/
|
|
class _JsonUtf8Parser extends _ChunkedJsonParserState
|
|
with _ChunkedJsonParser<U8List> {
|
|
static final U8List emptyChunk = U8List(0);
|
|
|
|
final _Utf8Decoder decoder;
|
|
U8List chunk = emptyChunk;
|
|
int chunkEnd = 0;
|
|
|
|
_JsonUtf8Parser(_JsonListener listener, bool allowMalformed)
|
|
: decoder = _Utf8Decoder(allowMalformed),
|
|
super(listener) {
|
|
// Starts out checking for an optional BOM (KWD_BOM, count = 0).
|
|
partialState =
|
|
_ChunkedJsonParser.PARTIAL_KEYWORD | _ChunkedJsonParser.KWD_BOM;
|
|
}
|
|
|
|
void parseChunk(List<int> value, int start, int end) {
|
|
if (value is U8List) {
|
|
chunk = value;
|
|
} else {
|
|
final bytes = U8List(end - start);
|
|
bytes.setRange(0, bytes.length, value, start);
|
|
end = bytes.length;
|
|
start = 0;
|
|
chunk = bytes;
|
|
}
|
|
chunkEnd = end;
|
|
parse(start);
|
|
}
|
|
|
|
@pragma('wasm:prefer-inline')
|
|
bool get isUtf16Input => false;
|
|
|
|
@pragma('wasm:prefer-inline')
|
|
int getChar(int position) => chunk.getUnchecked(position);
|
|
|
|
String getString(
|
|
int start,
|
|
int end,
|
|
int bits,
|
|
WasmArray<WasmI16> codeUnitsCache,
|
|
) {
|
|
final int length = end - start;
|
|
if (length == 0) return '';
|
|
|
|
if (bits <= 0x7f) {
|
|
return _stringFromBytesOrCache(start, end, codeUnitsCache);
|
|
}
|
|
beginString();
|
|
addStringSliceToString(decoder.convertChunked(chunk, start, end));
|
|
return endString();
|
|
}
|
|
|
|
String getJsonObjectKeyString(int start, int end, int bits, int stringHash) {
|
|
// We can only rely on [stringHash] if [bits] tell us it was ASCII.
|
|
if (bits <= 0x7f) {
|
|
return _internJSStringFromAsciiSlice(chunk, stringHash, start, end);
|
|
}
|
|
beginString();
|
|
addStringSliceToString(decoder.convertChunked(chunk, start, end));
|
|
return endString();
|
|
}
|
|
|
|
void beginString() {
|
|
assert(stringBuffer.isEmpty);
|
|
decoder.reset();
|
|
}
|
|
|
|
void addSliceToString(
|
|
int start,
|
|
int end,
|
|
int bits,
|
|
WasmArray<WasmI16> codeUnitsCache,
|
|
) {
|
|
if (!decoder.expectsContinuation && bits <= 0x7f) {
|
|
addStringSliceToString(
|
|
_stringFromBytesOrCache(start, end, codeUnitsCache),
|
|
);
|
|
return;
|
|
}
|
|
addStringSliceToString(decoder.convertChunked(chunk, start, end));
|
|
}
|
|
|
|
String _stringFromBytesOrCache(
|
|
int start,
|
|
int end,
|
|
WasmArray<WasmI16> codeUnitsCache,
|
|
) {
|
|
final length = end - start;
|
|
if (length <= codeUnitsCache.length) {
|
|
assert(
|
|
_verifyCacheIsValid(
|
|
codeUnitsCache,
|
|
chunk.data,
|
|
chunk.offsetInElements + start,
|
|
chunk.offsetInElements + end,
|
|
),
|
|
);
|
|
return _stringFromCharCodeArray(codeUnitsCache, 0, length);
|
|
}
|
|
return _stringFromAsciiList(chunk, start, end);
|
|
}
|
|
|
|
void addStringSliceToString(String string) {
|
|
stringBuffer.write(string);
|
|
}
|
|
|
|
void addCharToString(int charCode) {
|
|
decoder.flush(stringBuffer);
|
|
stringBuffer.writeCharCode(charCode);
|
|
}
|
|
|
|
String endString() {
|
|
decoder.flush(stringBuffer);
|
|
final string = stringBuffer.toString();
|
|
stringBuffer.clear();
|
|
return string;
|
|
}
|
|
|
|
void copyCharsToList(
|
|
int start,
|
|
int end,
|
|
WasmArray<WasmI8> target,
|
|
int offset,
|
|
) {
|
|
int length = end - start;
|
|
target.copy(offset, chunk.data, start, length);
|
|
}
|
|
|
|
double parseDouble(int start, int end) {
|
|
final string = _stringFromAsciiList(chunk, start, end);
|
|
return _parseValidFloat(string);
|
|
}
|
|
}
|
|
|
|
bool _verifyCacheIsValid(
|
|
WasmArray<WasmI16> codeUnitsCache,
|
|
WasmArray<WasmI8> source,
|
|
int start,
|
|
int end,
|
|
) {
|
|
final length = end - start;
|
|
assert(length <= codeUnitsCache.length);
|
|
for (int i = 0; i < length && i < codeUnitsCache.length; ++i) {
|
|
if (source.readUnsigned(start + i) != codeUnitsCache.readUnsigned(i)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Implements the chunked conversion from a UTF-8 encoding of JSON
|
|
* to its corresponding object.
|
|
*/
|
|
class _JsonUtf8DecoderSink extends ByteConversionSink {
|
|
final _JsonUtf8Parser _parser;
|
|
final Sink<Object?> _sink;
|
|
|
|
_JsonUtf8DecoderSink(reviver, this._sink, bool allowMalformed)
|
|
: _parser = _createParser(reviver, allowMalformed);
|
|
|
|
static _JsonUtf8Parser _createParser(
|
|
Object? Function(Object? key, Object? value)? reviver,
|
|
bool allowMalformed,
|
|
) => _JsonUtf8Parser(_JsonListener(reviver), allowMalformed);
|
|
|
|
void addSlice(List<int> chunk, int start, int end, bool isLast) {
|
|
_addChunk(chunk, start, end);
|
|
if (isLast) close();
|
|
}
|
|
|
|
void add(List<int> chunk) {
|
|
_addChunk(chunk, 0, chunk.length);
|
|
}
|
|
|
|
void _addChunk(List<int> chunk, int start, int end) {
|
|
_parser.parseChunk(chunk, start, end);
|
|
}
|
|
|
|
void close() {
|
|
_parser.close();
|
|
var decoded = _parser.result;
|
|
_sink.add(decoded);
|
|
_sink.close();
|
|
}
|
|
}
|
|
|
|
@patch
|
|
class _Utf8Decoder {
|
|
/// Flags indicating presence of the various kinds of bytes in the input.
|
|
int _scanFlags = 0;
|
|
|
|
/// How many bytes of the BOM have been read so far. Set to -1 when the BOM
|
|
/// has been skipped (or was not present).
|
|
int _bomIndex = 0;
|
|
|
|
// Table for the scanning phase, which quickly scans through the input.
|
|
//
|
|
// Each input byte is looked up in the table, providing a size and some flags.
|
|
// The sizes are summed, and the flags are or'ed together.
|
|
//
|
|
// The resulting size and flags indicate:
|
|
// A) How many UTF-16 code units will be emitted by the decoding of this
|
|
// input. This can be used to allocate a string of the correct length up
|
|
// front.
|
|
// B) Which decoder and resulting string representation is appropriate. There
|
|
// are three cases:
|
|
// 1) Pure ASCII (flags == 0): The input can simply be put into a
|
|
// OneByteString without further decoding.
|
|
// 2) Latin1 (flags == (flagLatin1 | flagExtension)): The result can be
|
|
// represented by a OneByteString, and the decoder can assume that only
|
|
// Latin1 characters are present.
|
|
// 3) Arbitrary input (otherwise): Needs a full-featured decoder. Output
|
|
// can be represented by a TwoByteString.
|
|
|
|
static const int sizeMask = 0x03;
|
|
static const int flagsMask = 0x3C;
|
|
|
|
static const int flagExtension = 1 << 2;
|
|
static const int flagLatin1 = 1 << 3;
|
|
static const int flagNonLatin1 = 1 << 4;
|
|
static const int flagIllegal = 1 << 5;
|
|
|
|
// ASCII 'A' = 64 + (1);
|
|
// Extension 'D' = 64 + (0 | flagExtension);
|
|
// Latin1 'I' = 64 + (1 | flagLatin1);
|
|
// BMP 'Q' = 64 + (1 | flagNonLatin1);
|
|
// Non-BMP 'R' = 64 + (2 | flagNonLatin1);
|
|
// Illegal 'a' = 64 + (1 | flagIllegal);
|
|
// Illegal 'b' = 64 + (2 | flagIllegal);
|
|
static const scanTable = ImmutableWasmArray<WasmI8>.literal([
|
|
// 00-1F
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,
|
|
|
|
// 20-3F
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,
|
|
|
|
// 40-5F
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,
|
|
|
|
// 60-7F
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65,
|
|
|
|
// 80-9F
|
|
68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68,
|
|
68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68,
|
|
|
|
// A0-BF
|
|
68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68,
|
|
68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68,
|
|
|
|
// C0-DF
|
|
97, 97, 73, 73, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
|
|
81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81,
|
|
|
|
// E0-FF
|
|
81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 82, 82, 82,
|
|
82, 82, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98, 98,
|
|
]);
|
|
|
|
external String? _convertSingleFastPath(
|
|
List<int> codeUnits,
|
|
int start,
|
|
int end,
|
|
);
|
|
|
|
/// Reset the decoder to a state where it is ready to decode a new string but
|
|
/// will not skip a leading BOM. Used by the fused UTF-8 / JSON decoder.
|
|
void reset() {
|
|
_state = initial;
|
|
_bomIndex = -1;
|
|
}
|
|
|
|
int scan(WasmArray<WasmI8> bytes, int start, int end) {
|
|
int size = 0;
|
|
int flags = 0;
|
|
int asciiCharacterIndex = 0;
|
|
for (int i = start; i < end; ++i) {
|
|
final byte = bytes.readUnsigned(i);
|
|
final int t = scanTable.readUnsigned(byte);
|
|
size += t & sizeMask;
|
|
flags |= t;
|
|
_characterArray.write(asciiCharacterIndex++, byte);
|
|
}
|
|
_scanFlags |= flags & flagsMask;
|
|
return size;
|
|
}
|
|
|
|
// The VM decoder handles BOM explicitly instead of via the state machine.
|
|
@patch
|
|
_Utf8Decoder(this.allowMalformed) : _state = initial;
|
|
|
|
@patch
|
|
String convertSingle(List<int> codeUnits, int start, int? maybeEnd) {
|
|
int end = RangeErrorUtils.checkValidRange(
|
|
start,
|
|
maybeEnd,
|
|
codeUnits.length,
|
|
);
|
|
if (start == end) return "";
|
|
|
|
if (_convertSingleFastPath(codeUnits, start, end) case final converted?) {
|
|
// Platform-specific fast path
|
|
return converted;
|
|
}
|
|
|
|
final WasmArray<WasmI8> bytes;
|
|
int errorOffset = 0;
|
|
if (codeUnits is U8List) {
|
|
final u8list = unsafeCast<U8List>(codeUnits);
|
|
bytes = u8list.data;
|
|
final offsetInBytes = u8list.offsetInBytes;
|
|
errorOffset = -start - offsetInBytes;
|
|
start += offsetInBytes;
|
|
end += offsetInBytes;
|
|
} else {
|
|
// If we're passed a `List<int>` other than `U8List` or a JS typed array,
|
|
// it means the performance is not too important. Convert the input to
|
|
// `U8List` to avoid shipping another UTF-8 decoder for `List<int>`.
|
|
final length = end - start;
|
|
bytes = WasmArray(length);
|
|
int bytesIdx = 0;
|
|
for (int codeUnitsIdx = start; codeUnitsIdx < end; codeUnitsIdx += 1) {
|
|
int byte = codeUnits[codeUnitsIdx];
|
|
// byte < 0 || byte > 255
|
|
if (byte.gtU(255)) {
|
|
if (allowMalformed) {
|
|
byte = 0xFF;
|
|
} else {
|
|
throw FormatException(
|
|
'Invalid UTF-8 byte',
|
|
codeUnits,
|
|
codeUnitsIdx,
|
|
);
|
|
}
|
|
}
|
|
bytes.write(bytesIdx++, byte);
|
|
}
|
|
start = 0;
|
|
end = length;
|
|
errorOffset = 0;
|
|
}
|
|
|
|
// Skip initial BOM.
|
|
start += skipBomSingle(bytes, start, end);
|
|
|
|
final int length = end - start;
|
|
if (length < _characterArraySize) {
|
|
return _convertChunkInternal(
|
|
bytes,
|
|
start,
|
|
end,
|
|
codeUnits,
|
|
errorOffset,
|
|
false,
|
|
);
|
|
}
|
|
|
|
String result = '';
|
|
const maxBytes = _maxChunkSizeInBytes;
|
|
final int chunks = length ~/ maxBytes;
|
|
final int remaining = length % maxBytes;
|
|
for (int i = 0; i < chunks; ++i) {
|
|
result += _convertChunkInternal(
|
|
bytes,
|
|
start,
|
|
start + maxBytes,
|
|
codeUnits,
|
|
errorOffset,
|
|
true,
|
|
);
|
|
start += maxBytes;
|
|
}
|
|
if (remaining > 0) {
|
|
assert(remaining < maxBytes);
|
|
result += _convertChunkInternal(
|
|
bytes,
|
|
start,
|
|
end,
|
|
codeUnits,
|
|
errorOffset,
|
|
true,
|
|
);
|
|
start += remaining;
|
|
}
|
|
assert(start == end);
|
|
return result;
|
|
}
|
|
|
|
@patch
|
|
String convertChunked(List<int> codeUnits, int start, int? maybeEnd) {
|
|
int end = RangeErrorUtils.checkValidRange(
|
|
start,
|
|
maybeEnd,
|
|
codeUnits.length,
|
|
);
|
|
if (start == end) return "";
|
|
|
|
final WasmArray<WasmI8> bytes;
|
|
int errorOffset = 0;
|
|
if (codeUnits is U8List) {
|
|
final u8list = unsafeCast<U8List>(codeUnits);
|
|
bytes = u8list.data;
|
|
final offsetInBytes = u8list.offsetInBytes;
|
|
errorOffset = -start - offsetInBytes;
|
|
start += offsetInBytes;
|
|
end += offsetInBytes;
|
|
} else {
|
|
bytes = _makeI8Array(codeUnits, start, end);
|
|
errorOffset = -start;
|
|
end -= start;
|
|
start = 0;
|
|
}
|
|
|
|
// Skip initial BOM.
|
|
start += skipBomChunked(bytes, start, end);
|
|
|
|
final int length = end - start;
|
|
if (length < _characterArraySize) {
|
|
return _convertChunkInternal(
|
|
bytes,
|
|
start,
|
|
end,
|
|
codeUnits,
|
|
errorOffset,
|
|
true,
|
|
);
|
|
}
|
|
|
|
String result = '';
|
|
const maxBytes = _maxChunkSizeInBytes;
|
|
final int chunks = length ~/ maxBytes;
|
|
final int remaining = length % maxBytes;
|
|
for (int i = 0; i < chunks; ++i) {
|
|
result += _convertChunkInternal(
|
|
bytes,
|
|
start,
|
|
start + maxBytes,
|
|
codeUnits,
|
|
errorOffset,
|
|
true,
|
|
);
|
|
start += maxBytes;
|
|
}
|
|
if (remaining > 0) {
|
|
assert(remaining < maxBytes);
|
|
result += _convertChunkInternal(
|
|
bytes,
|
|
start,
|
|
end,
|
|
codeUnits,
|
|
errorOffset,
|
|
true,
|
|
);
|
|
start += remaining;
|
|
}
|
|
assert(start == end);
|
|
return result;
|
|
}
|
|
|
|
@pragma('wasm:initialize-at-startup')
|
|
static final _characterArray = WasmArray<WasmI16>(_characterArraySize);
|
|
static const _characterArraySize = 1025;
|
|
|
|
// We use the pre-allocated [_characterArray] to avoid temporary allocations
|
|
// of `WasmArray<WasmI16>` objects when calling
|
|
// `wasm:js-string.fromCharCodeArray` builtin.
|
|
//
|
|
// This constant defines the maximum size of a chunk of utf-8 encoded
|
|
// bytes that will - when decoded to UTF-16 code units - fit into the
|
|
// [_characterArray].
|
|
//
|
|
// The reason we subtract one from the [_characterArraySize] is the following:
|
|
// When processing a chunk of N bytes we may have a carryover state from the
|
|
// previous chunk that contains an uninished unicode point that requires
|
|
// encoding it as a surrogate pair (i.e. 2 UTF-16 code units).
|
|
//
|
|
// So when e.g. processing 1024 bytes, the first byte may finish a unicode
|
|
// point that requires 2 UTF-16 code units, the remaining 1023 bytes may be
|
|
// ascii. So the 1024 byte chunk may turn into 1025 UTF-16 code units.
|
|
//
|
|
// => The [_characterArraySize] must be one larger than
|
|
// [_maxChunkSizeInBytes].
|
|
static const _maxChunkSizeInBytes = _characterArraySize - 1;
|
|
|
|
@pragma('wasm:prefer-inline')
|
|
String _convertChunkInternal(
|
|
WasmArray<WasmI8> bytes,
|
|
int start,
|
|
int end,
|
|
List<int> bytesForError,
|
|
int errorOffset,
|
|
bool isChunk,
|
|
) {
|
|
assert(0 <= start && start <= end);
|
|
assert(0 <= end && end <= bytes.length);
|
|
|
|
if (start == end) return "";
|
|
|
|
// Scan input to determine size and appropriate decoder.
|
|
int size = scan(bytes, start, end);
|
|
int flags = _scanFlags;
|
|
|
|
final int carryOverState = _state;
|
|
final int carryOverChar = _charOrIndex;
|
|
if (isChunk) {
|
|
// Adjust scan flags and size based on carry-over state.
|
|
switch (_state) {
|
|
case IA:
|
|
break;
|
|
case X1:
|
|
flags |= _charOrIndex < (0x100 >> 6) ? flagLatin1 : flagNonLatin1;
|
|
if (end - start >= 1) {
|
|
size += _charOrIndex < (0x10000 >> 6) ? 1 : 2;
|
|
}
|
|
break;
|
|
case X2:
|
|
flags |= flagNonLatin1;
|
|
if (end - start >= 2) {
|
|
size += _charOrIndex < (0x10000 >> 12) ? 1 : 2;
|
|
}
|
|
break;
|
|
case TO:
|
|
case TS:
|
|
flags |= flagNonLatin1;
|
|
if (end - start >= 2) size += 1;
|
|
break;
|
|
case X3:
|
|
case QO:
|
|
case QR:
|
|
flags |= flagNonLatin1;
|
|
if (end - start >= 3) size += 2;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (flags == 0) {
|
|
// Pure ASCII.
|
|
assert(_state == accept);
|
|
assert(size == end - start);
|
|
return decodeAscii(bytes, start, end, size);
|
|
}
|
|
|
|
// Do not include any final, incomplete character in size.
|
|
int extensionCount = 0;
|
|
int i = end - 1;
|
|
while (i >= start && (bytes.readUnsigned(i) & 0xC0) == 0x80) {
|
|
extensionCount++;
|
|
i--;
|
|
}
|
|
if (i >= start &&
|
|
bytes.readUnsigned(i) >= ((~0x3F >> extensionCount) & 0xFF)) {
|
|
size -= bytes.readUnsigned(i) >= 0xF0 ? 2 : 1;
|
|
}
|
|
|
|
String result;
|
|
if (flags == (flagLatin1 | flagExtension)) {
|
|
// Latin1.
|
|
result = decode8(bytes, start, end, size);
|
|
} else {
|
|
// Arbitrary Unicode.
|
|
result = decode16(bytes, start, end, size);
|
|
}
|
|
if (isChunk) {
|
|
if (!isErrorState(_state)) {
|
|
return result;
|
|
}
|
|
assert(_bomIndex == -1);
|
|
|
|
if (!allowMalformed) {
|
|
final String message = errorDescription(_state);
|
|
_state = initial; // Ready for more input.
|
|
throw FormatException(
|
|
message,
|
|
bytesForError,
|
|
errorOffset + _charOrIndex,
|
|
);
|
|
}
|
|
} else {
|
|
if (_state == accept) {
|
|
return result;
|
|
}
|
|
|
|
if (!allowMalformed) {
|
|
if (!isErrorState(_state)) {
|
|
// Unfinished sequence.
|
|
_state = errorUnfinished;
|
|
_charOrIndex = end;
|
|
}
|
|
final String message = errorDescription(_state);
|
|
throw FormatException(
|
|
message,
|
|
bytesForError,
|
|
errorOffset + _charOrIndex,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Start over on slow path.
|
|
_state = carryOverState;
|
|
_charOrIndex = carryOverChar;
|
|
result = _decodeGeneral(bytes, start, end, !isChunk);
|
|
assert(!isErrorState(_state));
|
|
return result;
|
|
}
|
|
|
|
@pragma("wasm:prefer-inline")
|
|
int skipBomSingle(WasmArray<WasmI8> bytes, int start, int end) {
|
|
if (end - start >= 3 &&
|
|
bytes.readUnsigned(start) == 0xEF &&
|
|
bytes.readUnsigned(start + 1) == 0xBB &&
|
|
bytes.readUnsigned(start + 2) == 0xBF) {
|
|
return 3;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
@pragma("wasm:prefer-inline")
|
|
int skipBomChunked(WasmArray<WasmI8> bytes, int start, int end) {
|
|
assert(start <= end);
|
|
int bomIndex = _bomIndex;
|
|
// Already skipped?
|
|
if (bomIndex == -1) return 0;
|
|
|
|
const bomValues = ImmutableWasmArray<WasmI8>.literal([0xEF, 0xBB, 0xBF]);
|
|
int i = 0;
|
|
while (bomIndex < 3) {
|
|
if (start + i == end) {
|
|
// Unfinished BOM.
|
|
_bomIndex = bomIndex;
|
|
return 0;
|
|
}
|
|
if (bytes.readUnsigned(start + i++) !=
|
|
bomValues.readUnsigned(bomIndex++)) {
|
|
// No BOM.
|
|
_bomIndex = -1;
|
|
return 0;
|
|
}
|
|
}
|
|
// Complete BOM.
|
|
_bomIndex = -1;
|
|
_state = initial;
|
|
return i;
|
|
}
|
|
|
|
String decodeAscii(WasmArray<WasmI8> bytes, int start, int end, int size) {
|
|
assert(size == (end - start));
|
|
assert(start < end);
|
|
assert(size <= _characterArraySize);
|
|
|
|
// In the ascii case the wasm array should be up-to-date already (populated
|
|
// during [scan]).
|
|
assert(
|
|
(() {
|
|
for (int i = 0; i < size; ++i) {
|
|
if (_characterArray.readUnsigned(i) !=
|
|
bytes.readUnsigned(start + i)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
})(),
|
|
);
|
|
return _stringFromCharCodeArray(_characterArray, 0, size);
|
|
}
|
|
|
|
String decode8(WasmArray<WasmI8> bytes, int start, int end, int size) {
|
|
assert(start < end);
|
|
assert(size <= _characterArraySize);
|
|
|
|
int i = start;
|
|
int j = 0;
|
|
if (_state == X1) {
|
|
// Half-way though 2-byte sequence
|
|
assert(_charOrIndex == 2 || _charOrIndex == 3);
|
|
final int e = bytes.readUnsigned(i++) ^ 0x80;
|
|
if (e >= 0x40) {
|
|
_state = errorMissingExtension;
|
|
_charOrIndex = i - 1;
|
|
return "";
|
|
}
|
|
_characterArray.write(j++, ((_charOrIndex << 6) | e) & 0xFF);
|
|
_state = accept;
|
|
}
|
|
assert(_state == accept);
|
|
while (i < end) {
|
|
int byte = bytes.readUnsigned(i++);
|
|
if (byte >= 0x80) {
|
|
if (byte < 0xC0) {
|
|
_state = errorUnexpectedExtension;
|
|
_charOrIndex = i - 1;
|
|
return "";
|
|
}
|
|
assert(byte == 0xC2 || byte == 0xC3);
|
|
if (i == end) {
|
|
_state = X1;
|
|
_charOrIndex = byte & 0x1F;
|
|
break;
|
|
}
|
|
final int e = bytes.readUnsigned(i++) ^ 0x80;
|
|
if (e >= 0x40) {
|
|
_state = errorMissingExtension;
|
|
_charOrIndex = i - 1;
|
|
return "";
|
|
}
|
|
byte = (byte << 6) | e;
|
|
}
|
|
_characterArray.write(j++, byte & 0xFF);
|
|
}
|
|
assert(j == size);
|
|
return _stringFromCharCodeArray(_characterArray, 0, size);
|
|
}
|
|
|
|
// This table is the Wasm array version of `_Utf8Decoder.transitionTable`,
|
|
// refer to the original type for documentation of the values.
|
|
static const _transitionTable = ImmutableWasmArray<WasmI8>.literal([
|
|
32, 0, 48, 58, 88, 69, 67, 67, 67, 67, 67, 78, 58, 108, 68, 98, 32, 0, //
|
|
48, 58, 88, 69, 67, 67, 67, 67, 67, 78, 118, 108, 68, 98, 32, 0, 48, 58, //
|
|
88, 69, 67, 67, 67, 67, 67, 78, 58, 108, 68, 98, 32, 65, 65, 65, 65, 65, //
|
|
0, 0, 0, 0, 0, 65, 65, 65, 65, 65, 48, 48, 48, 48, 48, 65, 65, 65, 65, //
|
|
65, 58, 58, 58, 58, 58, 65, 65, 65, 65, 65, 71, 71, 48, 48, 48, 65, 65, //
|
|
65, 65, 65, 48, 48, 75, 75, 75, 65, 65, 65, 65, 65, 71, 58, 58, 58, 58, //
|
|
65, 65, 65, 65, 65, 58, 73, 73, 73, 73, 65, 65, 65, 65, 65, 48, 48, 48, //
|
|
128, 48, 65, 65, 65, 65, 65, 0, 0, 0, 0, 32, 65, 65, 65, 65, 65,
|
|
]);
|
|
|
|
// This table is the Wasm array version of `_Utf8Decoder.typeTable`,
|
|
// refer to the original type for documentation of the values.
|
|
static const _typeTable = ImmutableWasmArray<WasmI8>.literal([
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, //
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, //
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, //
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, //
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, //
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, //
|
|
65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, //
|
|
65, 65, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, 70, //
|
|
71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 71, 72, 72, //
|
|
72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, //
|
|
72, 72, 72, 72, 72, 72, 72, 73, 72, 72, 72, 74, 69, 69, 66, 66, 66, 66, //
|
|
66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, //
|
|
66, 66, 66, 66, 66, 66, 66, 66, 75, 67, 67, 67, 67, 67, 67, 67, 67, 67, //
|
|
67, 67, 67, 68, 67, 76, 79, 78, 78, 78, 77, 69, 69, 69, 69, 69, 69, 69, //
|
|
69, 69, 69, 69,
|
|
]);
|
|
|
|
String decode16(WasmArray<WasmI8> bytes, int start, int end, int size) {
|
|
assert(start < end);
|
|
assert(size <= _characterArraySize);
|
|
|
|
int i = start;
|
|
int j = 0;
|
|
int state = _state;
|
|
int char;
|
|
|
|
// First byte
|
|
assert(!isErrorState(state));
|
|
final int byte = bytes.readUnsigned(i++);
|
|
final int type = _typeTable.readUnsigned(byte) & typeMask;
|
|
if (state == accept) {
|
|
char = byte & (shiftedByteMask >> type);
|
|
state = _transitionTable.readUnsigned(type);
|
|
} else {
|
|
char = (byte & 0x3F) | (_charOrIndex << 6);
|
|
state = _transitionTable.readUnsigned(state + type);
|
|
}
|
|
|
|
while (i < end) {
|
|
final int byte = bytes.readUnsigned(i++);
|
|
final int type = _typeTable.readUnsigned(byte) & typeMask;
|
|
if (state == accept) {
|
|
if (char >= 0x10000) {
|
|
assert(char < 0x110000);
|
|
_characterArray.write(j++, 0xD7C0 + (char >> 10));
|
|
_characterArray.write(j++, 0xDC00 + (char & 0x3FF));
|
|
} else {
|
|
_characterArray.write(j++, char);
|
|
}
|
|
char = byte & (shiftedByteMask >> type);
|
|
state = _transitionTable.readUnsigned(type);
|
|
} else if (isErrorState(state)) {
|
|
_state = state;
|
|
_charOrIndex = i - 2;
|
|
return "";
|
|
} else {
|
|
char = (byte & 0x3F) | (char << 6);
|
|
state = _transitionTable.readUnsigned(state + type);
|
|
}
|
|
}
|
|
|
|
// Final write?
|
|
if (state == accept) {
|
|
if (char >= 0x10000) {
|
|
assert(char < 0x110000);
|
|
_characterArray.write(j++, 0xD7C0 + (char >> 10));
|
|
_characterArray.write(j++, 0xDC00 + (char & 0x3FF));
|
|
} else {
|
|
_characterArray.write(j++, char);
|
|
}
|
|
} else if (isErrorState(state)) {
|
|
_state = state;
|
|
_charOrIndex = end - 1;
|
|
return "";
|
|
}
|
|
|
|
_state = state;
|
|
_charOrIndex = char;
|
|
|
|
assert(j == size);
|
|
return _stringFromCharCodeArray(_characterArray, 0, size);
|
|
}
|
|
|
|
String _decodeGeneral(
|
|
WasmArray<WasmI8> bytes,
|
|
int start,
|
|
int end,
|
|
bool single,
|
|
) {
|
|
int state = _state;
|
|
int char = _charOrIndex;
|
|
final StringBuffer buffer = StringBuffer();
|
|
int i = start;
|
|
int byte = bytes.readUnsigned(i++);
|
|
loop:
|
|
while (true) {
|
|
multibyte:
|
|
while (true) {
|
|
int type = _typeTable.readUnsigned(byte) & typeMask;
|
|
char = (state <= afterBom)
|
|
? byte & (shiftedByteMask >> type)
|
|
: (byte & 0x3F) | (char << 6);
|
|
state = _transitionTable.readUnsigned(state + type);
|
|
if (state == accept) {
|
|
buffer.writeCharCode(char);
|
|
if (i == end) break loop;
|
|
break multibyte;
|
|
} else if (isErrorState(state)) {
|
|
if (allowMalformed) {
|
|
switch (state) {
|
|
case errorInvalid:
|
|
case errorUnexpectedExtension:
|
|
// A single byte that can't start a sequence.
|
|
buffer.writeCharCode(unicodeReplacementCharacterRune);
|
|
break;
|
|
case errorMissingExtension:
|
|
// Unfinished sequence followed by a byte that can start a
|
|
// sequence.
|
|
buffer.writeCharCode(unicodeReplacementCharacterRune);
|
|
// Re-parse offending byte.
|
|
i -= 1;
|
|
break;
|
|
default:
|
|
// Unfinished sequence followed by a byte that can't start a
|
|
// sequence.
|
|
buffer.writeCharCode(unicodeReplacementCharacterRune);
|
|
buffer.writeCharCode(unicodeReplacementCharacterRune);
|
|
break;
|
|
}
|
|
state = initial;
|
|
} else {
|
|
_state = state;
|
|
_charOrIndex = i - 1;
|
|
return "";
|
|
}
|
|
}
|
|
if (i == end) break loop;
|
|
byte = bytes.readUnsigned(i++);
|
|
}
|
|
|
|
final int markStart = i;
|
|
byte = bytes.readUnsigned(i++);
|
|
if (byte < 128) {
|
|
int markEnd = end;
|
|
while (i < end) {
|
|
byte = bytes.readUnsigned(i++);
|
|
if (byte >= 128) {
|
|
markEnd = i - 1;
|
|
break;
|
|
}
|
|
}
|
|
assert(markStart < markEnd);
|
|
for (int m = markStart; m < markEnd; m++) {
|
|
buffer.writeCharCode(bytes.readUnsigned(m));
|
|
}
|
|
if (markEnd == end) break loop;
|
|
}
|
|
}
|
|
|
|
if (single && state > afterBom) {
|
|
// Unfinished sequence.
|
|
if (allowMalformed) {
|
|
buffer.writeCharCode(unicodeReplacementCharacterRune);
|
|
} else {
|
|
_state = errorUnfinished;
|
|
_charOrIndex = end;
|
|
return "";
|
|
}
|
|
}
|
|
_state = state;
|
|
_charOrIndex = char;
|
|
return buffer.toString();
|
|
}
|
|
}
|
|
|
|
WasmArray<WasmI8> _makeI8Array(List<int> codeUnits, int start, int end) {
|
|
if (codeUnits is WasmListBase) {
|
|
return _makeI8ArrayFromWasmListBase(
|
|
unsafeCast<WasmListBase<int>>(codeUnits),
|
|
start,
|
|
end,
|
|
);
|
|
}
|
|
|
|
if (codeUnits is WasmI8ArrayBase) {
|
|
return _makeI8ArrayFromWasmI8ArrayBase(
|
|
unsafeCast<WasmI8ArrayBase>(codeUnits),
|
|
start,
|
|
end,
|
|
);
|
|
}
|
|
|
|
final int length = end - start;
|
|
final WasmArray<WasmI8> bytes = WasmArray(length);
|
|
for (int i = 0; i < length; i++) {
|
|
int b = codeUnits[start + i];
|
|
if ((b & ~0xFF) != 0) {
|
|
// Replace invalid byte values by FF, which is also invalid.
|
|
b = 0xFF;
|
|
}
|
|
bytes.write(i, b);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
WasmArray<WasmI8> _makeI8ArrayFromWasmListBase(
|
|
WasmListBase<int> codeUnits,
|
|
int start,
|
|
int end,
|
|
) {
|
|
final int length = end - start;
|
|
final WasmArray<WasmI8> bytes = WasmArray(length);
|
|
final WasmArray<Object?> listData = codeUnits.data;
|
|
for (int i = 0; i < length; i++) {
|
|
int b = unsafeCast<int>(listData[start + i]);
|
|
if ((b & ~0xFF) != 0) {
|
|
// Replace invalid byte values by FF, which is also invalid.
|
|
b = 0xFF;
|
|
}
|
|
bytes.write(i, b);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
WasmArray<WasmI8> _makeI8ArrayFromWasmI8ArrayBase(
|
|
WasmI8ArrayBase codeUnits,
|
|
int start,
|
|
int end,
|
|
) {
|
|
final int length = end - start;
|
|
final WasmArray<WasmI8> bytes = WasmArray(length);
|
|
final WasmArray<WasmI8> listData = codeUnits.data;
|
|
final listDataOffset = codeUnits.offsetInBytes;
|
|
for (int i = 0; i < length; i++) {
|
|
int b = listData.readSigned(listDataOffset + start + i);
|
|
if ((b & ~0xFF) != 0) {
|
|
// Replace invalid byte values by FF, which is also invalid.
|
|
b = 0xFF;
|
|
}
|
|
bytes.write(i, b);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
external double _parseValidFloat(String string);
|
|
|
|
const ImmutableWasmArray<BoxedInt> _intBoxes256 = ImmutableWasmArray.literal([
|
|
0, 1, 2, 3, 4, 5, 6, 7, //
|
|
8, 9, 10, 11, 12, 13, 14, 15, //
|
|
16, 17, 18, 19, 20, 21, 22, 23, //
|
|
24, 25, 26, 27, 28, 29, 30, 31, //
|
|
32, 33, 34, 35, 36, 37, 38, 39, //
|
|
40, 41, 42, 43, 44, 45, 46, 47, //
|
|
48, 49, 50, 51, 52, 53, 54, 55, //
|
|
56, 57, 58, 59, 60, 61, 62, 63, //
|
|
64, 65, 66, 67, 68, 69, 70, 71, //
|
|
72, 73, 74, 75, 76, 77, 78, 79, //
|
|
80, 81, 82, 83, 84, 85, 86, 87, //
|
|
88, 89, 90, 91, 92, 93, 94, 95, //
|
|
96, 97, 98, 99, 100, 101, 102, 103, //
|
|
104, 105, 106, 107, 108, 109, 110, 111, //
|
|
112, 113, 114, 115, 116, 117, 118, 119, //
|
|
120, 121, 122, 123, 124, 125, 126, 127, //
|
|
128, 129, 130, 131, 132, 133, 134, 135, //
|
|
136, 137, 138, 139, 140, 141, 142, 143, //
|
|
144, 145, 146, 147, 148, 149, 150, 151, //
|
|
152, 153, 154, 155, 156, 157, 158, 159, //
|
|
160, 161, 162, 163, 164, 165, 166, 167, //
|
|
168, 169, 170, 171, 172, 173, 174, 175, //
|
|
176, 177, 178, 179, 180, 181, 182, 183, //
|
|
184, 185, 186, 187, 188, 189, 190, 191, //
|
|
192, 193, 194, 195, 196, 197, 198, 199, //
|
|
200, 201, 202, 203, 204, 205, 206, 207, //
|
|
208, 209, 210, 211, 212, 213, 214, 215, //
|
|
216, 217, 218, 219, 220, 221, 222, 223, //
|
|
224, 225, 226, 227, 228, 229, 230, 231, //
|
|
232, 233, 234, 235, 236, 237, 238, 239, //
|
|
240, 241, 242, 243, 244, 245, 246, 247, //
|
|
248, 249, 250, 251, 252, 253, 254, 255, //
|
|
]);
|