1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
11 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
12 Log: "chrome://remote/content/shared/Log.sys.mjs",
15 XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
16 lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
20 * @typedef {Object} OwnershipModel
24 * Enum of ownership models supported by the serialization.
27 * @enum {OwnershipModel}
29 export const OwnershipModel = {
41 const TYPED_ARRAY_CLASSES = [
56 * Build the serialized RemoteValue.
59 * An object with a mandatory `type` property, and optional `handle`,
60 * depending on the OwnershipModel, used for the serialization and
61 * on the value's type.
63 function buildSerialized(type, handle = null) {
64 const serialized = { type };
66 if (handle !== null) {
67 serialized.handle = handle;
74 * Helper to validate if a date string follows Date Time String format.
76 * @see https://tc39.es/ecma262/#sec-date-time-string-format
78 * @param {string} dateString
79 * String which needs to be validated.
81 * @throws {InvalidArgumentError}
82 * If <var>dateString</var> doesn't follow the format.
84 function checkDateTimeString(dateString) {
85 // Check if a date string follows a simplification of
86 // the ISO 8601 calendar date extended format.
87 const expandedYear = "[+-]\\d{6}";
88 const year = "\\d{4}";
89 const YYYY = `${expandedYear}|${year}`;
92 const date = `${YYYY}(?:-${MM})?(?:-${DD})?`;
93 const HH_mm = "\\d{2}:\\d{2}";
96 const TZ = `Z|[+-]${HH_mm}`;
97 const time = `T${HH_mm}(?::${SS}(?:\\.${sss})?(?:${TZ})?)?`;
98 const iso8601Format = new RegExp(`^${date}(?:${time})?$`);
100 // Check also if a date string is a valid date.
101 if (Number.isNaN(Date.parse(dateString)) || !iso8601Format.test(dateString)) {
102 throw new lazy.error.InvalidArgumentError(
103 `Expected "value" for Date to be a Date Time string, got ${dateString}`
109 * Helper to deserialize value list.
111 * @see https://w3c.github.io/webdriver-bidi/#deserialize-value-list
113 * @param {Realm} realm
114 * The Realm in which the value is deserialized.
115 * @param {Array} serializedValueList
116 * List of serialized values.
118 * @return {Array} List of deserialized values.
120 * @throws {InvalidArgumentError}
121 * If <var>serializedValueList</var> is not an array.
123 function deserializeValueList(realm, serializedValueList) {
126 `Expected "serializedValueList" to be an array, got ${serializedValueList}`
129 const deserializedValues = [];
131 for (const item of serializedValueList) {
132 deserializedValues.push(deserialize(realm, item));
135 return deserializedValues;
139 * Helper to deserialize key-value list.
141 * @see https://w3c.github.io/webdriver-bidi/#deserialize-key-value-list
143 * @param {Realm} realm
144 * The Realm in which the value is deserialized.
145 * @param {Array} serializedKeyValueList
146 * List of serialized key-value.
148 * @return {Array} List of deserialized key-value.
150 * @throws {InvalidArgumentError}
151 * If <var>serializedKeyValueList</var> is not an array or
152 * not an array of key-value arrays.
154 function deserializeKeyValueList(realm, serializedKeyValueList) {
156 serializedKeyValueList,
157 `Expected "serializedKeyValueList" to be an array, got ${serializedKeyValueList}`
160 const deserializedKeyValueList = [];
162 for (const serializedKeyValue of serializedKeyValueList) {
163 if (!Array.isArray(serializedKeyValue) || serializedKeyValue.length != 2) {
164 throw new lazy.error.InvalidArgumentError(
165 `Expected key-value pair to be an array with 2 elements, got ${serializedKeyValue}`
168 const [serializedKey, serializedValue] = serializedKeyValue;
169 const deserializedKey =
170 typeof serializedKey == "string"
172 : deserialize(realm, serializedKey);
173 const deserializedValue = deserialize(realm, serializedValue);
175 deserializedKeyValueList.push([deserializedKey, deserializedValue]);
178 return deserializedKeyValueList;
182 * Deserialize a local value.
184 * @see https://w3c.github.io/webdriver-bidi/#deserialize-local-value
186 * @param {Realm} realm
187 * The Realm in which the value is deserialized.
188 * @param {Object} serializedValue
189 * Value of any type to be deserialized.
191 * @return {Object} Deserialized representation of the value.
193 export function deserialize(realm, serializedValue) {
194 const { handle, type, value } = serializedValue;
196 // With a handle present deserialize as remote reference.
197 if (handle !== undefined) {
200 `Expected "handle" to be a string, got ${handle}`
203 const object = realm.getObjectForHandle(handle);
205 throw new lazy.error.InvalidArgumentError(
206 `Unable to find an object reference for "handle" ${handle}`
213 lazy.assert.string(type, `Expected "type" to be a string, got ${type}`);
215 // Primitive protocol values
224 `Expected "value" to be a string, got ${value}`
228 // If value is already a number return its value.
229 if (typeof value === "number") {
233 // Otherwise it has to be one of the special strings
236 ["NaN", "-0", "Infinity", "-Infinity"],
237 `Expected "value" to be one of "NaN", "-0", "Infinity", "-Infinity", got ${value}`
239 return Number(value);
243 `Expected "value" to be a boolean, got ${value}`
249 `Expected "value" to be a string, got ${value}`
252 return BigInt(value);
254 throw new lazy.error.InvalidArgumentError(
255 `Failed to deserialize value as BigInt: ${value}`
259 // Non-primitive protocol values
261 const array = realm.cloneIntoRealm([]);
262 deserializeValueList(realm, value).forEach(v => array.push(v));
265 // We want to support only Date Time String format,
266 // check if the value follows it.
267 checkDateTimeString(value);
269 return realm.cloneIntoRealm(new Date(value));
271 const map = realm.cloneIntoRealm(new Map());
272 deserializeKeyValueList(realm, value).forEach(([k, v]) => map.set(k, v));
275 const object = realm.cloneIntoRealm({});
276 deserializeKeyValueList(realm, value).forEach(
277 ([k, v]) => (object[k] = v)
283 `Expected "value" for RegExp to be an object, got ${value}`
285 const { pattern, flags } = value;
288 `Expected "pattern" for RegExp to be a string, got ${pattern}`
290 if (flags !== undefined) {
293 `Expected "flags" for RegExp to be a string, got ${flags}`
297 return realm.cloneIntoRealm(new RegExp(pattern, flags));
299 throw new lazy.error.InvalidArgumentError(
300 `Failed to deserialize value as RegExp: ${value}`
304 const set = realm.cloneIntoRealm(new Set());
305 deserializeValueList(realm, value).forEach(v => set.add(v));
309 lazy.logger.warn(`Unsupported type for local value ${type}`);
314 * Helper to retrieve the handle id for a given object, for the provided realm
315 * and ownership type.
317 * See https://w3c.github.io/webdriver-bidi/#handle-for-an-object
319 * @param {Realm} realm
320 * The Realm from which comes the value being serialized.
321 * @param {OwnershipModel} ownershipType
322 * The ownership model to use for this serialization.
323 * @param {Object} object
324 * The object being serialized.
326 * @return {string} The unique handle id for the object. Will be null if the
327 * Ownership type is "none".
329 function getHandleForObject(realm, ownershipType, object) {
330 if (ownershipType === OwnershipModel.None) {
333 return realm.getHandleForObject(object);
337 * Helper to serialize as a list.
339 * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-list
341 * @param {Iterable} iterable
342 * List of values to be serialized.
343 * @param {number|null} maxDepth
344 * Depth of a serialization.
345 * @param {OwnershipModel} childOwnership
346 * The ownership model to use for this serialization.
347 * @param {Map} serializationInternalMap
348 * Map of internal ids.
349 * @param {Realm} realm
350 * The Realm from which comes the value being serialized.
352 * @return {Array} List of serialized values.
354 function serializeList(
358 serializationInternalMap,
361 const serialized = [];
362 const childDepth = maxDepth !== null ? maxDepth - 1 : null;
364 for (const item of iterable) {
370 serializationInternalMap,
380 * Helper to serialize as a mapping.
382 * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-mapping
384 * @param {Iterable} iterable
385 * List of values to be serialized.
386 * @param {number|null} maxDepth
387 * Depth of a serialization.
388 * @param {OwnershipModel} childOwnership
389 * The ownership model to use for this serialization.
390 * @param {Map} serializationInternalMap
391 * Map of internal ids.
392 * @param {Realm} realm
393 * The Realm from which comes the value being serialized.
395 * @return {Array} List of serialized values.
397 function serializeMapping(
401 serializationInternalMap,
404 const serialized = [];
405 const childDepth = maxDepth !== null ? maxDepth - 1 : null;
407 for (const [key, item] of iterable) {
408 const serializedKey =
409 typeof key == "string"
415 serializationInternalMap,
418 const serializedValue = serialize(
422 serializationInternalMap,
426 serialized.push([serializedKey, serializedValue]);
433 * Serialize a value as a remote value.
435 * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-remote-value
437 * @param {Object} value
438 * Value of any type to be serialized.
439 * @param {number|null} maxDepth
440 * Depth of a serialization.
441 * @param {OwnershipModel} ownershipType
442 * The ownership model to use for this serialization.
443 * @param {Map} serializationInternalMap
444 * Map of internal ids.
445 * @param {Realm} realm
446 * The Realm from which comes the value being serialized.
448 * @return {Object} Serialized representation of the value.
450 export function serialize(
454 serializationInternalMap,
457 const type = typeof value;
459 // Primitive protocol values
460 if (type == "undefined") {
462 } else if (Object.is(value, null)) {
463 return { type: "null" };
464 } else if (Object.is(value, NaN)) {
465 return { type: "number", value: "NaN" };
466 } else if (Object.is(value, -0)) {
467 return { type: "number", value: "-0" };
468 } else if (Object.is(value, Infinity)) {
469 return { type: "number", value: "Infinity" };
470 } else if (Object.is(value, -Infinity)) {
471 return { type: "number", value: "-Infinity" };
472 } else if (type == "bigint") {
473 return { type, value: value.toString() };
474 } else if (["boolean", "number", "string"].includes(type)) {
475 return { type, value };
478 const handleId = getHandleForObject(realm, ownershipType, value);
479 const knownObject = serializationInternalMap.has(value);
481 // Set the OwnershipModel to use for all complex object serializations.
482 const childOwnership = OwnershipModel.None;
486 // symbols are primitive JS values which can only be serialized
488 if (type == "symbol") {
489 return buildSerialized("symbol", handleId);
492 // All other remote values are non-primitives and their
493 // className can be extracted with ChromeUtils.getClassName
494 const className = ChromeUtils.getClassName(value);
495 if (className == "Array") {
496 const serialized = buildSerialized("array", handleId);
497 setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
499 if (!knownObject && maxDepth !== null && maxDepth > 0) {
500 serialized.value = serializeList(
504 serializationInternalMap,
510 } else if (className == "RegExp") {
511 const serialized = buildSerialized("regexp", handleId);
512 serialized.value = { pattern: value.source, flags: value.flags };
514 } else if (className == "Date") {
515 const serialized = buildSerialized("date", handleId);
516 serialized.value = value.toISOString();
518 } else if (className == "Map") {
519 const serialized = buildSerialized("map", handleId);
520 setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
522 if (!knownObject && maxDepth !== null && maxDepth > 0) {
523 serialized.value = serializeMapping(
527 serializationInternalMap,
532 } else if (className == "Set") {
533 const serialized = buildSerialized("set", handleId);
534 setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
536 if (!knownObject && maxDepth !== null && maxDepth > 0) {
537 serialized.value = serializeList(
541 serializationInternalMap,
554 ].includes(className)
556 return buildSerialized(className.toLowerCase(), handleId);
557 } else if (lazy.error.isError(value)) {
558 return buildSerialized("error", handleId);
559 } else if (TYPED_ARRAY_CLASSES.includes(className)) {
560 return buildSerialized("typedarray", handleId);
562 // TODO: Bug 1770733 and 1792524. Remove the if condition when the serialization of all the other types is implemented,
563 // since then the serialization of plain objects should be the fallback.
564 else if (className == "Object") {
565 const serialized = buildSerialized("object", handleId);
566 setInternalIdsIfNeeded(serializationInternalMap, serialized, value);
568 if (!knownObject && maxDepth !== null && maxDepth > 0) {
569 serialized.value = serializeMapping(
570 Object.entries(value),
573 serializationInternalMap,
581 `Unsupported type: ${type} for remote value: ${stringify(value)}`
588 * Set the internalId property of a provided serialized RemoteValue,
589 * and potentially of a previously created serialized RemoteValue,
590 * corresponding to the same provided object.
592 * @see https://w3c.github.io/webdriver-bidi/#set-internal-ids-if-needed
594 * @param {Map} serializationInternalMap
595 * Map of objects to remote values.
596 * @param {Object} remoteValue
597 * A serialized RemoteValue for the provided object.
598 * @param {Object} object
599 * Object of any type to be serialized.
601 function setInternalIdsIfNeeded(serializationInternalMap, remoteValue, object) {
602 if (!serializationInternalMap.has(object)) {
603 // If the object was not tracked yet in the current serialization, add
604 // a new entry in the serialization internal map. An internal id will only
605 // be generated if the same object is encountered again.
606 serializationInternalMap.set(object, remoteValue);
608 // This is at least the second time this object is encountered, retrieve the
609 // original remote value stored for this object.
610 const previousRemoteValue = serializationInternalMap.get(object);
612 if (!previousRemoteValue.internalId) {
613 // If the original remote value has no internal id yet, generate a uuid
614 // and update the internalId of the original remote value with it.
615 previousRemoteValue.internalId = getUUID();
618 // Copy the internalId of the original remote value to the new remote value.
619 remoteValue.internalId = previousRemoteValue.internalId;
624 * Safely stringify a value.
626 * @param {Object} value
627 * Value of any type to be stringified.
629 * @return {string} String representation of the value.
631 export function stringify(obj) {
635 obj !== null && typeof obj === "object" ? obj.toString() : String(obj);
637 // The error-case will also be handled in `finally {}`.
639 if (typeof text != "string") {
640 text = Object.prototype.toString.apply(obj);