// Copyright (c) 2019, 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. /// Functions for converting between the different object references we use. library; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; /// Convert [argument] to a form usable in WIP evaluation calls. /// /// The [argument] should be either a RemoteObject or a simple object that can /// be passed through the protocol directly. /// /// Note that this doesn't agree with the Chrome Protocol type CallArgument - /// it's just a Map corresponding to a RemoteObject. But this seems to work /// consistently where the callArgument format doesn't, at least if we're /// using the `arguments` pseudo-variable in JS instead of passing directly /// as real arguments. Map callArgumentFor(Object argument) { if (argument is RemoteObject) { return _isPrimitive(argument) ? _callArgumentForPrimitive(argument.value) : _callArgumentForRemote(argument); } else { return _callArgumentForPrimitive(argument); } } /// True if [remote] represents a primitive bool _isPrimitive(RemoteObject remote) { final id = remote.objectId; return id == null || isStringId(id) || id.startsWith('objects/'); } /// A List of Chrome RemoteObjects from Dart object Ids [dartIds]. /// /// See [remoteObjectFor] for the accepted ID format. List remoteObjectsFor(Iterable dartIds) { return dartIds.map(remoteObjectFor).toList(); } /// A Chrome RemoteObject from a Dart object Id [dartId]. /// /// We expect [dartId] to be one of the following forms. /// * Chrome objectId - e.g. '{"injectedScriptId":1,"id":1}' /// * Our fabricated string Id - e.g. '#StringInstanceRef#actualString' /// * Dart fabricated IDs - e.g. objects/int-8765 /// /// Note that this does NOT accept a Dart library URI, which can be used as an /// InstanceRef identifier in the protocol. Libraries aren't first class, and /// must be handled separately. RemoteObject remoteObjectFor(String dartId) { final data = {}; data['objectId'] = dartId; if (isStringId(dartId)) { data['type'] = 'string'; data['value'] = _stringFromDartId(dartId); } else if (isDoubleId(dartId)) { data['type'] = 'number'; data['value'] = _doubleFromDartId(dartId); } else if (isIntId(dartId)) { data['type'] = 'number'; data['value'] = _intFromDartId(dartId); } else if (isBoolId(dartId)) { data['type'] = 'boolean'; data['value'] = _boolFromDartId(dartId); } else if (dartId == _nullId) { data['type'] = 'undefined'; data['value'] = null; } else { data['type'] = 'object'; } return RemoteObject(data); } /// A Dart object Id appropriate for [argument]. /// /// This will work for simple values, RemoteObject, and Maps representations of /// RemoteObjects. String dartIdFor(Object? argument) { if (argument == null) { return _nullId; } if (argument is double) { return '$_prefixForDoubleIds$argument'; } if (argument is int) { return '$_prefixForIntIds$argument'; } if (argument is bool) { return '$_prefixForBoolIds$argument'; } if (argument is String) { return '$_prefixForStringIds$argument'; } if (argument is RemoteObject) { if (argument.objectId == null) { throw ArgumentError.value(argument, 'objectId', 'No objectId found'); } return argument.objectId!; } if (argument is Map) { final id = argument['objectId'] as String?; if (id == null) { throw ArgumentError.value(argument, 'objectId', 'No objectId found'); } return id; } throw ArgumentError.value(argument, 'objectId', 'No objectId found'); } /// Converts a Dart object Id for a String to the underlying string. /// /// If the ID is not for a String, throws ArgumentError. If you don't know what /// the ID represents, use a more general API like [remoteObjectFor] and if it /// is a primitive, you can get the value from the resulting [RemoteObject]. String stringFromDartId(String dartId) { if (!isStringId(dartId)) { throw ArgumentError.value( dartId, 'dart object ID', 'Expected a valid ID for a String', ); } return _stringFromDartId(dartId); } /// Is [dartId] an Id for a String. bool isStringId(String dartId) => dartId.startsWith(_prefixForStringIds); /// Is [dartId] an Id for a boolean. bool isBoolId(String dartId) => dartId.startsWith(_prefixForBoolIds); /// Is [dartId] an Id for a int. bool isIntId(String dartId) => dartId.startsWith(_prefixForIntIds); /// Is [dartId] an Id for a double. bool isDoubleId(String dartId) => dartId.startsWith(_prefixForDoubleIds); /// Is [dartId] an Id for a Dart library. bool isLibraryId(String dartId) => _uriPrefixes.any(dartId.startsWith); /// A Map representing a RemoteObject for a primitive object. Map _callArgumentForPrimitive(Object? primitive) { return {'type': _jsTypeOf(primitive), 'value': primitive}; } /// A Map representing a RemoteObject from an actual RemoteObject. Map _callArgumentForRemote(RemoteObject remote) { return {'type': 'object', 'objectId': remote.objectId}; } /// The JS type name to use in a RemoteObject reference to [object]. String _jsTypeOf(Object? object) { if (object == null) return 'undefined'; if (object is String) return 'string'; if (object is num) return 'num'; if (object is bool) return 'bool'; return 'object'; } /// Prefixes we use to identify if a Dart ID is a library URI. const _uriPrefixes = ['dart:', 'package:', 'org-dartlang-app:']; /// Convert [dartIdForString] to its corresponding String. String _stringFromDartId(String dartIdForString) => dartIdForString.substring(_prefixForStringIds.length); /// Convert [dartIdForInt] to its corresponding int. int? _intFromDartId(String dartIdForInt) => int.tryParse(dartIdForInt.substring(_prefixForIntIds.length)); /// Convert [dartIdForDouble] to its corresponding double. double? _doubleFromDartId(String dartIdForDouble) => double.tryParse(dartIdForDouble.substring(_prefixForDoubleIds.length)); /// Convert [dartIdForBool] to its corresponding boolean. bool _boolFromDartId(String dartIdForBool) => dartIdForBool.substring(_prefixForBoolIds.length) == 'true'; /// Chrome doesn't give us an objectId for a String. So we use the string /// as its own ID, with a prefix. /// /// This should not be confused with any /// other object Ids, as those will be Chrome objectIds, which are /// opaque, but are JSON serialized objects of the form /// "{\"injectedScriptId\":1,\"id\":1}". const _prefixForStringIds = '#StringInstanceRef#'; /// The prefix the Dart VM uses for ints, followed by a string representation of /// the number. const _prefixForIntIds = 'objects/int-'; /// The prefix we use for doubles, followed by a string representation of /// the number. const _prefixForDoubleIds = 'objects/double-'; /// The prefix the Dart VM uses for booleans, followed by 'true' or 'false'. const _prefixForBoolIds = 'objects/bool-'; /// The object id the Dart VM uses for null. const _nullId = 'objects/null';