blob: 5117fef8e5b6510821b11c78cf4756a592d96720 [file] [log] [blame]
// 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.
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<String, Object?> 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<RemoteObject> remoteObjectsFor(Iterable<String> 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 = <String, Object?>{};
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<String, dynamic>) {
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<String, Object?> _callArgumentForPrimitive(Object? primitive) {
return {'type': _jsTypeOf(primitive), 'value': primitive};
}
/// A Map representing a RemoteObject from an actual RemoteObject.
Map<String, Object?> _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';