Bug 1793691 - adjust test-info-all to include manifests. r=gbrown
[gecko.git] / remote / webdriver-bidi / RemoteValue.sys.mjs
blob691bcd02deaf2ad51cf8dc972342228528c18ddf
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";
7 const lazy = {};
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",
13 });
15 XPCOMUtils.defineLazyGetter(lazy, "logger", () =>
16   lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
19 /**
20  * @typedef {Object} OwnershipModel
21  **/
23 /**
24  * Enum of ownership models supported by the serialization.
25  *
26  * @readonly
27  * @enum {OwnershipModel}
28  **/
29 export const OwnershipModel = {
30   None: "none",
31   Root: "root",
34 function getUUID() {
35   return Services.uuid
36     .generateUUID()
37     .toString()
38     .slice(1, -1);
41 const TYPED_ARRAY_CLASSES = [
42   "Uint8Array",
43   "Uint8ClampedArray",
44   "Uint16Array",
45   "Uint32Array",
46   "Int8Array",
47   "Int16Array",
48   "Int32Array",
49   "Float32Array",
50   "Float64Array",
51   "BigInt64Array",
52   "BigUint64Array",
55 /**
56  * Build the serialized RemoteValue.
57  *
58  * @return {Object}
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.
62  */
63 function buildSerialized(type, handle = null) {
64   const serialized = { type };
66   if (handle !== null) {
67     serialized.handle = handle;
68   }
70   return serialized;
73 /**
74  * Helper to validate if a date string follows Date Time String format.
75  *
76  * @see https://tc39.es/ecma262/#sec-date-time-string-format
77  *
78  * @param {string} dateString
79  *     String which needs to be validated.
80  *
81  * @throws {InvalidArgumentError}
82  *     If <var>dateString</var> doesn't follow the format.
83  */
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}`;
90   const MM = "\\d{2}";
91   const DD = "\\d{2}";
92   const date = `${YYYY}(?:-${MM})?(?:-${DD})?`;
93   const HH_mm = "\\d{2}:\\d{2}";
94   const SS = "\\d{2}";
95   const sss = "\\d{3}";
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}`
104     );
105   }
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.
122  */
123 function deserializeValueList(realm, serializedValueList) {
124   lazy.assert.array(
125     serializedValueList,
126     `Expected "serializedValueList" to be an array, got ${serializedValueList}`
127   );
129   const deserializedValues = [];
131   for (const item of serializedValueList) {
132     deserializedValues.push(deserialize(realm, item));
133   }
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.
153  */
154 function deserializeKeyValueList(realm, serializedKeyValueList) {
155   lazy.assert.array(
156     serializedKeyValueList,
157     `Expected "serializedKeyValueList" to be an array, got ${serializedKeyValueList}`
158   );
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}`
166       );
167     }
168     const [serializedKey, serializedValue] = serializedKeyValue;
169     const deserializedKey =
170       typeof serializedKey == "string"
171         ? serializedKey
172         : deserialize(realm, serializedKey);
173     const deserializedValue = deserialize(realm, serializedValue);
175     deserializedKeyValueList.push([deserializedKey, deserializedValue]);
176   }
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.
192  */
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) {
198     lazy.assert.string(
199       handle,
200       `Expected "handle" to be a string, got ${handle}`
201     );
203     const object = realm.getObjectForHandle(handle);
204     if (!object) {
205       throw new lazy.error.InvalidArgumentError(
206         `Unable to find an object reference for "handle" ${handle}`
207       );
208     }
210     return object;
211   }
213   lazy.assert.string(type, `Expected "type" to be a string, got ${type}`);
215   // Primitive protocol values
216   switch (type) {
217     case "undefined":
218       return undefined;
219     case "null":
220       return null;
221     case "string":
222       lazy.assert.string(
223         value,
224         `Expected "value" to be a string, got ${value}`
225       );
226       return value;
227     case "number":
228       // If value is already a number return its value.
229       if (typeof value === "number") {
230         return value;
231       }
233       // Otherwise it has to be one of the special strings
234       lazy.assert.in(
235         value,
236         ["NaN", "-0", "Infinity", "-Infinity"],
237         `Expected "value" to be one of "NaN", "-0", "Infinity", "-Infinity", got ${value}`
238       );
239       return Number(value);
240     case "boolean":
241       lazy.assert.boolean(
242         value,
243         `Expected "value" to be a boolean, got ${value}`
244       );
245       return value;
246     case "bigint":
247       lazy.assert.string(
248         value,
249         `Expected "value" to be a string, got ${value}`
250       );
251       try {
252         return BigInt(value);
253       } catch (e) {
254         throw new lazy.error.InvalidArgumentError(
255           `Failed to deserialize value as BigInt: ${value}`
256         );
257       }
259     // Non-primitive protocol values
260     case "array":
261       const array = realm.cloneIntoRealm([]);
262       deserializeValueList(realm, value).forEach(v => array.push(v));
263       return array;
264     case "date":
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));
270     case "map":
271       const map = realm.cloneIntoRealm(new Map());
272       deserializeKeyValueList(realm, value).forEach(([k, v]) => map.set(k, v));
273       return map;
274     case "object":
275       const object = realm.cloneIntoRealm({});
276       deserializeKeyValueList(realm, value).forEach(
277         ([k, v]) => (object[k] = v)
278       );
279       return object;
280     case "regexp":
281       lazy.assert.object(
282         value,
283         `Expected "value" for RegExp to be an object, got ${value}`
284       );
285       const { pattern, flags } = value;
286       lazy.assert.string(
287         pattern,
288         `Expected "pattern" for RegExp to be a string, got ${pattern}`
289       );
290       if (flags !== undefined) {
291         lazy.assert.string(
292           flags,
293           `Expected "flags" for RegExp to be a string, got ${flags}`
294         );
295       }
296       try {
297         return realm.cloneIntoRealm(new RegExp(pattern, flags));
298       } catch (e) {
299         throw new lazy.error.InvalidArgumentError(
300           `Failed to deserialize value as RegExp: ${value}`
301         );
302       }
303     case "set":
304       const set = realm.cloneIntoRealm(new Set());
305       deserializeValueList(realm, value).forEach(v => set.add(v));
306       return set;
307   }
309   lazy.logger.warn(`Unsupported type for local value ${type}`);
310   return undefined;
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".
328  */
329 function getHandleForObject(realm, ownershipType, object) {
330   if (ownershipType === OwnershipModel.None) {
331     return null;
332   }
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.
353  */
354 function serializeList(
355   iterable,
356   maxDepth,
357   childOwnership,
358   serializationInternalMap,
359   realm
360 ) {
361   const serialized = [];
362   const childDepth = maxDepth !== null ? maxDepth - 1 : null;
364   for (const item of iterable) {
365     serialized.push(
366       serialize(
367         item,
368         childDepth,
369         childOwnership,
370         serializationInternalMap,
371         realm
372       )
373     );
374   }
376   return serialized;
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.
396  */
397 function serializeMapping(
398   iterable,
399   maxDepth,
400   childOwnership,
401   serializationInternalMap,
402   realm
403 ) {
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"
410         ? key
411         : serialize(
412             key,
413             childDepth,
414             childOwnership,
415             serializationInternalMap,
416             realm
417           );
418     const serializedValue = serialize(
419       item,
420       childDepth,
421       childOwnership,
422       serializationInternalMap,
423       realm
424     );
426     serialized.push([serializedKey, serializedValue]);
427   }
429   return serialized;
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.
449  */
450 export function serialize(
451   value,
452   maxDepth,
453   ownershipType,
454   serializationInternalMap,
455   realm
456 ) {
457   const type = typeof value;
459   // Primitive protocol values
460   if (type == "undefined") {
461     return { type };
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 };
476   }
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;
484   // Remote values
486   // symbols are primitive JS values which can only be serialized
487   // as remote values.
488   if (type == "symbol") {
489     return buildSerialized("symbol", handleId);
490   }
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(
501         value,
502         maxDepth,
503         childOwnership,
504         serializationInternalMap,
505         realm
506       );
507     }
509     return serialized;
510   } else if (className == "RegExp") {
511     const serialized = buildSerialized("regexp", handleId);
512     serialized.value = { pattern: value.source, flags: value.flags };
513     return serialized;
514   } else if (className == "Date") {
515     const serialized = buildSerialized("date", handleId);
516     serialized.value = value.toISOString();
517     return serialized;
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(
524         value.entries(),
525         maxDepth,
526         childOwnership,
527         serializationInternalMap,
528         realm
529       );
530     }
531     return serialized;
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(
538         value.values(),
539         maxDepth,
540         childOwnership,
541         serializationInternalMap,
542         realm
543       );
544     }
545     return serialized;
546   } else if (
547     [
548       "ArrayBuffer",
549       "Function",
550       "Promise",
551       "WeakMap",
552       "WeakSet",
553       "Window",
554     ].includes(className)
555   ) {
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);
561   }
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),
571         maxDepth,
572         childOwnership,
573         serializationInternalMap,
574         realm
575       );
576     }
577     return serialized;
578   }
580   lazy.logger.warn(
581     `Unsupported type: ${type} for remote value: ${stringify(value)}`
582   );
584   return undefined;
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.
600  */
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);
607   } else {
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();
616     }
618     // Copy the internalId of the original remote value to the new remote value.
619     remoteValue.internalId = previousRemoteValue.internalId;
620   }
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.
630  */
631 export function stringify(obj) {
632   let text;
633   try {
634     text =
635       obj !== null && typeof obj === "object" ? obj.toString() : String(obj);
636   } catch (e) {
637     // The error-case will also be handled in `finally {}`.
638   } finally {
639     if (typeof text != "string") {
640       text = Object.prototype.toString.apply(obj);
641     }
642   }
644   return text;