utils.js (14636B)
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/. */ 4 5 "use strict"; 6 7 const { 8 DevToolsServer, 9 } = require("resource://devtools/server/devtools-server.js"); 10 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 11 const { assert } = DevToolsUtils; 12 13 loader.lazyRequireGetter( 14 this, 15 "LongStringActor", 16 "resource://devtools/server/actors/string.js", 17 true 18 ); 19 20 loader.lazyRequireGetter( 21 this, 22 "symbolGrip", 23 "resource://devtools/server/actors/object/symbol.js", 24 true 25 ); 26 27 /** 28 * Get thisDebugger.Object referent's `promiseState`. 29 * 30 * @returns Object 31 * An object of one of the following forms: 32 * - { state: "pending" } 33 * - { state: "fulfilled", value } 34 * - { state: "rejected", reason } 35 */ 36 function getPromiseState(obj) { 37 if (obj.class != "Promise") { 38 throw new Error( 39 "Can't call `getPromiseState` on `Debugger.Object`s that don't " + 40 "refer to Promise objects." 41 ); 42 } 43 44 const state = { state: obj.promiseState }; 45 if (state.state === "fulfilled") { 46 state.value = obj.promiseValue; 47 } else if (state.state === "rejected") { 48 state.reason = obj.promiseReason; 49 } 50 return state; 51 } 52 53 /** 54 * Returns true if value is an object or function. 55 * 56 * @param value 57 * @returns {boolean} 58 */ 59 60 function isObjectOrFunction(value) { 61 // Handle null, whose typeof is object 62 if (!value) { 63 return false; 64 } 65 66 const type = typeof value; 67 return type == "object" || type == "function"; 68 } 69 70 /** 71 * Make a debuggee value for the given object, if needed. Primitive values 72 * are left the same. 73 * 74 * Use case: you have a raw JS object (after unsafe dereference) and you want to 75 * send it to the client. In that case you need to use an ObjectActor which 76 * requires a debuggee value. The Debugger.Object.prototype.makeDebuggeeValue() 77 * method works only for JS objects and functions. 78 * 79 * @param Debugger.Object obj 80 * @param any value 81 * @return object 82 */ 83 function makeDebuggeeValueIfNeeded(obj, value) { 84 if (isObjectOrFunction(value)) { 85 return obj.makeDebuggeeValue(value); 86 } 87 return value; 88 } 89 90 /** 91 * Convert a debuggee value into the underlying raw object, if needed. 92 */ 93 function unwrapDebuggeeValue(value) { 94 if (value && typeof value == "object") { 95 return value.unsafeDereference(); 96 } 97 return value; 98 } 99 100 /** 101 * Create a grip for the given debuggee value. If the value is an object or a long string, 102 * it will create an actor and add it to the pool 103 * 104 * @param {ThreadActor} threadActor 105 * The related Thread Actor. 106 * @param {any} value 107 * The debuggee value. 108 * @param {Pool} pool 109 * The pool where the created actor will be added to. 110 * @param {number} [depth] 111 * The current depth within the chain of nested object actor being previewed. 112 * @param {object} [objectActorAttributes] 113 * An optional object whose properties will be assigned to the ObjectActor if one 114 * is created. 115 */ 116 function createValueGrip(threadActor, value, pool, depth = 0, objectActorAttributes = {}) { 117 switch (typeof value) { 118 case "boolean": 119 return value; 120 121 case "string": 122 return createStringGrip(pool, value); 123 124 case "number": 125 if (value === Infinity) { 126 return { type: "Infinity" }; 127 } else if (value === -Infinity) { 128 return { type: "-Infinity" }; 129 } else if (Number.isNaN(value)) { 130 return { type: "NaN" }; 131 } else if (!value && 1 / value === -Infinity) { 132 return { type: "-0" }; 133 } 134 return value; 135 136 case "bigint": 137 return createBigIntValueGrip(value); 138 139 // TODO(bug 1772157) 140 // Record/tuple grips aren't fully implemented yet. 141 case "record": 142 return { 143 class: "Record", 144 }; 145 case "tuple": 146 return { 147 class: "Tuple", 148 }; 149 case "undefined": 150 return { type: "undefined" }; 151 152 case "object": 153 if (value === null) { 154 return { type: "null" }; 155 } else if ( 156 value.optimizedOut || 157 value.uninitialized || 158 value.missingArguments 159 ) { 160 // The slot is optimized out, an uninitialized binding, or 161 // arguments on a dead scope 162 return { 163 type: "null", 164 optimizedOut: value.optimizedOut, 165 uninitialized: value.uninitialized, 166 missingArguments: value.missingArguments, 167 }; 168 } 169 return pool.createObjectGrip( 170 value, 171 depth, 172 objectActorAttributes, 173 ); 174 175 case "symbol": 176 return symbolGrip(threadActor, value, pool); 177 178 default: 179 assert(false, "Failed to provide a grip for: " + value); 180 return null; 181 } 182 } 183 184 /** 185 * Returns a grip for the passed BigInt 186 * 187 * @param {bigint} value 188 * @returns {object} 189 */ 190 function createBigIntValueGrip(value) { 191 return { 192 type: "BigInt", 193 text: value.toString(), 194 }; 195 } 196 197 /** 198 * of passing the value directly over the protocol. 199 * 200 * @param str String 201 * The string we are checking the length of. 202 */ 203 function stringIsLong(str) { 204 return str.length >= DevToolsServer.LONG_STRING_LENGTH; 205 } 206 207 const TYPED_ARRAY_CLASSES = [ 208 "Uint8Array", 209 "Uint8ClampedArray", 210 "Uint16Array", 211 "Uint32Array", 212 "Int8Array", 213 "Int16Array", 214 "Int32Array", 215 "Float32Array", 216 "Float64Array", 217 "BigInt64Array", 218 "BigUint64Array", 219 ]; 220 221 /** 222 * Returns true if a debuggee object is a typed array. 223 * 224 * @param obj Debugger.Object 225 * The debuggee object to test. 226 * @return Boolean 227 */ 228 function isTypedArray(object) { 229 return TYPED_ARRAY_CLASSES.includes(object.class); 230 } 231 232 /** 233 * Returns true if a debuggee object is an array, including a typed array. 234 * 235 * @param obj Debugger.Object 236 * The debuggee object to test. 237 * @return Boolean 238 */ 239 function isArray(object) { 240 return isTypedArray(object) || object.class === "Array"; 241 } 242 243 /** 244 * Returns the length of an array (or typed array). 245 * 246 * @param object Debugger.Object 247 * The debuggee object of the array. 248 * @return Number 249 * @throws if the object is not an array. 250 */ 251 function getArrayLength(object) { 252 if (!isArray(object)) { 253 throw new Error("Expected an array, got a " + object.class); 254 } 255 256 // Real arrays have a reliable `length` own property. 257 if (object.class === "Array") { 258 return DevToolsUtils.getProperty(object, "length"); 259 } 260 261 // For typed arrays, `DevToolsUtils.getProperty` is not reliable because the `length` 262 // getter could be shadowed by an own property, and `getOwnPropertyNames` is 263 // unnecessarily slow. Obtain the `length` getter safely and call it manually. 264 const typedProto = Object.getPrototypeOf(Uint8Array.prototype); 265 const getter = Object.getOwnPropertyDescriptor(typedProto, "length").get; 266 return getter.call(object.unsafeDereference()); 267 } 268 269 /** 270 * Returns true if the parameter is suitable to be an array index. 271 * 272 * @param str String 273 * @return Boolean 274 */ 275 function isArrayIndex(str) { 276 // Transform the parameter to a 32-bit unsigned integer. 277 const num = str >>> 0; 278 // Check that the parameter is a canonical Uint32 index. 279 return ( 280 num + "" === str && 281 // Array indices cannot attain the maximum Uint32 value. 282 num != -1 >>> 0 283 ); 284 } 285 286 /** 287 * Returns true if a debuggee object is a local or sessionStorage object. 288 * 289 * @param object Debugger.Object 290 * The debuggee object to test. 291 * @return Boolean 292 */ 293 function isStorage(object) { 294 return object.class === "Storage"; 295 } 296 297 /** 298 * Returns the length of a local or sessionStorage object. 299 * 300 * @param object Debugger.Object 301 * The debuggee object of the array. 302 * @return Number 303 * @throws if the object is not a local or sessionStorage object. 304 */ 305 function getStorageLength(object) { 306 if (!isStorage(object)) { 307 throw new Error("Expected a storage object, got a " + object.class); 308 } 309 return DevToolsUtils.getProperty(object, "length"); 310 } 311 312 /** 313 * Returns an array of properties based on event class name. 314 * 315 * @param className 316 * @returns {Array} 317 */ 318 function getPropsForEvent(className) { 319 const positionProps = ["buttons", "clientX", "clientY", "layerX", "layerY"]; 320 const eventToPropsMap = { 321 MouseEvent: positionProps, 322 DragEvent: positionProps, 323 PointerEvent: positionProps, 324 SimpleGestureEvent: positionProps, 325 WheelEvent: positionProps, 326 KeyboardEvent: ["key", "charCode", "keyCode"], 327 TransitionEvent: ["propertyName", "pseudoElement"], 328 AnimationEvent: ["animationName", "pseudoElement"], 329 ClipboardEvent: ["clipboardData"], 330 }; 331 332 if (className in eventToPropsMap) { 333 return eventToPropsMap[className]; 334 } 335 336 return []; 337 } 338 339 /** 340 * Returns an array of of all properties of an object 341 * 342 * @param obj 343 * @param rawObj 344 * @returns {Array|Iterable} If rawObj is localStorage/sessionStorage, we don't return an 345 * array but an iterable object (with the proper `length` property) to avoid 346 * performance issues. 347 */ 348 function getPropNamesFromObject(obj, rawObj) { 349 try { 350 if (isStorage(obj)) { 351 // local and session storage cannot be iterated over using 352 // Object.getOwnPropertyNames() because it skips keys that are duplicated 353 // on the prototype e.g. "key", "getKeys" so we need to gather the real 354 // keys using the storage.key() function. 355 // As the method is pretty slow, we return an iterator here, so we don't consume 356 // more than we need, especially since we're calling this from previewers in which 357 // we only need the first 10 entries for the preview (See Bug 1741804). 358 359 // Still return the proper number of entries. 360 const length = rawObj.length; 361 const iterable = { length }; 362 iterable[Symbol.iterator] = function*() { 363 for (let j = 0; j < length; j++) { 364 yield rawObj.key(j); 365 } 366 }; 367 return iterable; 368 } 369 370 return obj.getOwnPropertyNames(); 371 } catch (ex) { 372 // Calling getOwnPropertyNames() on some wrapped native prototypes is not 373 // allowed: "cannot modify properties of a WrappedNative". See bug 952093. 374 } 375 376 return []; 377 } 378 379 /** 380 * Returns an array of private properties of an object 381 * 382 * @param obj 383 * @returns {Array} 384 */ 385 function getSafePrivatePropertiesSymbols(obj) { 386 try { 387 return obj.getOwnPrivateProperties(); 388 } catch (ex) { 389 return []; 390 } 391 } 392 393 /** 394 * Returns an array of all symbol properties of an object 395 * 396 * @param obj 397 * @returns {Array} 398 */ 399 function getSafeOwnPropertySymbols(obj) { 400 try { 401 return obj.getOwnPropertySymbols(); 402 } catch (ex) { 403 return []; 404 } 405 } 406 407 /** 408 * Returns an array modifiers based on keys 409 * 410 * @param rawObj 411 * @returns {Array} 412 */ 413 function getModifiersForEvent(rawObj) { 414 const modifiers = []; 415 const keysToModifiersMap = { 416 altKey: "Alt", 417 ctrlKey: "Control", 418 metaKey: "Meta", 419 shiftKey: "Shift", 420 }; 421 422 for (const key in keysToModifiersMap) { 423 if (keysToModifiersMap.hasOwnProperty(key) && rawObj[key]) { 424 modifiers.push(keysToModifiersMap[key]); 425 } 426 } 427 428 return modifiers; 429 } 430 431 /** 432 * Make a debuggee value for the given value. 433 * 434 * @param TargetActor targetActor 435 * The Target Actor from which this object originates. 436 * @param mixed value 437 * The value you want to get a debuggee value for. 438 * @return object 439 * Debuggee value for |value|. 440 */ 441 function makeDebuggeeValue(targetActor, value) { 442 // Primitive types are debuggee values and Debugger.Object.makeDebuggeeValue 443 // would return them unchanged. So avoid the expense of: 444 // getGlobalForObject+makeGlobalObjectReference+makeDebugeeValue for them. 445 // 446 // It is actually easier to identify non primitive which can only be object or function. 447 if (!isObjectOrFunction(value)) { 448 return value; 449 } 450 451 // `value` may come from various globals. 452 // And Debugger.Object.makeDebuggeeValue only works for objects 453 // related to the same global. So fetch the global first, 454 // in order to instantiate a Debugger.Object for it. 455 // 456 // In the worker thread, we don't have access to Cu, 457 // but at the same time, there is only one global, the worker one. 458 const valueGlobal = isWorker ? targetActor.targetGlobal : Cu.getGlobalForObject(value); 459 let dbgGlobal; 460 try { 461 dbgGlobal = targetActor.dbg.makeGlobalObjectReference( 462 valueGlobal 463 ); 464 } catch(e) { 465 // makeGlobalObjectReference will throw if the global is invisible to Debugger, 466 // in this case instantiate a Debugger.Object for the top level global 467 // of the target. Even if value will come from another global, it will "work", 468 // but the Debugger.Object created via dbgGlobal.makeDebuggeeValue will throw 469 // on most methods as the object will also be invisible to Debuggee... 470 if (e.message.includes("object in compartment marked as invisible to Debugger")) { 471 dbgGlobal = targetActor.dbg.makeGlobalObjectReference( 472 targetActor.window 473 ); 474 475 } else { 476 throw e; 477 } 478 } 479 480 return dbgGlobal.makeDebuggeeValue(value); 481 } 482 483 /** 484 * Create a grip for the given string. 485 * 486 * @param TargetActor targetActor 487 * The Target Actor from which this object originates. 488 */ 489 function createStringGrip(targetActor, string) { 490 if (string && stringIsLong(string)) { 491 const actor = new LongStringActor(targetActor.conn, string); 492 targetActor.manage(actor); 493 return actor.form(); 494 } 495 return string; 496 } 497 498 /** 499 * Create a grip for the given value. 500 * 501 * @param TargetActor targetActor 502 * The Target Actor from which this object originates. 503 * @param mixed value 504 * The value you want to get a debuggee value for. 505 * @param Number depth 506 * Depth of the object compared to the top level object, 507 * when we are inspecting nested attributes. 508 * @param Object [objectActorAttributes] 509 * An optional object whose properties will be assigned to the ObjectActor if one 510 * is created. 511 * @return object 512 */ 513 function createValueGripForTarget( 514 targetActor, 515 value, 516 depth = 0, 517 objectActorAttributes = {} 518 ) { 519 return createValueGrip(targetActor.threadActor, value, targetActor.objectsPool, depth, objectActorAttributes); 520 } 521 522 module.exports = { 523 getPromiseState, 524 makeDebuggeeValueIfNeeded, 525 unwrapDebuggeeValue, 526 createBigIntValueGrip, 527 createValueGrip, 528 stringIsLong, 529 isTypedArray, 530 isArray, 531 isStorage, 532 getArrayLength, 533 getStorageLength, 534 isArrayIndex, 535 getPropsForEvent, 536 getPropNamesFromObject, 537 getSafeOwnPropertySymbols, 538 getSafePrivatePropertiesSymbols, 539 getModifiersForEvent, 540 isObjectOrFunction, 541 createStringGrip, 542 makeDebuggeeValue, 543 createValueGripForTarget, 544 };