--- a/devtools/server/actors/object.js
+++ b/devtools/server/actors/object.js
@@ -9,20 +9,16 @@
const { Cu, Ci } = require("chrome");
const { GeneratedLocation } = require("devtools/server/actors/common");
const { DebuggerServer } = require("devtools/server/main");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const { assert, dumpn } = DevToolsUtils;
loader.lazyRequireGetter(this, "ThreadSafeChromeUtils");
-const TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
- "Uint32Array", "Int8Array", "Int16Array", "Int32Array",
- "Float32Array", "Float64Array"];
-
// Number of items to preview in objects, arrays, maps, sets, lists,
// collections, etc.
const OBJECT_PREVIEW_MAX_ITEMS = 10;
/**
* Creates an actor for the specified object.
*
* @param obj Debugger.Object
@@ -115,17 +111,17 @@ ObjectActor.prototype = {
}
if (g.class == "Promise") {
g.promiseState = this._createPromiseState();
}
// FF40+: Allow to know how many properties an object has to lazily display them
// when there is a bunch.
- if (TYPED_ARRAY_CLASSES.indexOf(g.class) != -1) {
+ if (isTypedArray(g)) {
// Bug 1348761: getOwnPropertyNames is unnecessary slow on TypedArrays
let length = DevToolsUtils.getProperty(this.obj, "length");
g.ownPropertyLength = length;
} else if (g.class != "Proxy") {
g.ownPropertyLength = this.obj.getOwnPropertyNames().length;
}
let raw = this.obj.unsafeDereference();
@@ -344,18 +340,17 @@ ObjectActor.prototype = {
if (!unwrapped || unwrapped.isProxy) {
return safeGetterValues;
}
// Most objects don't have any safe getters but inherit some from their
// prototype. Avoid calling getOwnPropertyNames on objects that may have
// many properties like Array, strings or js objects. That to avoid
// freezing firefox when doing so.
- if (TYPED_ARRAY_CLASSES.includes(this.obj.class) ||
- ["Array", "Object", "String"].includes(this.obj.class)) {
+ if (isArray(this.obj) || ["Object", "String"].includes(this.obj.class)) {
obj = obj.proto;
level++;
}
while (obj) {
// Stop iterating when an inaccessible or a proxy object is found.
unwrapped = unwrap(obj);
if (!unwrapped || unwrapped.isProxy) {
@@ -815,17 +810,21 @@ function PropertyIteratorActor(objectAct
this.iterator = enumWeakMapEntries(objectActor);
} else if (cls == "Set") {
this.iterator = enumSetEntries(objectActor);
} else if (cls == "WeakSet") {
this.iterator = enumWeakSetEntries(objectActor);
} else {
throw new Error("Unsupported class to enumerate entries from: " + cls);
}
- } else if (options.ignoreNonIndexedProperties && !options.query) {
+ } else if (
+ isArray(objectActor.obj)
+ && options.ignoreNonIndexedProperties
+ && !options.query
+ ) {
this.iterator = enumArrayProperties(objectActor, options);
} else {
this.iterator = enumObjectProperties(objectActor, options);
}
}
PropertyIteratorActor.prototype = {
actorPrefix: "propertyIterator",
@@ -867,23 +866,23 @@ PropertyIteratorActor.prototype = {
PropertyIteratorActor.prototype.requestTypes = {
"names": PropertyIteratorActor.prototype.names,
"slice": PropertyIteratorActor.prototype.slice,
"all": PropertyIteratorActor.prototype.all,
};
function enumArrayProperties(objectActor, options) {
let length = DevToolsUtils.getProperty(objectActor.obj, "length");
- if (typeof length !== "number") {
+ if (!isSafePositiveInteger(length)) {
// Pseudo arrays are flagged as ArrayLike if they have
// subsequent indexed properties without having any length attribute.
length = 0;
let names = objectActor.obj.getOwnPropertyNames();
for (let key of names) {
- if (isNaN(key) || key != length++) {
+ if (!isSafeIndex(key) || key != length++) {
break;
}
}
}
return {
size: length,
propertyName(index) {
@@ -901,37 +900,54 @@ function enumObjectProperties(objectActo
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) {
let length = DevToolsUtils.getProperty(objectActor.obj, "length");
- if (typeof length !== "number") {
- // Pseudo arrays are flagged as ArrayLike if they have
- // subsequent indexed properties without having any length attribute.
- length = 0;
- for (let key of names) {
- if (isNaN(key) || key != length++) {
- break;
+ let sliceIndex;
+
+ const isLengthTrustworthy =
+ isSafePositiveInteger(length)
+ && (length > 0 && isSafeIndex(names[length - 1]))
+ && !isSafeIndex(names[length]);
+
+ if (!isLengthTrustworthy) {
+ // The length property may not reflect what the object looks like, let's find
+ // where indexed properties end.
+
+ if (!isSafeIndex(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 (isSafeIndex(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,
// like webaudio buffers.
if (options.ignoreIndexedProperties) {
- // Keep items after `length` index
- names = names.slice(length);
+ // Keep items after `sliceIndex` index
+ names = names.slice(sliceIndex);
} else if (options.ignoreNonIndexedProperties) {
- // Remove `length` first items
- names.splice(length);
+ // Keep `sliceIndex` first items
+ names.length = sliceIndex;
}
}
let safeGetterValues = objectActor._findSafeGetterValues(names, 0);
let safeGetterNames = Object.keys(safeGetterValues);
// Merge the safe getter values into the existing properties list.
for (let name of safeGetterNames) {
if (!names.includes(name)) {
@@ -1611,17 +1627,17 @@ function GenericObject(objectActor, grip
}
return true;
}
// Preview functions that do not rely on the object class.
DebuggerServer.ObjectActorPreviewers.Object = [
function TypedArray({obj, hooks}, grip) {
- if (TYPED_ARRAY_CLASSES.indexOf(obj.class) == -1) {
+ if (!isTypedArray(obj)) {
return false;
}
let length = DevToolsUtils.getProperty(obj, "length");
if (typeof length != "number") {
return false;
}
@@ -2484,15 +2500,66 @@ function unwrap(obj) {
if (!unwrapped || unwrapped === obj) {
return unwrapped;
}
// Recursively remove additional security wrappers.
return unwrap(unwrapped);
}
+const TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
+ "Uint32Array", "Int8Array", "Int16Array", "Int32Array",
+ "Float32Array", "Float64Array"];
+
+/**
+ * Returns true if a debuggee object is a typed array.
+ *
+ * @param obj Debugger.Object
+ * The debuggee object to test.
+ * @return Boolean
+ */
+function isTypedArray(object) {
+ return TYPED_ARRAY_CLASSES.includes(object.class);
+}
+
+/**
+ * Returns true if a debuggee object is an array, including a typed array.
+ *
+ * @param obj Debugger.Object
+ * The debuggee object to test.
+ * @return Boolean
+ */
+function isArray(object) {
+ return isTypedArray(object) || object.class === "Array";
+}
+
+/**
+ * Returns true if the parameter is a safe positive integer.
+ *
+ * @param num Number
+ * The number to test.
+ * @return Boolean
+ */
+function isSafePositiveInteger(num) {
+ return Number.isSafeInteger(num) && 1 / num > 0;
+}
+
+/**
+ * Returns true if the parameter is suitable to be an array index.
+ *
+ * @param num Any
+ * @return Boolean
+ */
+function isSafeIndex(str) {
+ // Transform the parameter to a number using the Unary operator.
+ let num = +str;
+ return isSafePositiveInteger(num) &&
+ // Check the string since unary can transform non number (boolean, null, …).
+ num + "" === str;
+}
+
exports.ObjectActor = ObjectActor;
exports.PropertyIteratorActor = PropertyIteratorActor;
exports.LongStringActor = LongStringActor;
exports.createValueGrip = createValueGrip;
exports.stringIsLong = stringIsLong;
exports.longStringGrip = longStringGrip;
exports.arrayBufferGrip = arrayBufferGrip;
new file mode 100644
--- /dev/null
+++ b/devtools/server/tests/unit/test_objectgrips-20.js
@@ -0,0 +1,237 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable no-shadow, max-nested-callbacks */
+
+"use strict";
+
+// Test that onEnumProperties returns the expected data
+// when passing `ignoreNonIndexedProperties` and `ignoreIndexedProperties` options
+// with various objects. (See Bug 1403065)
+
+async function run_test() {
+ do_test_pending();
+ await run_test_with_server(DebuggerServer);
+ await run_test_with_server(WorkerDebuggerServer);
+ do_test_finished();
+}
+
+const DO_NOT_CHECK_VALUE = Symbol();
+
+async function run_test_with_server(server) {
+ initTestDebuggerServer(server);
+ const debuggee = addTestGlobal("test-grips", server);
+ debuggee.eval(function stopMe(arg1) {
+ debugger;
+ }.toString());
+
+ const dbgClient = new DebuggerClient(server.connectPipe());
+ await dbgClient.connect();
+ const [,, threadClient] = await attachTestTabAndResume(dbgClient, "test-grips");
+
+ [{
+ evaledObject: { a: 10 },
+ expectedIndexedProperties: [],
+ expectedNonIndexedProperties: [["a", 10]],
+ }, {
+ evaledObject: { length: 10 },
+ expectedIndexedProperties: [],
+ expectedNonIndexedProperties: [["length", 10]],
+ }, {
+ evaledObject: { a: 10, 0: "indexed" },
+ expectedIndexedProperties: [["0", "indexed"]],
+ expectedNonIndexedProperties: [["a", 10]],
+ }, {
+ evaledObject: { 1: 1, length: 42, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [["length", 42], ["a", 10]],
+ }, {
+ evaledObject: { 1: 1, length: 2.34, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [["length", 2.34], ["a", 10]],
+ }, {
+ evaledObject: { 1: 1, length: -0, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [["length", -0], ["a", 10]],
+ }, {
+ evaledObject: { 1: 1, length: -10, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [["length", -10], ["a", 10]],
+ }, {
+ evaledObject: { 1: 1, length: true, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [["length", true], ["a", 10]],
+ }, {
+ evaledObject: { 1: 1, length: null, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [["length", DO_NOT_CHECK_VALUE], ["a", 10]],
+ }, {
+ evaledObject: { 1: 1, length: Math.pow(2, 53), a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [["length", 9007199254740992], ["a", 10]],
+ }, {
+ evaledObject: { 1: 1, length: "fake", a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [["length", "fake"], ["a", 10]],
+ }, {
+ evaledObject: { 1: 1, length: Infinity, a: 10 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [["length", DO_NOT_CHECK_VALUE], ["a", 10]],
+ }, {
+ evaledObject: { 0: 0, length: 0},
+ expectedIndexedProperties: [["0", 0]],
+ expectedNonIndexedProperties: [["length", 0]],
+ }, {
+ evaledObject: { 0: 0, 1: 1, length: 1},
+ expectedIndexedProperties: [["0", 0], ["1", 1]],
+ expectedNonIndexedProperties: [["length", 1]],
+ }, {
+ evaledObject: { length: 0},
+ expectedIndexedProperties: [],
+ expectedNonIndexedProperties: [["length", 0]],
+ }, {
+ evaledObject: { 1: 1 },
+ expectedIndexedProperties: [["1", 1]],
+ expectedNonIndexedProperties: [],
+ }, {
+ evaledObject: `(() => {
+ x = [12, 42];
+ x.foo = 90;
+ return x;
+ })()`,
+ expectedIndexedProperties: [["0", 12], ["1", 42]],
+ expectedNonIndexedProperties: [["length", 2], ["foo", 90]],
+ }, {
+ evaledObject: `(() => {
+ x = [12, 42];
+ x.length = 3;
+ return x;
+ })()`,
+ expectedIndexedProperties: [["0", 12], ["1", 42], ["2", undefined]],
+ expectedNonIndexedProperties: [["length", 3]],
+ }, {
+ evaledObject: `(() => {
+ x = [12, 42];
+ x.length = 1;
+ return x;
+ })()`,
+ expectedIndexedProperties: [["0", 12]],
+ expectedNonIndexedProperties: [["length", 1]],
+ }, {
+ evaledObject: `(() => {
+ x = [, 42,,];
+ x.foo = 90;
+ return x;
+ })()`,
+ expectedIndexedProperties: [["0", undefined], ["1", 42], ["2", undefined]],
+ expectedNonIndexedProperties: [["length", 3], ["foo", 90]],
+ }, {
+ evaledObject: `(() => {
+ x = Array(2);
+ x.foo = "bar";
+ x.bar = "foo";
+ return x;
+ })()`,
+ expectedIndexedProperties: [["0", undefined], ["1", undefined]],
+ expectedNonIndexedProperties: [["length", 2], ["foo", "bar"], ["bar", "foo"]],
+ }, {
+ evaledObject: `(() => {
+ x = new Int8Array(new ArrayBuffer(2));
+ x.foo = "bar";
+ x.bar = "foo";
+ return x;
+ })()`,
+ expectedIndexedProperties: [["0", 0], ["1", 0]],
+ expectedNonIndexedProperties: [
+ ["foo", "bar"],
+ ["bar", "foo"],
+ ["length", 2],
+ ["buffer", DO_NOT_CHECK_VALUE],
+ ["byteLength", 2],
+ ["byteOffset", 0],
+ ],
+ }].forEach(async (testData) => {
+ await test_object_grip(debuggee, dbgClient, threadClient, testData);
+ });
+
+ await dbgClient.close();
+}
+
+async function test_object_grip(debuggee, dbgClient, threadClient, testData = {}) {
+ const {
+ evaledObject,
+ expectedIndexedProperties,
+ expectedNonIndexedProperties,
+ } = testData;
+
+ return new Promise((resolve, reject) => {
+ threadClient.addOneTimeListener("paused", async function (event, packet) {
+ let [grip] = packet.frame.arguments;
+
+ let objClient = threadClient.pauseGrip(grip);
+
+ do_print(`
+ Check enumProperties response for
+ ${
+ typeof evaledObject === "string"
+ ? evaledObject
+ : JSON.stringify(evaledObject)
+ }
+ `);
+
+ // Checks the result of enumProperties.
+ let response = await objClient.enumProperties({ ignoreNonIndexedProperties: true });
+ await check_enum_properties(response, expectedIndexedProperties);
+
+ response = await objClient.enumProperties({ ignoreIndexedProperties: true });
+ await check_enum_properties(response, expectedNonIndexedProperties);
+
+ await threadClient.resume();
+ resolve();
+ });
+
+ debuggee.eval(`
+ stopMe(${
+ typeof evaledObject === "string"
+ ? evaledObject
+ : JSON.stringify(evaledObject)
+ });
+ `);
+ });
+}
+
+async function check_enum_properties(response, expected = []) {
+ ok(response && Object.getOwnPropertyNames(response).includes("iterator"),
+ "The response object has an iterator property");
+
+ const {iterator} = response;
+ equal(iterator.count, expected.length, "iterator.count has the expected value");
+
+ do_print("Check iterator.slice response for all properties");
+ let sliceResponse = await iterator.slice(0, iterator.count);
+ ok(sliceResponse && Object.getOwnPropertyNames(sliceResponse).includes("ownProperties"),
+ "The response object has an ownProperties property");
+
+ let {ownProperties} = sliceResponse;
+ let names = Object.getOwnPropertyNames(ownProperties);
+ equal(names.length, expected.length,
+ "The response has the expected number of properties");
+ for (let i = 0; i < names.length; i++) {
+ const name = names[i];
+ const [key, value] = expected[i];
+ equal(name, key, "Property has the expected name");
+ const property = ownProperties[name];
+
+ if (value === DO_NOT_CHECK_VALUE) {
+ return;
+ }
+
+ if (value === undefined) {
+ equal(property, undefined, `Response has no value for the "${key}" property`);
+ } else {
+ const propValue = property.hasOwnProperty("value")
+ ? property.value
+ : property.getterValue;
+ equal(propValue, value, `Property "${key}" has the expected value`);
+ }
+ }
+}