| // Copyright (c) 2013, 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. | |
| // Patch file for dart:convert library. | |
| import 'dart:_js_helper' show argumentErrorValue, patch; | |
| import 'dart:_foreign_helper' show JS; | |
| import 'dart:_interceptors' show JSExtendableArray; | |
| import 'dart:_internal' show MappedIterable, ListIterable; | |
| import 'dart:collection' show Maps, LinkedHashMap, MapBase; | |
| import 'dart:_native_typed_data' show NativeUint8List; | |
| /** | |
| * Parses [json] and builds the corresponding parsed JSON value. | |
| * | |
| * Parsed JSON values Nare of the types [num], [String], [bool], [Null], | |
| * [List]s of parsed JSON values or [Map]s from [String] to parsed | |
| * JSON values. | |
| * | |
| * The optional [reviver] function, if provided, is called once for each object | |
| * or list property parsed. The arguments are the property name ([String]) or | |
| * list index ([int]), and the value is the parsed value. The return value of | |
| * the reviver will be used as the value of that property instead of the parsed | |
| * value. The top level value is passed to the reviver with the empty string as | |
| * a key. | |
| * | |
| * Throws [FormatException] if the input is not valid JSON text. | |
| */ | |
| @patch | |
| _parseJson(String source, reviver(Object key, Object value)) { | |
| if (source is! String) throw argumentErrorValue(source); | |
| var parsed; | |
| try { | |
| parsed = JS('=Object|JSExtendableArray|Null|bool|num|String', | |
| 'JSON.parse(#)', source); | |
| } catch (e) { | |
| throw new FormatException(JS('String', 'String(#)', e)); | |
| } | |
| if (reviver == null) { | |
| return _convertJsonToDartLazy(parsed); | |
| } else { | |
| return _convertJsonToDart(parsed, reviver); | |
| } | |
| } | |
| /** | |
| * Walks the raw JavaScript value [json], replacing JavaScript Objects with | |
| * Maps. [json] is expected to be freshly allocated so elements can be replaced | |
| * in-place. | |
| */ | |
| _convertJsonToDart(json, reviver(Object key, Object value)) { | |
| assert(reviver != null); | |
| walk(e) { | |
| // JavaScript null, string, number, bool are in the correct representation. | |
| if (JS('bool', '# == null', e) || JS('bool', 'typeof # != "object"', e)) { | |
| return e; | |
| } | |
| // This test is needed to avoid identifying '{"__proto__":[]}' as an Array. | |
| // TODO(sra): Replace this test with cheaper '#.constructor === Array' when | |
| // bug 621 below is fixed. | |
| if (JS('bool', 'Object.getPrototypeOf(#) === Array.prototype', e)) { | |
| // In-place update of the elements since JS Array is a Dart List. | |
| for (int i = 0; i < JS('int', '#.length', e); i++) { | |
| // Use JS indexing to avoid range checks. We know this is the only | |
| // reference to the list, but the compiler will likely never be able to | |
| // tell that this instance of the list cannot have its length changed by | |
| // the reviver even though it later will be passed to the reviver at the | |
| // outer level. | |
| var item = JS('', '#[#]', e, i); | |
| JS('', '#[#]=#', e, i, reviver(i, walk(item))); | |
| } | |
| return e; | |
| } | |
| // Otherwise it is a plain object, so copy to a JSON map, so we process | |
| // and revive all entries recursively. | |
| _JsonMap map = new _JsonMap(e); | |
| var processed = map._processed; | |
| List<String> keys = map._computeKeys(); | |
| for (int i = 0; i < keys.length; i++) { | |
| String key = keys[i]; | |
| var revived = reviver(key, walk(JS('', '#[#]', e, key))); | |
| JS('', '#[#]=#', processed, key, revived); | |
| } | |
| // Update the JSON map structure so future access is cheaper. | |
| map._original = processed; // Don't keep two objects around. | |
| return map; | |
| } | |
| return reviver(null, walk(json)); | |
| } | |
| _convertJsonToDartLazy(object) { | |
| // JavaScript null and undefined are represented as null. | |
| if (object == null) return null; | |
| // JavaScript string, number, bool already has the correct representation. | |
| if (JS('bool', 'typeof # != "object"', object)) { | |
| return object; | |
| } | |
| // This test is needed to avoid identifying '{"__proto__":[]}' as an array. | |
| // TODO(sra): Replace this test with cheaper '#.constructor === Array' when | |
| // bug https://code.google.com/p/v8/issues/detail?id=621 is fixed. | |
| if (JS('bool', 'Object.getPrototypeOf(#) !== Array.prototype', object)) { | |
| return new _JsonMap(object); | |
| } | |
| // Update the elements in place since JS arrays are Dart lists. | |
| for (int i = 0; i < JS('int', '#.length', object); i++) { | |
| // Use JS indexing to avoid range checks. We know this is the only | |
| // reference to the list, but the compiler will likely never be able to | |
| // tell that this instance of the list cannot have its length changed by | |
| // the reviver even though it later will be passed to the reviver at the | |
| // outer level. | |
| var item = JS('', '#[#]', object, i); | |
| JS('', '#[#]=#', object, i, _convertJsonToDartLazy(item)); | |
| } | |
| return object; | |
| } | |
| class _JsonMap extends MapBase<String, dynamic> { | |
| // The original JavaScript object remains unchanged until | |
| // the map is eventually upgraded, in which case we null it | |
| // out to reclaim the memory used by it. | |
| var _original; | |
| // We keep track of the map entries that we have already | |
| // processed by adding them to a separate JavaScript object. | |
| var _processed = _newJavaScriptObject(); | |
| // If the data slot isn't null, it represents either the list | |
| // of keys (for non-upgraded JSON maps) or the upgraded map. | |
| var _data = null; | |
| _JsonMap(this._original); | |
| operator [](key) { | |
| if (_isUpgraded) { | |
| return _upgradedMap[key]; | |
| } else if (key is! String) { | |
| return null; | |
| } else { | |
| var result = _getProperty(_processed, key); | |
| if (_isUnprocessed(result)) result = _process(key); | |
| return result; | |
| } | |
| } | |
| int get length => _isUpgraded ? _upgradedMap.length : _computeKeys().length; | |
| bool get isEmpty => length == 0; | |
| bool get isNotEmpty => length > 0; | |
| Iterable<String> get keys { | |
| if (_isUpgraded) return _upgradedMap.keys; | |
| return new _JsonMapKeyIterable(this); | |
| } | |
| Iterable get values { | |
| if (_isUpgraded) return _upgradedMap.values; | |
| return new MappedIterable(_computeKeys(), (each) => this[each]); | |
| } | |
| operator []=(key, value) { | |
| if (_isUpgraded) { | |
| _upgradedMap[key] = value; | |
| } else if (containsKey(key)) { | |
| var processed = _processed; | |
| _setProperty(processed, key, value); | |
| var original = _original; | |
| if (!identical(original, processed)) { | |
| _setProperty(original, key, null); // Reclaim memory. | |
| } | |
| } else { | |
| _upgrade()[key] = value; | |
| } | |
| } | |
| void addAll(Map<String, dynamic> other) { | |
| other.forEach((key, value) { | |
| this[key] = value; | |
| }); | |
| } | |
| bool containsValue(value) { | |
| if (_isUpgraded) return _upgradedMap.containsValue(value); | |
| List<String> keys = _computeKeys(); | |
| for (int i = 0; i < keys.length; i++) { | |
| String key = keys[i]; | |
| if (this[key] == value) return true; | |
| } | |
| return false; | |
| } | |
| bool containsKey(key) { | |
| if (_isUpgraded) return _upgradedMap.containsKey(key); | |
| if (key is! String) return false; | |
| return _hasProperty(_original, key); | |
| } | |
| putIfAbsent(key, ifAbsent()) { | |
| if (containsKey(key)) return this[key]; | |
| var value = ifAbsent(); | |
| this[key] = value; | |
| return value; | |
| } | |
| remove(Object key) { | |
| if (!_isUpgraded && !containsKey(key)) return null; | |
| return _upgrade().remove(key); | |
| } | |
| void clear() { | |
| if (_isUpgraded) { | |
| _upgradedMap.clear(); | |
| } else { | |
| if (_data != null) { | |
| // Clear the list of keys to make sure we force | |
| // a concurrent modification error if anyone is | |
| // currently iterating over it. | |
| _data.clear(); | |
| } | |
| _original = _processed = null; | |
| _data = {}; | |
| } | |
| } | |
| void forEach(void f(String key, value)) { | |
| if (_isUpgraded) return _upgradedMap.forEach(f); | |
| List<String> keys = _computeKeys(); | |
| for (int i = 0; i < keys.length; i++) { | |
| String key = keys[i]; | |
| // Compute the value under the assumption that the property | |
| // is present but potentially not processed. | |
| var value = _getProperty(_processed, key); | |
| if (_isUnprocessed(value)) { | |
| value = _convertJsonToDartLazy(_getProperty(_original, key)); | |
| _setProperty(_processed, key, value); | |
| } | |
| // Do the callback. | |
| f(key, value); | |
| // Check if invoking the callback function changed | |
| // the key set. If so, throw an exception. | |
| if (!identical(keys, _data)) { | |
| throw new ConcurrentModificationError(this); | |
| } | |
| } | |
| } | |
| // ------------------------------------------ | |
| // Private helper methods. | |
| // ------------------------------------------ | |
| bool get _isUpgraded => _processed == null; | |
| Map<String, dynamic> get _upgradedMap { | |
| assert(_isUpgraded); | |
| // 'cast' the union type to LinkedHashMap. It would be even better if we | |
| // could 'cast' to the implementation type, since LinkedHashMap includes | |
| // _JsonMap. | |
| return JS('LinkedHashMap', '#', _data); | |
| } | |
| List<String> _computeKeys() { | |
| assert(!_isUpgraded); | |
| List keys = _data; | |
| if (keys == null) { | |
| keys = _data = _getPropertyNames(_original); | |
| } | |
| return JS('JSExtendableArray', '#', keys); | |
| } | |
| Map<String, dynamic> _upgrade() { | |
| if (_isUpgraded) return _upgradedMap; | |
| // Copy all the (key, value) pairs to a freshly allocated | |
| // linked hash map thus preserving the ordering. | |
| var result = <String, dynamic>{}; | |
| List<String> keys = _computeKeys(); | |
| for (int i = 0; i < keys.length; i++) { | |
| String key = keys[i]; | |
| result[key] = this[key]; | |
| } | |
| // We only upgrade when we need to extend the map, so we can | |
| // safely force a concurrent modification error in case | |
| // someone is iterating over the map here. | |
| if (keys.isEmpty) { | |
| keys.add(null); | |
| } else { | |
| keys.clear(); | |
| } | |
| // Clear out the associated JavaScript objects and mark the | |
| // map as having been upgraded. | |
| _original = _processed = null; | |
| _data = result; | |
| assert(_isUpgraded); | |
| return result; | |
| } | |
| _process(String key) { | |
| if (!_hasProperty(_original, key)) return null; | |
| var result = _convertJsonToDartLazy(_getProperty(_original, key)); | |
| return _setProperty(_processed, key, result); | |
| } | |
| // ------------------------------------------ | |
| // Private JavaScript helper methods. | |
| // ------------------------------------------ | |
| static bool _hasProperty(object, String key) => | |
| JS('bool', 'Object.prototype.hasOwnProperty.call(#,#)', object, key); | |
| static _getProperty(object, String key) => JS('', '#[#]', object, key); | |
| static _setProperty(object, String key, value) => | |
| JS('', '#[#]=#', object, key, value); | |
| static List _getPropertyNames(object) => | |
| JS('JSExtendableArray', 'Object.keys(#)', object); | |
| static bool _isUnprocessed(object) => | |
| JS('bool', 'typeof(#)=="undefined"', object); | |
| static _newJavaScriptObject() => JS('=Object', 'Object.create(null)'); | |
| } | |
| class _JsonMapKeyIterable extends ListIterable<String> { | |
| final _JsonMap _parent; | |
| _JsonMapKeyIterable(this._parent); | |
| int get length => _parent.length; | |
| String elementAt(int index) { | |
| return _parent._isUpgraded | |
| ? _parent.keys.elementAt(index) | |
| : _parent._computeKeys()[index]; | |
| } | |
| /// Although [ListIterable] defines its own iterator, we return the iterator | |
| /// of the underlying list [_keys] in order to propagate | |
| /// [ConcurrentModificationError]s. | |
| Iterator<String> get iterator { | |
| return _parent._isUpgraded | |
| ? _parent.keys.iterator | |
| : _parent._computeKeys().iterator; | |
| } | |
| /// Delegate to [parent.containsKey] to ensure the performance expected | |
| /// from [Map.keys.containsKey]. | |
| bool contains(Object key) => _parent.containsKey(key); | |
| } | |
| @patch | |
| class JsonDecoder { | |
| @patch | |
| StringConversionSink startChunkedConversion(Sink<Object> sink) { | |
| return new _JsonDecoderSink(_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. | |
| */ | |
| // TODO(floitsch): don't accumulate everything before starting to decode. | |
| class _JsonDecoderSink extends _StringSinkConversionSink { | |
| final _Reviver _reviver; | |
| final Sink<Object> _sink; | |
| _JsonDecoderSink(this._reviver, this._sink) : super(new StringBuffer('')); | |
| void close() { | |
| super.close(); | |
| StringBuffer buffer = _stringSink; | |
| String accumulated = buffer.toString(); | |
| buffer.clear(); | |
| Object decoded = _parseJson(accumulated, _reviver); | |
| _sink.add(decoded); | |
| _sink.close(); | |
| } | |
| } | |
| @patch | |
| class Utf8Decoder { | |
| @patch | |
| Converter<List<int>, T> fuse<T>(Converter<String, T> next) { | |
| return super.fuse(next); | |
| } | |
| // Currently not intercepting UTF8 decoding. | |
| @patch | |
| static String _convertIntercepted( | |
| bool allowMalformed, List<int> codeUnits, int start, int end) { | |
| // Test `codeUnits is NativeUint8List`. Dart's NativeUint8List is | |
| // implemented by JavaScript's Uint8Array. | |
| if (JS('bool', '# instanceof Uint8Array', codeUnits)) { | |
| // JS 'cast' to avoid a downcast equivalent to the is-check we hand-coded. | |
| NativeUint8List casted = JS('NativeUint8List', '#', codeUnits); | |
| return _convertInterceptedUint8List(allowMalformed, casted, start, end); | |
| } | |
| } | |
| static String _convertInterceptedUint8List( | |
| bool allowMalformed, NativeUint8List codeUnits, int start, int end) { | |
| if (allowMalformed) { | |
| // TextDecoder with option {fatal: false} does not produce the same result | |
| // as [Utf8Decoder]. It disagrees on the number of `U+FFFD` (REPLACEMENT | |
| // CHARACTER) generated for some malformed sequences. We could use | |
| // TextDecoder with option {fatal: true}, catch the error, and re-try | |
| // without acceleration. That turns out to be extremely slow (the Error | |
| // captures a stack trace). | |
| // TODO(31370): Bring Utf8Decoder into alignment with TextDecoder. | |
| // TODO(sra): If we can't do that, can we detect valid input fast enough | |
| // to use a check like the [_unsafe] check below? | |
| return null; | |
| } | |
| var decoder = _decoder; | |
| if (decoder == null) return null; | |
| if (0 == start && end == null) { | |
| return _useTextDecoderChecked(decoder, codeUnits); | |
| } | |
| int length = codeUnits.length; | |
| end = RangeError.checkValidRange(start, end, length); | |
| if (0 == start && end == codeUnits.length) { | |
| return _useTextDecoderChecked(decoder, codeUnits); | |
| } | |
| return _useTextDecoderChecked(decoder, | |
| JS('NativeUint8List', '#.subarray(#, #)', codeUnits, start, end)); | |
| } | |
| static String _useTextDecoderChecked(decoder, NativeUint8List codeUnits) { | |
| if (_unsafe(codeUnits)) return null; | |
| return _useTextDecoderUnchecked(decoder, codeUnits); | |
| } | |
| static String _useTextDecoderUnchecked(decoder, NativeUint8List codeUnits) { | |
| // If the input is malformed, catch the exception and return `null` to fall | |
| // back on unintercepted decoder. The fallback will either succeed in | |
| // decoding, or report the problem better than TextDecoder. | |
| try { | |
| return JS('String', '#.decode(#)', decoder, codeUnits); | |
| } catch (e) {} | |
| return null; | |
| } | |
| /// Returns `true` if [codeUnits] contains problematic encodings. | |
| /// | |
| /// TextDecoder behaves differently to [Utf8Encoder] when the input encodes a | |
| /// surrogate (U+D800 through U+DFFF). TextDecoder considers the surrogate to | |
| /// be an encoding error and, depending on the `fatal` option, either throws | |
| /// and Error or encodes the surrogate as U+FFFD. [Utf8Decoder] does not | |
| /// consider the surrogate to be an error and returns the code unit encoded by | |
| /// the surrogate. | |
| /// | |
| /// Throwing an `Error` captures the stack, whoch makes it so expensive that | |
| /// it is worth checking the input for surrogates and avoiding TextDecoder in | |
| /// this case. | |
| static bool _unsafe(NativeUint8List codeUnits) { | |
| // Surrogates encode as (hex) ED Ax xx or ED Bx xx. | |
| int limit = codeUnits.length - 2; | |
| for (int i = 0; i < limit; i++) { | |
| int unit1 = codeUnits[i]; | |
| if (unit1 == 0xED) { | |
| int unit2 = JS('!', '#', codeUnits[i + 1]); | |
| if ((unit2 & 0xE0) == 0xA0) return true; | |
| } | |
| } | |
| return false; | |
| } | |
| //// TextDecoder is not defined on some browsers and on the stand-alone d8 and | |
| /// jsshell engines. Use a lazy initializer to do feature detection once. | |
| static final _decoder = () { | |
| try { | |
| // Use `{fatal: true}`. 'fatal' does not correspond exactly to | |
| // `!allowMalformed`: TextDecoder rejects unpaired surrogates which | |
| // [Utf8Decoder] accepts. In non-fatal mode, TextDecoder translates | |
| // unpaired surrogates to REPLACEMENT CHARACTER (U+FFFD) whereas | |
| // [Utf8Decoder] leaves the surrogate intact. | |
| return JS('', 'new TextDecoder("utf-8", {fatal: true})'); | |
| } catch (e) {} | |
| return null; | |
| }(); | |
| } |