blob: d7dfb5f61026a56345bc7ad81440edd1efbcaaaf [file] [log] [blame]
// 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 JSArray, JSExtendableArray;
import 'dart:_internal' show MappedIterable, ListIterable;
import 'dart:collection' show 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(key, 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 FormatException(JS<String>('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)) {
walk(e) {
// JavaScript null, string, number, bool are in the correct representation.
if (JS<bool>('bool', '# == null', e) ||
JS<bool>('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>('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>('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 = _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>('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>(
'bool', 'Object.getPrototypeOf(#) !== Array.prototype', object)) {
return _JsonMap(object);
}
// Update the elements in place since JS arrays are Dart lists.
for (int i = 0; i < JS<int>('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 _JsonMapKeyIterable(this);
}
Iterable get values {
if (_isUpgraded) return _upgradedMap.values;
return 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 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 = new JSArray<String>.typed(_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("");
} 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>(
'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>('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 _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<StringBuffer> {
final Object? Function(Object? key, Object? value)? _reviver;
final Sink<Object?> _sink;
_JsonDecoderSink(this._reviver, this._sink) : super(StringBuffer(''));
void close() {
super.close();
String accumulated = _stringSink.toString();
_stringSink.clear();
Object? decoded = _parseJson(accumulated, _reviver);
_sink.add(decoded);
_sink.close();
}
}
@patch
class Utf8Decoder {
// Always fall back to the Dart implementation for strings shorter than this
// threshold, as there is a large, constant overhead for using TextDecoder.
static const int _shortInputThreshold = 15;
@patch
Converter<List<int>, T> fuse<T>(Converter<String, T> next) {
return super.fuse(next);
}
@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>('bool', '# instanceof Uint8Array', codeUnits)) {
// JS 'cast' to avoid a downcast equivalent to the is-check we hand-coded.
NativeUint8List casted =
JS<NativeUint8List>('NativeUint8List', '#', codeUnits);
// Always use Dart implementation for short strings.
end ??= casted.length;
if (end - start < _shortInputThreshold) {
return null;
}
String? result =
_convertInterceptedUint8List(allowMalformed, casted, start, end);
if (result != null && allowMalformed) {
// In principle, TextDecoder should have provided the correct result
// here, but some browsers deviate from the standard as to how many
// replacement characters they produce. Thus, we fall back to the Dart
// implementation if the result contains any replacement characters.
if (JS<int>('int', r'#.indexOf(#)', result, '\uFFFD') >= 0) {
return null;
}
}
return result;
}
return null; // This call was not intercepted.
}
static String? _convertInterceptedUint8List(
bool allowMalformed, NativeUint8List codeUnits, int start, int end) {
final decoder = allowMalformed ? _decoderNonfatal : _decoder;
if (decoder == null) return null;
if (0 == start && end == codeUnits.length) {
return _useTextDecoder(decoder, codeUnits);
}
int length = codeUnits.length;
end = RangeError.checkValidRange(start, end, length);
return _useTextDecoder(
decoder,
JS<NativeUint8List>(
'NativeUint8List', '#.subarray(#, #)', codeUnits, start, end));
}
static String? _useTextDecoder(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>('String', '#.decode(#)', decoder, codeUnits);
} catch (e) {}
return null;
}
// 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 {
return JS('', 'new TextDecoder("utf-8", {fatal: true})');
} catch (e) {}
return null;
}();
static final _decoderNonfatal = () {
try {
return JS('', 'new TextDecoder("utf-8", {fatal: false})');
} catch (e) {}
return null;
}();
}
@patch
class _Utf8Decoder {
@patch
_Utf8Decoder(this.allowMalformed) : _state = beforeBom;
@patch
String convertSingle(List<int> codeUnits, int start, int? maybeEnd) {
return convertGeneral(codeUnits, start, maybeEnd, true);
}
@patch
String convertChunked(List<int> codeUnits, int start, int? maybeEnd) {
return convertGeneral(codeUnits, start, maybeEnd, false);
}
}