/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
Log: "chrome://remote/content/shared/Log.sys.mjs",
});
XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
);
/**
* @typedef {Object} OwnershipModel
**/
/**
* Enum of ownership models supported by the serialization.
*
* @readonly
* @enum {OwnershipModel}
**/
export const OwnershipModel = {
None: "none",
Root: "root",
};
function getUUID() {
return Services.uuid
.generateUUID()
.toString()
.slice(1, -1);
}
const TYPED_ARRAY_CLASSES = [
"Uint8Array",
"Uint8ClampedArray",
"Uint16Array",
"Uint32Array",
"Int8Array",
"Int16Array",
"Int32Array",
"Float32Array",
"Float64Array",
"BigInt64Array",
"BigUint64Array",
];
/**
* Build the serialized RemoteValue.
*
* @return {Object}
* An object with a mandatory `type` property, and optional `handle`,
* depending on the OwnershipModel, used for the serialization and
* on the value's type.
*/
function buildSerialized(type, handle = null) {
const serialized = { type };
if (handle !== null) {
serialized.handle = handle;
}
return serialized;
}
/**
* Helper to validate if a date string follows Date Time String format.
*
* @see https://tc39.es/ecma262/#sec-date-time-string-format
*
* @param {string} dateString
* String which needs to be validated.
*
* @throws {InvalidArgumentError}
* If dateString doesn't follow the format.
*/
function checkDateTimeString(dateString) {
// Check if a date string follows a simplification of
// the ISO 8601 calendar date extended format.
const expandedYear = "[+-]\\d{6}";
const year = "\\d{4}";
const YYYY = `${expandedYear}|${year}`;
const MM = "\\d{2}";
const DD = "\\d{2}";
const date = `${YYYY}(?:-${MM})?(?:-${DD})?`;
const HH_mm = "\\d{2}:\\d{2}";
const SS = "\\d{2}";
const sss = "\\d{3}";
const TZ = `Z|[+-]${HH_mm}`;
const time = `T${HH_mm}(?::${SS}(?:\\.${sss})?(?:${TZ})?)?`;
const iso8601Format = new RegExp(`^${date}(?:${time})?$`);
// Check also if a date string is a valid date.
if (Number.isNaN(Date.parse(dateString)) || !iso8601Format.test(dateString)) {
throw new lazy.error.InvalidArgumentError(
`Expected "value" for Date to be a Date Time string, got ${dateString}`
);
}
}
/**
* Helper to deserialize value list.
*
* @see https://w3c.github.io/webdriver-bidi/#deserialize-value-list
*
* @param {Realm} realm
* The Realm in which the value is deserialized.
* @param {Array} serializedValueList
* List of serialized values.
*
* @return {Array} List of deserialized values.
*
* @throws {InvalidArgumentError}
* If serializedValueList is not an array.
*/
function deserializeValueList(realm, serializedValueList) {
lazy.assert.array(
serializedValueList,
`Expected "serializedValueList" to be an array, got ${serializedValueList}`
);
const deserializedValues = [];
for (const item of serializedValueList) {
deserializedValues.push(deserialize(realm, item));
}
return deserializedValues;
}
/**
* Helper to deserialize key-value list.
*
* @see https://w3c.github.io/webdriver-bidi/#deserialize-key-value-list
*
* @param {Realm} realm
* The Realm in which the value is deserialized.
* @param {Array} serializedKeyValueList
* List of serialized key-value.
*
* @return {Array} List of deserialized key-value.
*
* @throws {InvalidArgumentError}
* If serializedKeyValueList is not an array or
* not an array of key-value arrays.
*/
function deserializeKeyValueList(realm, serializedKeyValueList) {
lazy.assert.array(
serializedKeyValueList,
`Expected "serializedKeyValueList" to be an array, got ${serializedKeyValueList}`
);
const deserializedKeyValueList = [];
for (const serializedKeyValue of serializedKeyValueList) {
if (!Array.isArray(serializedKeyValue) || serializedKeyValue.length != 2) {
throw new lazy.error.InvalidArgumentError(
`Expected key-value pair to be an array with 2 elements, got ${serializedKeyValue}`
);
}
const [serializedKey, serializedValue] = serializedKeyValue;
const deserializedKey =
typeof serializedKey == "string"
? serializedKey
: deserialize(realm, serializedKey);
const deserializedValue = deserialize(realm, serializedValue);
deserializedKeyValueList.push([deserializedKey, deserializedValue]);
}
return deserializedKeyValueList;
}
/**
* Deserialize a local value.
*
* @see https://w3c.github.io/webdriver-bidi/#deserialize-local-value
*
* @param {Realm} realm
* The Realm in which the value is deserialized.
* @param {Object} serializedValue
* Value of any type to be deserialized.
*
* @return {Object} Deserialized representation of the value.
*/
export function deserialize(realm, serializedValue) {
const { handle, type, value } = serializedValue;
// With a handle present deserialize as remote reference.
if (handle !== undefined) {
lazy.assert.string(
handle,
`Expected "handle" to be a string, got ${handle}`
);
const object = realm.getObjectForHandle(handle);
if (!object) {
throw new lazy.error.InvalidArgumentError(
`Unable to find an object reference for "handle" ${handle}`
);
}
return object;
}
lazy.assert.string(type, `Expected "type" to be a string, got ${type}`);
// Primitive protocol values
switch (type) {
case "undefined":
return undefined;
case "null":
return null;
case "string":
lazy.assert.string(
value,
`Expected "value" to be a string, got ${value}`
);
return value;
case "number":
// If value is already a number return its value.
if (typeof value === "number") {
return value;
}
// Otherwise it has to be one of the special strings
lazy.assert.in(
value,
["NaN", "-0", "Infinity", "-Infinity"],
`Expected "value" to be one of "NaN", "-0", "Infinity", "-Infinity", got ${value}`
);
return Number(value);
case "boolean":
lazy.assert.boolean(
value,
`Expected "value" to be a boolean, got ${value}`
);
return value;
case "bigint":
lazy.assert.string(
value,
`Expected "value" to be a string, got ${value}`
);
try {
return BigInt(value);
} catch (e) {
throw new lazy.error.InvalidArgumentError(
`Failed to deserialize value as BigInt: ${value}`
);
}
// Non-primitive protocol values
case "array":
const array = realm.cloneIntoRealm([]);
deserializeValueList(realm, value).forEach(v => array.push(v));
return array;
case "date":
// We want to support only Date Time String format,
// check if the value follows it.
checkDateTimeString(value);
return realm.cloneIntoRealm(new Date(value));
case "map":
const map = realm.cloneIntoRealm(new Map());
deserializeKeyValueList(realm, value).forEach(([k, v]) => map.set(k, v));
return map;
case "object":
const object = realm.cloneIntoRealm({});
deserializeKeyValueList(realm, value).forEach(
([k, v]) => (object[k] = v)
);
return object;
case "regexp":
lazy.assert.object(
value,
`Expected "value" for RegExp to be an object, got ${value}`
);
const { pattern, flags } = value;
lazy.assert.string(
pattern,
`Expected "pattern" for RegExp to be a string, got ${pattern}`
);
if (flags !== undefined) {
lazy.assert.string(
flags,
`Expected "flags" for RegExp to be a string, got ${flags}`
);
}
try {
return realm.cloneIntoRealm(new RegExp(pattern, flags));
} catch (e) {
throw new lazy.error.InvalidArgumentError(
`Failed to deserialize value as RegExp: ${value}`
);
}
case "set":
const set = realm.cloneIntoRealm(new Set());
deserializeValueList(realm, value).forEach(v => set.add(v));
return set;
}
lazy.logger.warn(`Unsupported type for local value ${type}`);
return undefined;
}
/**
* Helper to retrieve the handle id for a given object, for the provided realm
* and ownership type.
*
* See https://w3c.github.io/webdriver-bidi/#handle-for-an-object
*
* @param {Realm} realm
* The Realm from which comes the value being serialized.
* @param {OwnershipModel} ownershipType
* The ownership model to use for this serialization.
* @param {Object} object
* The object being serialized.
*
* @return {string} The unique handle id for the object. Will be null if the
* Ownership type is "none".
*/
function getHandleForObject(realm, ownershipType, object) {
if (ownershipType === OwnershipModel.None) {
return null;
}
return realm.getHandleForObject(object);
}
/**
* Helper to serialize an Array-like object.
*
* @see https://w3c.github.io/webdriver-bidi/#serialize-an-array-like
*
* @param {string} production
* Type of object
* @param {string} handleId
* The unique id of the value.
* @param {boolean} knownObject
* Indicates if the value has already been serialized.
* @param {Object} value
* The Array-like object to serialize.
* @param {number|null} maxDepth
* Depth of a serialization.
* @param {OwnershipModel} ownershipType
* The ownership model to use for this serialization.
* @param {Map} serializationInternalMap
* Map of internal ids.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
*
* @return {Object} Object for serialized values.
*/
function serializeArrayLike(
production,
handleId,
knownObject,
value,
maxDepth,
ownershipType,
serializationInternalMap,
realm
) {
const serialized = buildSerialized(production, handleId);
setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
if (!knownObject && maxDepth !== null && maxDepth > 0) {
serialized.value = serializeList(
value,
maxDepth,
ownershipType,
serializationInternalMap,
realm
);
}
return serialized;
}
/**
* Helper to serialize as a list.
*
* @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-list
*
* @param {Iterable} iterable
* List of values to be serialized.
* @param {number|null} maxDepth
* Depth of a serialization.
* @param {OwnershipModel} ownershipType
* The ownership model to use for this serialization.
* @param {Map} serializationInternalMap
* Map of internal ids.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
*
* @return {Array} List of serialized values.
*/
function serializeList(
iterable,
maxDepth,
ownershipType,
serializationInternalMap,
realm
) {
const serialized = [];
const childDepth = maxDepth !== null ? maxDepth - 1 : null;
for (const item of iterable) {
serialized.push(
serialize(
item,
childDepth,
ownershipType,
serializationInternalMap,
realm
)
);
}
return serialized;
}
/**
* Helper to serialize as a mapping.
*
* @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-mapping
*
* @param {Iterable} iterable
* List of values to be serialized.
* @param {number|null} maxDepth
* Depth of a serialization.
* @param {OwnershipModel} ownershipType
* The ownership model to use for this serialization.
* @param {Map} serializationInternalMap
* Map of internal ids.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
*
* @return {Array} List of serialized values.
*/
function serializeMapping(
iterable,
maxDepth,
ownershipType,
serializationInternalMap,
realm
) {
const serialized = [];
const childDepth = maxDepth !== null ? maxDepth - 1 : null;
for (const [key, item] of iterable) {
const serializedKey =
typeof key == "string"
? key
: serialize(
key,
childDepth,
ownershipType,
serializationInternalMap,
realm
);
const serializedValue = serialize(
item,
childDepth,
ownershipType,
serializationInternalMap,
realm
);
serialized.push([serializedKey, serializedValue]);
}
return serialized;
}
/**
* Helper to serialize as a Node.
*
* @param {Node} node
* Node to be serialized.
* @param {number|null} maxDepth
* Depth of a serialization.
* @param {OwnershipModel} ownershipType
* The ownership model to use for this serialization.
* @param {Map} serializationInternalMap
* Map of internal ids.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
*
* @return {Object} Serialized value.
*/
function serializeNode(
node,
maxDepth,
ownershipType,
serializationInternalMap,
realm
) {
const isAttribute = Attr.isInstance(node);
const isElement = Element.isInstance(node);
const serialized = {
nodeType: node.nodeType,
};
if (node.nodeValue !== null) {
serialized.nodeValue = node.nodeValue;
}
if (isElement || isAttribute) {
serialized.localName = node.localName;
serialized.namespaceURI = node.namespaceURI;
}
serialized.childNodeCount = node.childNodes.length;
let children = null;
if (maxDepth !== 0) {
children = [];
const childDepth = maxDepth !== null ? maxDepth - 1 : null;
for (const child of node.childNodes) {
children.push(
serialize(
child,
childDepth,
ownershipType,
serializationInternalMap,
realm
)
);
}
}
serialized.children = children;
if (isElement) {
serialized.attributes = [...node.attributes].reduce((map, attr) => {
map[attr.name] = attr.value;
return map;
}, {});
// TODO: Bug 1802137 - Add support for shadowRoot
}
return serialized;
}
/**
* Serialize a value as a remote value.
*
* @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-remote-value
*
* @param {Object} value
* Value of any type to be serialized.
* @param {number|null} maxDepth
* Depth of a serialization.
* @param {OwnershipModel} ownershipType
* The ownership model to use for this serialization.
* @param {Map} serializationInternalMap
* Map of internal ids.
* @param {Realm} realm
* The Realm from which comes the value being serialized.
*
* @return {Object} Serialized representation of the value.
*/
export function serialize(
value,
maxDepth,
ownershipType,
serializationInternalMap,
realm
) {
const type = typeof value;
// Primitive protocol values
if (type == "undefined") {
return { type };
} else if (Object.is(value, null)) {
return { type: "null" };
} else if (Object.is(value, NaN)) {
return { type: "number", value: "NaN" };
} else if (Object.is(value, -0)) {
return { type: "number", value: "-0" };
} else if (Object.is(value, Infinity)) {
return { type: "number", value: "Infinity" };
} else if (Object.is(value, -Infinity)) {
return { type: "number", value: "-Infinity" };
} else if (type == "bigint") {
return { type, value: value.toString() };
} else if (["boolean", "number", "string"].includes(type)) {
return { type, value };
}
const handleId = getHandleForObject(realm, ownershipType, value);
const knownObject = serializationInternalMap.has(value);
// Set the OwnershipModel to use for all complex object serializations.
ownershipType = OwnershipModel.None;
// Remote values
// symbols are primitive JS values which can only be serialized
// as remote values.
if (type == "symbol") {
return buildSerialized("symbol", handleId);
}
// All other remote values are non-primitives and their
// className can be extracted with ChromeUtils.getClassName
const className = ChromeUtils.getClassName(value);
if (["Array", "HTMLCollection", "NodeList"].includes(className)) {
return serializeArrayLike(
className.toLowerCase(),
handleId,
knownObject,
value,
maxDepth,
ownershipType,
serializationInternalMap,
realm
);
} else if (className == "RegExp") {
const serialized = buildSerialized("regexp", handleId);
serialized.value = { pattern: value.source, flags: value.flags };
return serialized;
} else if (className == "Date") {
const serialized = buildSerialized("date", handleId);
serialized.value = value.toISOString();
return serialized;
} else if (className == "Map") {
const serialized = buildSerialized("map", handleId);
setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
if (!knownObject && maxDepth !== null && maxDepth > 0) {
serialized.value = serializeMapping(
value.entries(),
maxDepth,
ownershipType,
serializationInternalMap,
realm
);
}
return serialized;
} else if (className == "Set") {
const serialized = buildSerialized("set", handleId);
setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
if (!knownObject && maxDepth !== null && maxDepth > 0) {
serialized.value = serializeList(
value.values(),
maxDepth,
ownershipType,
serializationInternalMap,
realm
);
}
return serialized;
} else if (
[
"ArrayBuffer",
"Function",
"Promise",
"WeakMap",
"WeakSet",
"Window",
].includes(className)
) {
return buildSerialized(className.toLowerCase(), handleId);
} else if (lazy.error.isError(value)) {
return buildSerialized("error", handleId);
} else if (TYPED_ARRAY_CLASSES.includes(className)) {
return buildSerialized("typedarray", handleId);
} else if (Node.isInstance(value)) {
const serialized = buildSerialized("node", handleId);
setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
if (!knownObject) {
serialized.value = serializeNode(
value,
maxDepth,
ownershipType,
serializationInternalMap,
realm
);
}
return serialized;
} else if (ChromeUtils.isDOMObject(value)) {
const serialized = buildSerialized("object", handleId);
return serialized;
}
// Otherwise serialize the JavaScript object as generic object.
const serialized = buildSerialized("object", handleId);
setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
if (!knownObject && maxDepth !== null && maxDepth > 0) {
serialized.value = serializeMapping(
Object.entries(value),
maxDepth,
ownershipType,
serializationInternalMap,
realm
);
}
return serialized;
}
/**
* Set the internalId property of a provided serialized RemoteValue,
* and potentially of a previously created serialized RemoteValue,
* corresponding to the same provided object.
*
* @see https://w3c.github.io/webdriver-bidi/#set-internal-ids-if-needed
*
* @param {Map} serializationInternalMap
* Map of objects to remote values.
* @param {Object} remoteValue
* A serialized RemoteValue for the provided object.
* @param {Object} object
* Object of any type to be serialized.
*/
function setInternalIdsIfNeeded(serializationInternalMap, remoteValue, object) {
if (!serializationInternalMap.has(object)) {
// If the object was not tracked yet in the current serialization, add
// a new entry in the serialization internal map. An internal id will only
// be generated if the same object is encountered again.
serializationInternalMap.set(object, remoteValue);
} else {
// This is at least the second time this object is encountered, retrieve the
// original remote value stored for this object.
const previousRemoteValue = serializationInternalMap.get(object);
if (!previousRemoteValue.internalId) {
// If the original remote value has no internal id yet, generate a uuid
// and update the internalId of the original remote value with it.
previousRemoteValue.internalId = getUUID();
}
// Copy the internalId of the original remote value to the new remote value.
remoteValue.internalId = previousRemoteValue.internalId;
}
}
/**
* Safely stringify a value.
*
* @param {Object} value
* Value of any type to be stringified.
*
* @return {string} String representation of the value.
*/
export function stringify(obj) {
let text;
try {
text =
obj !== null && typeof obj === "object" ? obj.toString() : String(obj);
} catch (e) {
// The error-case will also be handled in `finally {}`.
} finally {
if (typeof text != "string") {
text = Object.prototype.toString.apply(obj);
}
}
return text;
}