| // 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, // |
| ]); |