diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/server/actors/object/property-iterator.js | 685 |
1 files changed, 685 insertions, 0 deletions
diff --git a/devtools/server/actors/object/property-iterator.js b/devtools/server/actors/object/property-iterator.js new file mode 100644 index 0000000000..7bd2c0a704 --- /dev/null +++ b/devtools/server/actors/object/property-iterator.js @@ -0,0 +1,685 @@ +/* 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/. */ + +"use strict"; + +const { Actor } = require("resource://devtools/shared/protocol.js"); +const { + propertyIteratorSpec, +} = require("resource://devtools/shared/specs/property-iterator.js"); + +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); +loader.lazyRequireGetter( + this, + "ObjectUtils", + "resource://devtools/server/actors/object/utils.js" +); + +/** + * Creates an actor to iterate over an object's property names and values. + * + * @param objectActor ObjectActor + * The object actor. + * @param options Object + * A dictionary object with various boolean attributes: + * - enumEntries Boolean + * If true, enumerates the entries of a Map or Set object + * instead of enumerating properties. + * - ignoreIndexedProperties Boolean + * If true, filters out Array items. + * e.g. properties names between `0` and `object.length`. + * - ignoreNonIndexedProperties Boolean + * If true, filters out items that aren't array items + * e.g. properties names that are not a number between `0` + * and `object.length`. + * - sort Boolean + * If true, the iterator will sort the properties by name + * before dispatching them. + * - query String + * If non-empty, will filter the properties by names and values + * containing this query string. The match is not case-sensitive. + * Regarding value filtering it just compare to the stringification + * of the property value. + */ +class PropertyIteratorActor extends Actor { + constructor(objectActor, options, conn) { + super(conn, propertyIteratorSpec); + if (!DevToolsUtils.isSafeDebuggerObject(objectActor.obj)) { + this.iterator = { + size: 0, + propertyName: index => undefined, + propertyDescription: index => undefined, + }; + } else if (options.enumEntries) { + const cls = objectActor.obj.class; + if (cls == "Map") { + this.iterator = enumMapEntries(objectActor); + } else if (cls == "WeakMap") { + this.iterator = enumWeakMapEntries(objectActor); + } else if (cls == "Set") { + this.iterator = enumSetEntries(objectActor); + } else if (cls == "WeakSet") { + this.iterator = enumWeakSetEntries(objectActor); + } else if (cls == "Storage") { + this.iterator = enumStorageEntries(objectActor); + } else if (cls == "URLSearchParams") { + this.iterator = enumURLSearchParamsEntries(objectActor); + } else if (cls == "Headers") { + this.iterator = enumHeadersEntries(objectActor); + } else if (cls == "HighlightRegistry") { + this.iterator = enumHighlightRegistryEntries(objectActor); + } else if (cls == "FormData") { + this.iterator = enumFormDataEntries(objectActor); + } else if (cls == "MIDIInputMap") { + this.iterator = enumMidiInputMapEntries(objectActor); + } else if (cls == "MIDIOutputMap") { + this.iterator = enumMidiOutputMapEntries(objectActor); + } else { + throw new Error( + "Unsupported class to enumerate entries from: " + cls + ); + } + } else if ( + ObjectUtils.isArray(objectActor.obj) && + options.ignoreNonIndexedProperties && + !options.query + ) { + this.iterator = enumArrayProperties(objectActor, options); + } else { + this.iterator = enumObjectProperties(objectActor, options); + } + } + + form() { + return { + type: this.typeName, + actor: this.actorID, + count: this.iterator.size, + }; + } + + names({ indexes }) { + const list = []; + for (const idx of indexes) { + list.push(this.iterator.propertyName(idx)); + } + return indexes; + } + + slice({ start, count }) { + const ownProperties = Object.create(null); + for (let i = start, m = start + count; i < m; i++) { + const name = this.iterator.propertyName(i); + ownProperties[name] = this.iterator.propertyDescription(i); + } + + return { + ownProperties, + }; + } + + all() { + return this.slice({ start: 0, count: this.iterator.size }); + } + } + +function waiveXrays(obj) { + return isWorker ? obj : Cu.waiveXrays(obj); +} + +function unwaiveXrays(obj) { + return isWorker ? obj : Cu.unwaiveXrays(obj); +} + +/** + * Helper function to create a grip from a Map/Set entry + */ +function gripFromEntry({ obj, hooks }, entry) { + entry = unwaiveXrays(entry); + return hooks.createValueGrip( + ObjectUtils.makeDebuggeeValueIfNeeded(obj, entry) + ); +} + +function enumArrayProperties(objectActor, options) { + return { + size: ObjectUtils.getArrayLength(objectActor.obj), + propertyName(index) { + return index; + }, + propertyDescription(index) { + return objectActor._propertyDescriptor(index); + }, + }; +} + +function enumObjectProperties(objectActor, options) { + let names = []; + try { + names = objectActor.obj.getOwnPropertyNames(); + } catch (ex) { + // Calling getOwnPropertyNames() on some wrapped native prototypes is not + // allowed: "cannot modify properties of a WrappedNative". See bug 952093. + } + + if (options.ignoreNonIndexedProperties || options.ignoreIndexedProperties) { + const length = DevToolsUtils.getProperty(objectActor.obj, "length"); + let sliceIndex; + + const isLengthTrustworthy = + isUint32(length) && + (!length || ObjectUtils.isArrayIndex(names[length - 1])) && + !ObjectUtils.isArrayIndex(names[length]); + + if (!isLengthTrustworthy) { + // The length property may not reflect what the object looks like, let's find + // where indexed properties end. + + if (!ObjectUtils.isArrayIndex(names[0])) { + // If the first item is not a number, this means there is no indexed properties + // in this object. + sliceIndex = 0; + } else { + sliceIndex = names.length; + while (sliceIndex > 0) { + if (ObjectUtils.isArrayIndex(names[sliceIndex - 1])) { + break; + } + sliceIndex--; + } + } + } else { + sliceIndex = length; + } + + // It appears that getOwnPropertyNames always returns indexed properties + // first, so we can safely slice `names` for/against indexed properties. + // We do such clever operation to optimize very large array inspection. + if (options.ignoreIndexedProperties) { + // Keep items after `sliceIndex` index + names = names.slice(sliceIndex); + } else if (options.ignoreNonIndexedProperties) { + // Keep `sliceIndex` first items + names.length = sliceIndex; + } + } + + const safeGetterValues = objectActor._findSafeGetterValues(names); + const safeGetterNames = Object.keys(safeGetterValues); + // Merge the safe getter values into the existing properties list. + for (const name of safeGetterNames) { + if (!names.includes(name)) { + names.push(name); + } + } + + if (options.query) { + let { query } = options; + query = query.toLowerCase(); + names = names.filter(name => { + // Filter on attribute names + if (name.toLowerCase().includes(query)) { + return true; + } + // and then on attribute values + let desc; + try { + desc = objectActor.obj.getOwnPropertyDescriptor(name); + } catch (e) { + // Calling getOwnPropertyDescriptor on wrapped native prototypes is not + // allowed (bug 560072). + } + if (desc?.value && String(desc.value).includes(query)) { + return true; + } + return false; + }); + } + + if (options.sort) { + names.sort(); + } + + return { + size: names.length, + propertyName(index) { + return names[index]; + }, + propertyDescription(index) { + const name = names[index]; + let desc = objectActor._propertyDescriptor(name); + if (!desc) { + desc = safeGetterValues[name]; + } else if (name in safeGetterValues) { + // Merge the safe getter values into the existing properties list. + const { getterValue, getterPrototypeLevel } = safeGetterValues[name]; + desc.getterValue = getterValue; + desc.getterPrototypeLevel = getterPrototypeLevel; + } + return desc; + }, + }; +} + +function getMapEntries(obj) { + // Iterating over a Map via .entries goes through various intermediate + // objects - an Iterator object, then a 2-element Array object, then the + // actual values we care about. We don't have Xrays to Iterator objects, + // so we get Opaque wrappers for them. And even though we have Xrays to + // Arrays, the semantics often deny access to the entires based on the + // nature of the values. So we need waive Xrays for the iterator object + // and the tupes, and then re-apply them on the underlying values until + // we fix bug 1023984. + // + // Even then though, we might want to continue waiving Xrays here for the + // same reason we do so for Arrays above - this filtering behavior is likely + // to be more confusing than beneficial in the case of Object previews. + const raw = obj.unsafeDereference(); + const iterator = obj.makeDebuggeeValue( + waiveXrays(Map.prototype.keys.call(raw)) + ); + return [...DevToolsUtils.makeDebuggeeIterator(iterator)].map(k => { + const key = waiveXrays(ObjectUtils.unwrapDebuggeeValue(k)); + const value = Map.prototype.get.call(raw, key); + return [key, value]; + }); +} + +function enumMapEntries(objectActor) { + const entries = getMapEntries(objectActor.obj); + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, value].map(val => gripFromEntry(objectActor, val)); + } + }, + size: entries.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const [key, val] = entries[index]; + return { + enumerable: true, + value: { + type: "mapEntry", + preview: { + key: gripFromEntry(objectActor, key), + value: gripFromEntry(objectActor, val), + }, + }, + }; + }, + }; +} + +function enumStorageEntries(objectActor) { + // Iterating over local / sessionStorage entries goes through various + // intermediate objects - an Iterator object, then a 2-element Array object, + // then the actual values we care about. We don't have Xrays to Iterator + // objects, so we get Opaque wrappers for them. + const raw = objectActor.obj.unsafeDereference(); + const keys = []; + for (let i = 0; i < raw.length; i++) { + keys.push(raw.key(i)); + } + return { + [Symbol.iterator]: function*() { + for (const key of keys) { + const value = raw.getItem(key); + yield [key, value].map(val => gripFromEntry(objectActor, val)); + } + }, + size: keys.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const key = keys[index]; + const val = raw.getItem(key); + return { + enumerable: true, + value: { + type: "storageEntry", + preview: { + key: gripFromEntry(objectActor, key), + value: gripFromEntry(objectActor, val), + }, + }, + }; + }, + }; +} + +function enumURLSearchParamsEntries(objectActor) { + let obj = objectActor.obj; + let raw = obj.unsafeDereference(); + const entries = [...waiveXrays(URLSearchParams.prototype.entries.call(raw))]; + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, value]; + } + }, + size: entries.length, + propertyName(index) { + // UrlSearchParams entries can have the same key multiple times (e.g. `?a=1&a=2`), + // so let's return the index as a name to be able to display them properly in the client. + return index; + }, + propertyDescription(index) { + const [key, value] = entries[index]; + + return { + enumerable: true, + value: { + type: "urlSearchParamsEntry", + preview: { + key: gripFromEntry(objectActor, key), + value: gripFromEntry(objectActor, value), + }, + }, + }; + }, + }; +} + +function enumFormDataEntries(objectActor) { + let obj = objectActor.obj; + let raw = obj.unsafeDereference(); + const entries = [...waiveXrays(FormData.prototype.entries.call(raw))]; + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, value]; + } + }, + size: entries.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const [key, value] = entries[index]; + + return { + enumerable: true, + value: { + type: "formDataEntry", + preview: { + key: gripFromEntry(objectActor, key), + value: gripFromEntry(objectActor, value), + }, + }, + }; + }, + }; +} + +function enumHeadersEntries(objectActor) { + let raw = objectActor.obj.unsafeDereference(); + const entries = [...waiveXrays(Headers.prototype.entries.call(raw))]; + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, value]; + } + }, + size: entries.length, + propertyName(index) { + return entries[index][0]; + }, + propertyDescription(index) { + return { + enumerable: true, + value: gripFromEntry(objectActor, entries[index][1]), + }; + }, + }; +} + +function enumHighlightRegistryEntries(objectActor) { + const entriesFuncDbgObj = objectActor.obj.getProperty("entries").return; + const entriesDbgObj = entriesFuncDbgObj ? entriesFuncDbgObj.call(objectActor.obj).return : null; + const entries = entriesDbgObj + ? [...waiveXrays( entriesDbgObj.unsafeDereference())] + : []; + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, gripFromEntry(objectActor, value)]; + } + }, + size: entries.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const [key, value] = entries[index]; + return { + enumerable: true, + value: { + type: "highlightRegistryEntry", + preview: { + key: key, + value: gripFromEntry(objectActor, value), + }, + }, + }; + }, + }; +} + +function enumMidiInputMapEntries(objectActor) { + let raw = objectActor.obj.unsafeDereference(); + // We need to waive `raw` as we can't get the iterator from the Xray for MapLike (See Bug 1173651). + // We also need to waive Xrays on the result of the call to `entries` as we don't have + // Xrays to Iterator objects (see Bug 1023984) + const entries = Array.from( + waiveXrays(MIDIInputMap.prototype.entries.call(waiveXrays(raw))) + ); + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, gripFromEntry(objectActor, value)]; + } + }, + size: entries.length, + propertyName(index) { + return entries[index][0]; + }, + propertyDescription(index) { + return { + enumerable: true, + value: gripFromEntry(objectActor, entries[index][1]), + }; + }, + }; +} + +function enumMidiOutputMapEntries(objectActor) { + let raw = objectActor.obj.unsafeDereference(); + // We need to waive `raw` as we can't get the iterator from the Xray for MapLike (See Bug 1173651). + // We also need to waive Xrays on the result of the call to `entries` as we don't have + // Xrays to Iterator objects (see Bug 1023984) + const entries = Array.from( + waiveXrays(MIDIOutputMap.prototype.entries.call(waiveXrays(raw))) + ); + + return { + [Symbol.iterator]: function*() { + for (const [key, value] of entries) { + yield [key, gripFromEntry(objectActor, value)]; + } + }, + size: entries.length, + propertyName(index) { + return entries[index][0]; + }, + propertyDescription(index) { + return { + enumerable: true, + value: gripFromEntry(objectActor, entries[index][1]), + }; + }, + }; +} + +function getWeakMapEntries(obj) { + // We currently lack XrayWrappers for WeakMap, so when we iterate over + // the values, the temporary iterator objects get created in the target + // compartment. However, we _do_ have Xrays to Object now, so we end up + // Xraying those temporary objects, and filtering access to |it.value| + // based on whether or not it's Xrayable and/or callable, which breaks + // the for/of iteration. + // + // This code is designed to handle untrusted objects, so we can safely + // waive Xrays on the iterable, and relying on the Debugger machinery to + // make sure we handle the resulting objects carefully. + const raw = obj.unsafeDereference(); + const keys = waiveXrays(ChromeUtils.nondeterministicGetWeakMapKeys(raw)); + + return keys.map(k => [k, WeakMap.prototype.get.call(raw, k)]); +} + +function enumWeakMapEntries(objectActor) { + const entries = getWeakMapEntries(objectActor.obj); + + return { + [Symbol.iterator]: function*() { + for (let i = 0; i < entries.length; i++) { + yield entries[i].map(val => gripFromEntry(objectActor, val)); + } + }, + size: entries.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const [key, val] = entries[index]; + return { + enumerable: true, + value: { + type: "mapEntry", + preview: { + key: gripFromEntry(objectActor, key), + value: gripFromEntry(objectActor, val), + }, + }, + }; + }, + }; +} + +function getSetValues(obj) { + // We currently lack XrayWrappers for Set, so when we iterate over + // the values, the temporary iterator objects get created in the target + // compartment. However, we _do_ have Xrays to Object now, so we end up + // Xraying those temporary objects, and filtering access to |it.value| + // based on whether or not it's Xrayable and/or callable, which breaks + // the for/of iteration. + // + // This code is designed to handle untrusted objects, so we can safely + // waive Xrays on the iterable, and relying on the Debugger machinery to + // make sure we handle the resulting objects carefully. + const raw = obj.unsafeDereference(); + const iterator = obj.makeDebuggeeValue( + waiveXrays(Set.prototype.values.call(raw)) + ); + return [...DevToolsUtils.makeDebuggeeIterator(iterator)]; +} + +function enumSetEntries(objectActor) { + const values = getSetValues(objectActor.obj).map(v => + waiveXrays(ObjectUtils.unwrapDebuggeeValue(v)) + ); + + return { + [Symbol.iterator]: function*() { + for (const item of values) { + yield gripFromEntry(objectActor, item); + } + }, + size: values.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const val = values[index]; + return { + enumerable: true, + value: gripFromEntry(objectActor, val), + }; + }, + }; +} + +function getWeakSetEntries(obj) { + // We currently lack XrayWrappers for WeakSet, so when we iterate over + // the values, the temporary iterator objects get created in the target + // compartment. However, we _do_ have Xrays to Object now, so we end up + // Xraying those temporary objects, and filtering access to |it.value| + // based on whether or not it's Xrayable and/or callable, which breaks + // the for/of iteration. + // + // This code is designed to handle untrusted objects, so we can safely + // waive Xrays on the iterable, and relying on the Debugger machinery to + // make sure we handle the resulting objects carefully. + const raw = obj.unsafeDereference(); + return waiveXrays(ChromeUtils.nondeterministicGetWeakSetKeys(raw)); +} + +function enumWeakSetEntries(objectActor) { + const keys = getWeakSetEntries(objectActor.obj); + + return { + [Symbol.iterator]: function*() { + for (const item of keys) { + yield gripFromEntry(objectActor, item); + } + }, + size: keys.length, + propertyName(index) { + return index; + }, + propertyDescription(index) { + const val = keys[index]; + return { + enumerable: true, + value: gripFromEntry(objectActor, val), + }; + }, + }; +} + +/** + * Returns true if the parameter can be stored as a 32-bit unsigned integer. + * If so, it will be suitable for use as the length of an array object. + * + * @param num Number + * The number to test. + * @return Boolean + */ +function isUint32(num) { + return num >>> 0 === num; +} + +module.exports = { + PropertyIteratorActor, + enumMapEntries, + enumMidiInputMapEntries, + enumMidiOutputMapEntries, + enumSetEntries, + enumURLSearchParamsEntries, + enumFormDataEntries, + enumHeadersEntries, + enumHighlightRegistryEntries, + enumWeakMapEntries, + enumWeakSetEntries, +}; |