diff options
Diffstat (limited to 'js/xpconnect/tests/chrome/test_xrayToJS.xhtml')
-rw-r--r-- | js/xpconnect/tests/chrome/test_xrayToJS.xhtml | 1200 |
1 files changed, 1200 insertions, 0 deletions
diff --git a/js/xpconnect/tests/chrome/test_xrayToJS.xhtml b/js/xpconnect/tests/chrome/test_xrayToJS.xhtml new file mode 100644 index 0000000000..cc009a2d55 --- /dev/null +++ b/js/xpconnect/tests/chrome/test_xrayToJS.xhtml @@ -0,0 +1,1200 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=933681 +--> +<window title="Mozilla Bug 933681" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=933681" + target="_blank">Mozilla Bug 933681</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for ES constructors on Xrayed globals. **/ + SimpleTest.waitForExplicitFinish(); + let global = Cu.getGlobalForObject.bind(Cu); + + function checkThrows(f, rgxp, msg) { + try { + f(); + ok(false, "Should have thrown: " + msg); + } catch (e) { + ok(true, "Threw as expected: " + msg); + ok(rgxp.test(e), "Message correct: " + e); + } + } + + var { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + var isNightlyBuild = AppConstants.NIGHTLY_BUILD; + var isReleaseOrBeta = AppConstants.RELEASE_OR_BETA; + + let typedArrayClasses = ['Uint8Array', 'Int8Array', 'Uint16Array', 'Int16Array', + 'Uint32Array', 'Int32Array', 'Float32Array', 'Float64Array', + 'Uint8ClampedArray']; + let errorObjectClasses = ['Error', 'InternalError', 'EvalError', 'RangeError', 'ReferenceError', + 'SyntaxError', 'TypeError', 'URIError', 'AggregateError']; + + // A simpleConstructors entry can either be the name of a constructor as a + // string, or an object containing the properties `name`, and `args`. + // In the former case, the constructor is invoked without any args; in the + // latter case, it is invoked with `args` as the arguments list. + let simpleConstructors = ['Object', 'Function', 'Array', 'Boolean', 'Date', 'Number', + 'String', 'RegExp', 'ArrayBuffer', 'WeakMap', 'WeakSet', 'Map', 'Set', + {name: 'Promise', args: [function(){}]}]; + + // All TypedArray constructors can be called with zero arguments. + simpleConstructors = simpleConstructors.concat(typedArrayClasses); + + // All Error constructors except AggregateError can be called with zero arguments. + simpleConstructors = simpleConstructors.concat(errorObjectClasses.filter(name => { + return name !== 'AggregateError'; + })); + + function go() { + window.iwin = document.getElementById('ifr').contentWindow; + /* global iwin */ + + // Test constructors that can be instantiated with zero arguments, or with + // a fixed set of arguments provided using `...rest`. + for (let c of simpleConstructors) { + var args = []; + if (typeof c === 'object') { + args = c.args; + c = c.name; + } + ok(iwin[c], "Constructors appear: " + c); + is(iwin[c], Cu.unwaiveXrays(iwin.wrappedJSObject[c]), + "we end up with the appropriate constructor: " + c); + is(Cu.unwaiveXrays(Cu.waiveXrays(new iwin[c](...args)).constructor), iwin[c], + "constructor property is set up right: " + c); + let expectedProto = Cu.isOpaqueWrapper(new iwin[c](...args)) ? + iwin.Object.prototype : iwin[c].prototype; + is(Object.getPrototypeOf(new iwin[c](...args)), expectedProto, + "prototype is correct: " + c); + is(global(new iwin[c](...args)), iwin, "Got the right global: " + c); + } + + // Test Object in more detail. + var num = new iwin.Object(4); + is(Cu.waiveXrays(num).valueOf(), 4, "primitive object construction works"); + is(global(num), iwin, "correct global for num"); + var obj = new iwin.Object(); + obj.foo = 2; + var withProto = Cu.unwaiveXrays(Cu.waiveXrays(iwin).Object.create(obj)); + is(global(withProto), iwin, "correct global for withProto"); + is(Cu.waiveXrays(withProto).foo, 2, "Inherits properly"); + + // Test Function. + var primitiveFun = new iwin.Function('return 2'); + is(global(primitiveFun), iwin, "function construction works"); + is(primitiveFun(), 2, "basic function works"); + var doSetFoo = new iwin.Function('arg', 'arg.foo = 2;'); + is(global(doSetFoo), iwin, "function with args works"); + try { + doSetFoo({}); + ok(false, "should have thrown while setting property on object"); + } catch (e) { + ok(!!/denied/.test(e), "Threw correctly: " + e); + } + var factoryFun = new iwin.Function('return {foo: 32}'); + is(global(factoryFun), iwin, "proper global for factoryFun"); + is(factoryFun().foo, 32, "factoryFun invokable"); + is(global(factoryFun()), iwin, "minted objects live in the content scope"); + testXray('Function', factoryFun, new iwin.Function(), ['length', 'name']); + var echoThis = new iwin.Function('return this;'); + echoThis.wrappedJSObject.bind = 42; + var boundEchoThis = echoThis.bind(document); + is(boundEchoThis(), document, "bind() works correctly over Xrays"); + is(global(boundEchoThis), window, "bound functions live in the caller's scope"); + ok(/return this/.test(echoThis.toSource()), 'toSource works: ' + echoThis.toSource()); + ok(/return this/.test(echoThis.toString()), 'toString works: ' + echoThis.toString()); + is(iwin.Function.prototype, Object.getPrototypeOf(echoThis), "Function.prototype works for standard classes"); + is(echoThis.prototype, undefined, "Function.prototype not visible for non standard constructors"); + iwin.eval('var foopyFunction = function namedFoopyFunction(a, b, c) {}'); + var foopyFunction = Cu.unwaiveXrays(Cu.waiveXrays(iwin).foopyFunction); + ok(Cu.isXrayWrapper(foopyFunction), "Should be Xrays"); + is(foopyFunction.name, "namedFoopyFunction", ".name works over Xrays"); + is(foopyFunction.length, 3, ".length works over Xrays"); + ok(Object.getOwnPropertyNames(foopyFunction).includes('length'), "Should list length"); + ok(Object.getOwnPropertyNames(foopyFunction).includes('name'), "Should list name"); + ok(!Object.getOwnPropertyNames(foopyFunction).includes('prototype'), "Should not list prototype"); + ok(Object.getOwnPropertyNames(iwin.Array).includes('prototype'), "Should list prototype for standard constructor"); + + // Test BoundFunction. + iwin.eval('var boundFoopyFunction = foopyFunction.bind(null, 1)'); + var boundFoopyFunction = Cu.unwaiveXrays(Cu.waiveXrays(iwin).boundFoopyFunction); + is(boundFoopyFunction.name, "bound namedFoopyFunction", "bound .name works over Xrays"); + is(boundFoopyFunction.length, 2, "bound .length works over Xrays"); + is(JSON.stringify(Object.getOwnPropertyNames(boundFoopyFunction).sort()), '["length","name"]', "Should list length and name"); + // Mutate .name, it's just a data property. + iwin.eval('Object.defineProperty(boundFoopyFunction, "name", {value: "foobar", configurable: true, writable: true});'); + is(boundFoopyFunction.name, "foobar", "mutated .name works over Xrays"); + iwin.eval('boundFoopyFunction.name = 123;'); + is(boundFoopyFunction.name, undefined, "only support string for .name"); + iwin.eval('delete boundFoopyFunction.name'); + is(boundFoopyFunction.name, undefined, "deleted .name works over Xrays"); + // Mutate .length. + iwin.eval('Object.defineProperty(boundFoopyFunction, "length", {value: 456, configurable: true, writable: true});'); + is(boundFoopyFunction.length, 456, "mutated .length works over Xrays"); + iwin.eval('boundFoopyFunction.length = "bar";'); + is(boundFoopyFunction.length, undefined, "only support number for .length"); + + // Test proxies. + var targetObject = new iwin.Object(); + targetObject.foo = 9; + var forwardingProxy = new iwin.Proxy(targetObject, new iwin.Object()); + is(global(forwardingProxy), iwin, "proxy global correct"); + is(Cu.waiveXrays(forwardingProxy).foo, 9, "forwards correctly"); + + // Test AggregateError. + { + // AggregateError throws when called without an iterable object as its first argument. + let args = [new iwin.Array()]; + + ok(iwin.AggregateError, "AggregateError constructor is present"); + is(iwin.AggregateError, Cu.unwaiveXrays(iwin.wrappedJSObject.AggregateError), + "we end up with the appropriate AggregateError constructor"); + is(Cu.unwaiveXrays(Cu.waiveXrays(new iwin.AggregateError(...args)).constructor), iwin.AggregateError, + "AggregateError constructor property is set up right"); + let expectedProto = Cu.isOpaqueWrapper(new iwin.AggregateError(...args)) ? + iwin.Object.prototype : iwin.AggregateError.prototype; + is(Object.getPrototypeOf(new iwin.AggregateError(...args)), expectedProto, + "AggregateError prototype is correct"); + is(global(new iwin.AggregateError(...args)), iwin, "Got the right global for AggregateError"); + } + + // Test eval. + var toEval = "({a: 2, b: {foo: 'bar'}, f: function() { return window; }})"; + is(global(iwin.eval(toEval)), iwin, "eval creates objects in the correct global"); + is(iwin.eval(toEval).b.foo, 'bar', "eval-ed object looks right"); + is(Cu.waiveXrays(iwin.eval(toEval)).f(), Cu.waiveXrays(iwin), "evaled function works right"); + + testDate(); + + testObject(); + + testArray(); + + testTypedArrays(); + + testErrorObjects(); + + testRegExp(); + + testPromise(); + + testArrayBuffer(); + + testMap(); + + testSet(); + + testWeakMap(); + + testWeakSet(); + + testProxy(); + + testDataView(); + + testNumber(); + + SimpleTest.finish(); + } + + // Maintain a static list of the properties that are available on each standard + // prototype, so that we make sure to audit any new ones to make sure they're + // Xray-safe. + // + // DO NOT CHANGE WTIHOUT REVIEW FROM AN XPCONNECT PEER. + var gPrototypeProperties = {}; + var gConstructorProperties = {}; + // Properties which cannot be invoked if callable without potentially + // rendering the object useless. + var gStatefulProperties = {}; + function constructorProps(arr) { + // Some props live on all constructors + return arr.concat(["prototype", "length", "name"]); + } + gPrototypeProperties.Date = + ["getTime", "getTimezoneOffset", "getYear", "getFullYear", "getUTCFullYear", + "getMonth", "getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", + "getHours", "getUTCHours", "getMinutes", "getUTCMinutes", "getSeconds", + "getUTCSeconds", "getMilliseconds", "getUTCMilliseconds", "setTime", + "setYear", "setFullYear", "setUTCFullYear", "setMonth", "setUTCMonth", + "setDate", "setUTCDate", "setHours", "setUTCHours", "setMinutes", + "setUTCMinutes", "setSeconds", "setUTCSeconds", "setMilliseconds", + "setUTCMilliseconds", "toUTCString", "toLocaleString", + "toLocaleDateString", "toLocaleTimeString", "toDateString", "toTimeString", + "toISOString", "toJSON", "toSource", "toString", "valueOf", "constructor", + "toGMTString", Symbol.toPrimitive]; + gConstructorProperties.Date = constructorProps(["UTC", "parse", "now"]); + gPrototypeProperties.Object = + ["constructor", "toSource", "toString", "toLocaleString", "valueOf", + "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", + "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__", + "__proto__"]; + gConstructorProperties.Object = + constructorProps(["setPrototypeOf", "getOwnPropertyDescriptor", "getOwnPropertyDescriptors", + "keys", "is", "defineProperty", "defineProperties", "create", + "getOwnPropertyNames", "getOwnPropertySymbols", + "preventExtensions", "freeze", "fromEntries", "isFrozen", "seal", + "isSealed", "assign", "getPrototypeOf", "values", + "entries", "isExtensible", "hasOwn", "groupBy"]); + gPrototypeProperties.Array = + ["length", "toSource", "toString", "toLocaleString", "join", "reverse", "sort", "push", + "pop", "shift", "unshift", "splice", "concat", "slice", "lastIndexOf", "indexOf", + "includes", "forEach", "map", "reduce", "reduceRight", "filter", "some", "every", "find", + "findIndex", "copyWithin", "fill", Symbol.iterator, Symbol.unscopables, "entries", "keys", + "values", "constructor", "flat", "flatMap", "at", "findLast", "findLastIndex", + "toReversed", "toSorted", "toSpliced", "with"]; + gConstructorProperties.Array = + constructorProps(["isArray", "from", "fromAsync", "of", Symbol.species]); + for (let c of typedArrayClasses) { + gPrototypeProperties[c] = ["constructor", "BYTES_PER_ELEMENT"]; + gConstructorProperties[c] = constructorProps(["BYTES_PER_ELEMENT"]); + } + // There is no TypedArray constructor, looks like. + is(window.TypedArray, undefined, "If this ever changes, add to this test!"); + for (let c of errorObjectClasses) { + gPrototypeProperties[c] = ["constructor", "name", "message", "stack"]; + gConstructorProperties[c] = constructorProps([]); + } + // toString and toSource only live on the parent proto (Error.prototype). + gPrototypeProperties.Error.push('toString'); + gPrototypeProperties.Error.push('toSource'); + + gPrototypeProperties.Function = + ["constructor", "toSource", "toString", "apply", "call", "bind", + "length", "name", "arguments", "caller", Symbol.hasInstance]; + gConstructorProperties.Function = constructorProps([]) + + gPrototypeProperties.RegExp = + ["constructor", "toSource", "toString", "compile", "exec", "test", + Symbol.match, Symbol.matchAll, Symbol.replace, Symbol.search, Symbol.split, + "flags", "dotAll", "global", "hasIndices", "ignoreCase", "multiline", "source", "sticky", + "unicode", "unicodeSets"]; + gConstructorProperties.RegExp = + constructorProps(["input", "lastMatch", "lastParen", + "leftContext", "rightContext", "$1", "$2", "$3", "$4", + "$5", "$6", "$7", "$8", "$9", "$_", "$&", "$+", + "$`", "$'", Symbol.species]) + + gPrototypeProperties.Promise = + ["constructor", "catch", "then", "finally", Symbol.toStringTag]; + gConstructorProperties.Promise = + constructorProps(["resolve", "reject", "all", "allSettled", "any", "race", + "withResolvers", Symbol.species]); + + gPrototypeProperties.ArrayBuffer = + ["constructor", "byteLength", "detached", "slice", Symbol.toStringTag, "transfer", "transferToFixedLength"]; + gConstructorProperties.ArrayBuffer = + constructorProps(["isView", Symbol.species]); + gStatefulProperties.ArrayBuffer = ["transfer", "transferToFixedLength"] + + gPrototypeProperties.SharedArrayBuffer = ["constructor", "slice", "byteLength", "detached", Symbol.toStringTag, "transfer", "transferToFixedLength"]; + gConstructorProperties.SharedArrayBuffer = constructorProps([Symbol.species]); + gStatefulProperties.SharedArrayBuffer = ["transfer", "transferToFixedLength"] + + gPrototypeProperties.Map = + ["constructor", "size", Symbol.toStringTag, "get", "has", "set", "delete", + "keys", "values", "clear", "forEach", "entries", Symbol.iterator]; + gConstructorProperties.Map = + constructorProps(["groupBy", Symbol.species]); + + gPrototypeProperties.Set = + ["constructor", "size", Symbol.toStringTag, "has", "add", "delete", + "keys", "values", "clear", "forEach", "entries", Symbol.iterator]; + gConstructorProperties.Set = + constructorProps([Symbol.species]); + + gPrototypeProperties.WeakMap = + ["constructor", Symbol.toStringTag, "get", "has", "set", "delete"]; + gConstructorProperties.WeakMap = + constructorProps([]); + + gPrototypeProperties.WeakSet = + ["constructor", Symbol.toStringTag, "has", "add", "delete"]; + gConstructorProperties.WeakSet = + constructorProps([]); + + gPrototypeProperties.DataView = + ["constructor", "buffer", "byteLength", "byteOffset", Symbol.toStringTag, + "getInt8", "getUint8", "getInt16", "getUint16", + "getInt32", "getUint32", "getFloat32", "getFloat64", + "setInt8", "setUint8", "setInt16", "setUint16", + "setInt32", "setUint32", "setFloat32", "setFloat64", + "getBigInt64", "getBigUint64", "setBigInt64", "setBigUint64"] + gConstructorProperties.DataView = constructorProps([]); + + // Sort an array that may contain symbols as well as strings. + function sortProperties(arr) { + function sortKey(prop) { + return typeof prop + ":" + prop.toString(); + } + arr.sort((a, b) => sortKey(a) < sortKey(b) ? -1 : +1); + } + + // Sort all the lists so we don't need to mutate them later (or copy them + // again to sort them). + for (let c of Object.keys(gPrototypeProperties)) + sortProperties(gPrototypeProperties[c]); + for (let c of Object.keys(gConstructorProperties)) + sortProperties(gConstructorProperties[c]); + + function filterOut(array, props) { + return array.filter(p => !props.includes(p)); + } + + function isTypedArrayClass(classname) { + return typedArrayClasses.includes(classname); + } + + function propertyIsGetter(obj, name, classname) { + return !!Object.getOwnPropertyDescriptor(obj, name).get; + } + + function testProtoCallables(protoCallables, xray, xrayProto, localProto, callablesExcluded) { + // Handle undefined callablesExcluded. + let dontCall = callablesExcluded ?? []; + for (let name of protoCallables) { + info("Running tests for property: " + name); + // Test both methods and getter properties. + function lookupCallable(obj) { + let desc = null; + do { + desc = Object.getOwnPropertyDescriptor(obj, name); + if (desc) { + break; + } + obj = Object.getPrototypeOf(obj); + } while (obj); + return desc ? (desc.get || desc.value) : undefined; + }; + ok(xrayProto.hasOwnProperty(name), `proto should have the property '${name}' as own`); + ok(!xray.hasOwnProperty(name), `instance should not have the property '${name}' as own`); + let method = lookupCallable(xrayProto); + is(typeof method, 'function', "Methods from Xrays are functions"); + is(global(method), window, "Methods from Xrays are local"); + ok(method instanceof Function, "instanceof works on methods from Xrays"); + is(lookupCallable(xrayProto), method, "Holder caching works properly"); + is(lookupCallable(xray), method, "Proto props resolve on the instance"); + let local = lookupCallable(localProto); + is(method.length, local.length, "Function.length identical"); + if (!method.length && !dontCall.includes(name)) { + is(method.call(xray) + "", local.call(xray) + "", + "Xray and local method results stringify identically"); + + // If invoking this method returns something non-Xrayable (opaque), the + // stringification is going to return [object Object]. + // This happens for set[@@iterator] and other Iterator objects. + let callable = lookupCallable(xray.wrappedJSObject); + if (!Cu.isOpaqueWrapper(method.call(xray)) && callable) { + is(method.call(xray) + "", + callable.call(xray.wrappedJSObject) + "", + "Xray and waived method results stringify identically"); + } + } + } + } + + function testCtorCallables(ctorCallables, xrayCtor, localCtor) { + for (let name of ctorCallables) { + // Don't try to test Function.prototype, since that is in fact a callable + // but doesn't really do the things we expect callables to do here + // (e.g. it's in the wrong global, since it gets Xrayed itself). + if (name == "prototype" && localCtor.name == "Function") { + continue; + } + info(`Running tests for property: ${localCtor.name}.${name}`); + // Test both methods and getter properties. + function lookupCallable(obj) { + let desc = null; + do { + desc = Object.getOwnPropertyDescriptor(obj, name); + obj = Object.getPrototypeOf(obj); + } while (!desc); + return desc.get || desc.value; + }; + + ok(xrayCtor.hasOwnProperty(name), "ctor should have the property as own"); + let method = lookupCallable(xrayCtor); + is(typeof method, 'function', "Methods from ctor Xrays are functions"); + is(global(method), window, "Methods from ctor Xrays are local"); + ok(method instanceof Function, + "instanceof works on methods from ctor Xrays"); + is(lookupCallable(xrayCtor), method, + "Holder caching works properly on ctors"); + let local = lookupCallable(localCtor); + is(method.length, local.length, + "Function.length identical for method from ctor"); + // Don't try to do the return-value check on Date.now(), since there is + // absolutely no reason it should return the same value each time. + // + // Also don't try to do the return-value check on Regexp.lastMatch and + // Regexp["$&"] (which are aliases), because they get state off the global + // they live in, as far as I can tell, so testing them over Xrays will be + // wrong: on the Xray they will actaully get the lastMatch of _our_ + // global, not the Xrayed one. + if (!method.length && + !(localCtor.name == "Date" && name == "now") && + !(localCtor.name == "RegExp" && (name == "lastMatch" || name == "$&"))) { + is(method.call(xrayCtor) + "", local.call(xrayCtor) + "", + "Xray and local method results stringify identically on constructors"); + is(method.call(xrayCtor) + "", + lookupCallable(xrayCtor.wrappedJSObject).call(xrayCtor.wrappedJSObject) + "", + "Xray and waived method results stringify identically"); + } + } + } + + function testXray(classname, xray, xray2, propsToSkip, ctorPropsToSkip = []) { + propsToSkip = propsToSkip || []; + let xrayProto = Object.getPrototypeOf(xray); + let localProto = window[classname].prototype; + let desiredProtoProps = Object.getOwnPropertyNames(localProto).sort(); + + is(desiredProtoProps.toSource(), + gPrototypeProperties[classname].filter(id => typeof id === "string").toSource(), + "A property on the " + classname + + " prototype has changed! You need a security audit from an XPConnect peer"); + is(Object.getOwnPropertySymbols(localProto).map(uneval).sort().toSource(), + gPrototypeProperties[classname].filter(id => typeof id !== "string").map(uneval).sort().toSource(), + "A symbol-keyed property on the " + classname + + " prototype has been changed! You need a security audit from an XPConnect peer"); + + let protoProps = filterOut(desiredProtoProps, propsToSkip); + let protoCallables = protoProps.filter(name => propertyIsGetter(localProto, name, classname) || + typeof localProto[name] == 'function' && + name != 'constructor'); + let callablesExcluded = gStatefulProperties[classname]; + ok(!!protoCallables.length, "Need something to test"); + is(xrayProto, iwin[classname].prototype, "Xray proto is correct"); + is(xrayProto, xray.__proto__, "Proto accessors agree"); + var protoProto = classname == "Object" ? null : iwin.Object.prototype; + is(Object.getPrototypeOf(xrayProto), protoProto, "proto proto is correct"); + testProtoCallables(protoCallables, xray, xrayProto, localProto, callablesExcluded); + is(Object.getOwnPropertyNames(xrayProto).sort().toSource(), + protoProps.toSource(), "getOwnPropertyNames works"); + is(Object.getOwnPropertySymbols(xrayProto).map(uneval).sort().toSource(), + gPrototypeProperties[classname].filter(id => typeof id !== "string" && !propsToSkip.includes(id)) + .map(uneval).sort().toSource(), + "getOwnPropertySymbols works"); + + is(xrayProto.constructor, iwin[classname], "constructor property works"); + + xrayProto.expando = 42; + is(xray.expando, 42, "Xrayed instances see proto expandos"); + is(xray2.expando, 42, "Xrayed instances see proto expandos"); + + // Now test constructors + let localCtor = window[classname]; + let xrayCtor = xrayProto.constructor; + // We already checked that this is the same as iwin[classname] + + let desiredCtorProps = + Object.getOwnPropertyNames(localCtor).sort(); + is(desiredCtorProps.toSource(), + gConstructorProperties[classname].filter(id => typeof id === "string").toSource(), + "A property on the " + classname + + " constructor has changed! You need a security audit from an XPConnect peer"); + let desiredCtorSymbols = + Object.getOwnPropertySymbols(localCtor).map(uneval).sort() + is(desiredCtorSymbols.toSource(), + gConstructorProperties[classname].filter(id => typeof id !== "string").map(uneval).sort().toSource(), + "A symbol-keyed property on the " + classname + + " constructor has been changed! You need a security audit from an XPConnect peer"); + + let ctorProps = filterOut(desiredCtorProps, ctorPropsToSkip); + let ctorSymbols = filterOut(desiredCtorSymbols, ctorPropsToSkip.map(uneval)); + let ctorCallables = ctorProps.filter(name => propertyIsGetter(localCtor, name, classname) || + typeof localCtor[name] == 'function'); + testCtorCallables(ctorCallables, xrayCtor, localCtor); + is(Object.getOwnPropertyNames(xrayCtor).sort().toSource(), + ctorProps.toSource(), "getOwnPropertyNames works on Xrayed ctors"); + is(Object.getOwnPropertySymbols(xrayCtor).map(uneval).sort().toSource(), + ctorSymbols.toSource(), "getOwnPropertySymbols works on Xrayed ctors"); + } + + // We will need arraysEqual and testArrayIterators both in this global scope + // and in sandboxes. + function arraysEqual(arr1, arr2, reason) { + is(arr1.length, arr2.length, `${reason}; lengths should be equal`) + for (var i = 0; i < arr1.length; ++i) { + if (Array.isArray(arr2[i])) { + arraysEqual(arr1[i], arr2[i], `${reason}; item at index ${i}`); + } else { + is(arr1[i], arr2[i], `${reason}; item at index ${i} should be equal`); + } + } + } + + function testArrayIterators(arrayLike, equivalentArray, reason) { + arraysEqual([...arrayLike], equivalentArray, `${reason}; spread operator`); + arraysEqual([...arrayLike.entries()], [...equivalentArray.entries()], + `${reason}; entries`); + arraysEqual([...arrayLike.keys()], [...equivalentArray.keys()], + `${reason}; keys`); + if (arrayLike.values) { + arraysEqual([...arrayLike.values()], equivalentArray, + `${reason}; values`); + } + + var forEachCopy = []; + arrayLike.forEach(function(arg) { forEachCopy.push(arg); }); + arraysEqual(forEachCopy, equivalentArray, `${reason}; forEach copy`); + + var everyCopy = []; + arrayLike.every(function(arg) { everyCopy.push(arg); return true; }); + arraysEqual(everyCopy, equivalentArray, `${reason}; every() copy`); + + var filterCopy = []; + var filterResult = arrayLike.filter(function(arg) { + filterCopy.push(arg); + return true; + }); + arraysEqual(filterCopy, equivalentArray, `${reason}; filter copy`); + arraysEqual([...filterResult], equivalentArray, `${reason}; filter result`); + + var findCopy = []; + arrayLike.find(function(arg) { findCopy.push(arg); return false; }); + arraysEqual(findCopy, equivalentArray, `${reason}; find() copy`); + + var findIndexCopy = []; + arrayLike.findIndex(function(arg) { findIndexCopy.push(arg); return false; }); + arraysEqual(findIndexCopy, equivalentArray, `${reason}; findIndex() copy`); + + var mapCopy = []; + var mapResult = arrayLike.map(function(arg) { mapCopy.push(arg); return arg}); + arraysEqual(mapCopy, equivalentArray, `${reason}; map() copy`); + arraysEqual([...mapResult], equivalentArray, `${reason}; map() result`); + + var reduceCopy = []; + arrayLike.reduce(function(_, arg) { reduceCopy.push(arg); }, 0); + arraysEqual(reduceCopy, equivalentArray, `${reason}; reduce() copy`); + + var reduceRightCopy = []; + arrayLike.reduceRight(function(_, arg) { reduceRightCopy.unshift(arg); }, 0); + arraysEqual(reduceRightCopy, equivalentArray, `${reason}; reduceRight() copy`); + + var someCopy = []; + arrayLike.some(function(arg) { someCopy.push(arg); return false; }); + arraysEqual(someCopy, equivalentArray, `${reason}; some() copy`); + } + + function testDate() { + // toGMTString is handled oddly in the engine. We don't bother to support + // it over Xrays. + let propsToSkip = ['toGMTString']; + + testXray('Date', new iwin.Date(), new iwin.Date(), propsToSkip); + + // Test the self-hosted toLocaleString. + var d = new iwin.Date(); + isnot(d.toLocaleString, Cu.unwaiveXrays(d.wrappedJSObject.toLocaleString), "Different function identities"); + is(Cu.getGlobalForObject(d.toLocaleString), window, "Xray global is correct"); + is(Cu.getGlobalForObject(d.wrappedJSObject.toLocaleString), iwin, "Underlying global is correct"); + is(d.toLocaleString('de-DE'), d.wrappedJSObject.toLocaleString('de-DE'), "Results match"); + } + + var uniqueSymbol; + + function testObject() { + testXray('Object', Cu.unwaiveXrays(Cu.waiveXrays(iwin).Object.create(new iwin.Object())), + new iwin.Object(), []); + + // Construct an object full of tricky things. + let symbolProps = ''; + uniqueSymbol = iwin.eval('var uniqueSymbol = Symbol("uniqueSymbol"); uniqueSymbol'); + symbolProps = `, [uniqueSymbol]: 43, + [Symbol.for("registrySymbolProp")]: 44`; + var trickyObject = + iwin.eval(`(function() { + var o = new Object({ + primitiveProp: 42, objectProp: { foo: 2 }, + xoProp: top, hasOwnProperty: 10, + get getterProp() { return 2; }, + set setterProp(x) { }, + get getterSetterProp() { return 3; }, + set getterSetterProp(x) { }, + callableProp: function() { }, + nonXrayableProp: new Map()[Symbol.iterator]() + ${symbolProps} + }); + Object.defineProperty(o, "nonConfigurableGetterSetterProp", + { get: function() { return 5; }, set: function() {} }); + return o; + })()`); + testTrickyObject(trickyObject); + } + + function testArray() { + // The |length| property is generally very weird, especially with respect + // to its behavior on the prototype. Array.prototype is actually an Array + // instance, and therefore has a vestigial .length. But we don't want to + // show that over Xrays, and generally want .length to just appear as an + // |own| data property. So we add it to the ignore list here, and check it + // separately. + // + // |Symbol.unscopables| should in principle be exposed, but it is + // inconvenient (as it's a data property, unsupported by ClassSpec) and + // low value. + let propsToSkip = ['length', Symbol.unscopables]; + + testXray('Array', new iwin.Array(20), new iwin.Array(), propsToSkip); + + let symbolProps = ''; + uniqueSymbol = iwin.eval('var uniqueSymbol = Symbol("uniqueSymbol"); uniqueSymbol'); + symbolProps = `trickyArray[uniqueSymbol] = 43; + trickyArray[Symbol.for("registrySymbolProp")] = 44;`; + var trickyArray = + iwin.eval(`var trickyArray = []; + trickyArray.primitiveProp = 42; + trickyArray.objectProp = { foo: 2 }; + trickyArray.xoProp = top; + trickyArray.hasOwnProperty = 10; + Object.defineProperty(trickyArray, 'getterProp', { get: function() { return 2; }}); + Object.defineProperty(trickyArray, 'setterProp', { set: function(x) {}}); + Object.defineProperty(trickyArray, 'getterSetterProp', { get: function() { return 3; }, set: function(x) {}, configurable: true}); + Object.defineProperty(trickyArray, 'nonConfigurableGetterSetterProp', { get: function() { return 5; }, set: function(x) {}}); + trickyArray.callableProp = function() {}; + trickyArray.nonXrayableProp = new Map()[Symbol.iterator](); + ${symbolProps} + trickyArray;`); + + // Test indexed access. + trickyArray.wrappedJSObject[9] = "some indexed property"; + is(trickyArray[9], "some indexed property", "indexed properties work correctly over Xrays"); + is(trickyArray.length, 10, "Length works correctly over Xrays"); + checkThrows(function() { "use strict"; delete trickyArray.length; }, /config/, "Can't delete non-configurable 'length' property"); + delete trickyArray[9]; + is(trickyArray[9], undefined, "Delete works correctly over Xrays"); + is(trickyArray.wrappedJSObject[9], undefined, "Delete works correctly over Xrays (viewed via waiver)"); + is(trickyArray.length, 10, "length doesn't change"); + trickyArray[11] = "some other indexed property"; + is(trickyArray.length, 12, "length now changes"); + is(trickyArray.wrappedJSObject[11], "some other indexed property"); + trickyArray.length = 0; + is(trickyArray.length, 0, "Setting length works over Xray"); + is(trickyArray[11], undefined, "Setting length truncates over Xray"); + Object.defineProperty(trickyArray, 'length', { configurable: false, enumerable: false, writable: false, value: 0 }); + trickyArray[1] = "hi"; + is(trickyArray.length, 0, "Length remains non-writable"); + is(trickyArray[1], undefined, "Frozen length forbids new properties"); + is(trickyArray instanceof iwin.Array, true, "instanceof should work across xray wrappers."); + testTrickyObject(trickyArray); + + testArrayIterators(new iwin.Array(1, 1, 2, 3, 5), [1, 1, 2, 3, 5]); + } + + // Parts of this function are kind of specific to testing Object, but we factor + // it out so that we can re-use the trickyObject stuff on Arrays. + function testTrickyObject(trickyObject) { + + // Make sure it looks right under the hood. + is(trickyObject.wrappedJSObject.getterProp, 2, "Underlying object has getter"); + is(Cu.unwaiveXrays(trickyObject.wrappedJSObject.xoProp), top, "Underlying object has xo property"); + + // Test getOwnPropertyNames. + var expectedNames = ['objectProp', 'primitiveProp']; + if (trickyObject instanceof iwin.Array) + expectedNames.push('length'); + is(Object.getOwnPropertyNames(trickyObject).sort().toSource(), + expectedNames.sort().toSource(), "getOwnPropertyNames should be filtered correctly"); + var expectedSymbols = [Symbol.for("registrySymbolProp"), uniqueSymbol]; + is(Object.getOwnPropertySymbols(trickyObject).map(uneval).sort().toSource(), + expectedSymbols.map(uneval).sort().toSource(), + "getOwnPropertySymbols should be filtered correctly"); + + // Test that cloning uses the Xray view. + var cloned = Cu.cloneInto(trickyObject, this); + is(Object.getOwnPropertyNames(cloned).sort().toSource(), + expectedNames.sort().toSource(), "structured clone should use the Xray view"); + is(Object.getOwnPropertySymbols(cloned).map(uneval).sort().toSource(), + "[]", "structured cloning doesn't clone symbol-keyed properties yet"); + + // Test iteration and in-place modification. Beware of 'expando', which is the property + // we placed on the xray proto. + var propCount = 0; + for (let prop in trickyObject) { + if (prop == 'primitiveProp') + trickyObject[prop] = trickyObject[prop] - 10; + if (prop != 'expando') { + // eslint-disable-next-line no-self-assign + trickyObject[prop] = trickyObject[prop]; + } + ++propCount; + } + is(propCount, 3, "Should iterate the correct number of times"); + + // Test Object.keys. + is(Object.keys(trickyObject).sort().toSource(), + ['objectProp', 'primitiveProp'].toSource(), "Object.keys should be filtered correctly"); + + // Test getOwnPropertyDescriptor. + is(trickyObject.primitiveProp, 32, "primitive prop works"); + is(trickyObject.objectProp.foo, 2, "object prop works"); + is(typeof trickyObject.callableProp, 'undefined', "filtering works correctly"); + is(Object.getOwnPropertyDescriptor(trickyObject, 'primitiveProp').value, 32, "getOwnPropertyDescriptor works"); + is(Object.getOwnPropertyDescriptor(trickyObject, 'xoProp'), undefined, "filtering works with getOwnPropertyDescriptor"); + + // Test defineProperty. + + trickyObject.primitiveSetByXray = 'fourty two'; + is(trickyObject.primitiveSetByXray, 'fourty two', "Can set primitive correctly over Xray (ready via Xray)"); + is(trickyObject.wrappedJSObject.primitiveSetByXray, 'fourty two', "Can set primitive correctly over Xray (ready via Waiver)"); + + var newContentObject = iwin.eval('new Object({prop: 99, get getterProp() { return 2; }})'); + trickyObject.objectSetByXray = newContentObject; + is(trickyObject.objectSetByXray.prop, 99, "Can set object correctly over Xray (ready via Xray)"); + is(trickyObject.wrappedJSObject.objectSetByXray.prop, 99, "Can set object correctly over Xray (ready via Waiver)"); + checkThrows(function() { trickyObject.rejectedProp = {foo: 33}}, /cross-origin object/, + "Should reject privileged object property definition"); + + // Test JSON.stringify. + var jsonStr = JSON.stringify(newContentObject); + ok(/prop/.test(jsonStr), "JSON stringification should work: " + jsonStr); + + // Test deletion. + delete newContentObject.prop; + ok(!newContentObject.hasOwnProperty('prop'), "Deletion should work"); + ok(!newContentObject.wrappedJSObject.hasOwnProperty('prop'), "Deletion should forward"); + delete newContentObject.getterProp; + ok(newContentObject.wrappedJSObject.hasOwnProperty('getterProp'), "Deletion be no-op for filtered property"); + + // We should be able to overwrite an existing accessor prop and convert it + // to a value prop. + is(trickyObject.wrappedJSObject.getterSetterProp, 3, "Underlying object has getter"); + is(trickyObject.getterSetterProp, undefined, "Filtering properly over Xray"); + trickyObject.getterSetterProp = 'redefined'; + is(trickyObject.getterSetterProp, 'redefined', "Redefinition works"); + is(trickyObject.wrappedJSObject.getterSetterProp, 'redefined', "Redefinition forwards"); + + // We should NOT be able to overwrite an existing non-configurable accessor + // prop, though. + is(trickyObject.wrappedJSObject.nonConfigurableGetterSetterProp, 5, + "Underlying object has getter"); + is(trickyObject.nonConfigurableGetterSetterProp, undefined, + "Filtering properly over Xray here too"); + is((trickyObject.nonConfigurableGetterSetterProp = 'redefined'), 'redefined', + "Assigning to non-configurable prop should fail silently in non-strict mode"); + checkThrows(function() { + "use strict"; + trickyObject.nonConfigurableGetterSetterProp = 'redefined'; + }, /config/, "Should throw when redefining non-configurable prop in strict mode"); + is(trickyObject.nonConfigurableGetterSetterProp, undefined, + "Redefinition should have failed"); + is(trickyObject.wrappedJSObject.nonConfigurableGetterSetterProp, 5, + "Redefinition really should have failed"); + + checkThrows(function() { trickyObject.hasOwnProperty = 33; }, /shadow/, + "Should reject shadowing of pre-existing inherited properties over Xrays"); + + checkThrows(function() { Object.defineProperty(trickyObject, 'rejectedProp', { get() {}}); }, + /accessor property/, "Should reject accessor property definition"); + } + + function testTypedArrays() { + // We don't invoke testXray with %TypedArray%, because that function isn't + // set up to deal with "anonymous" dependent classes (that is, classes not + // visible as a global property, which %TypedArray% is not), and fixing it + // up is more trouble than it's worth. + + var typedArrayProto = Object.getPrototypeOf(Int8Array.prototype); + + var desiredInheritedProps = Object.getOwnPropertyNames(typedArrayProto).sort(); + var inheritedProps = + filterOut(desiredInheritedProps, ["BYTES_PER_ELEMENT", "constructor"]); + + var inheritedCallables = + inheritedProps.filter(name => (propertyIsGetter(typedArrayProto, name) || + typeof typedArrayProto[name] === "function") && + name !== "constructor"); + + for (let c of typedArrayClasses) { + var t = new iwin[c](10); + checkThrows(function() { t[2]; }, /performant/, "direct property-wise reading of typed arrays forbidden over Xrays"); + checkThrows(function() { t[2] = 3; }, /performant/, "direct property-wise writing of typed arrays forbidden over Xrays"); + var wesb = new Cu.Sandbox([iwin], {isWebExtensionContentScript: true}); + wesb.t = t; + wesb.eval('t[2] = 3'); + is(wesb.eval('t.wrappedJSObject[2]'), 3, "direct property-wise writing of typed arrays allowed for WebExtension content scripts"); + is(wesb.eval('t[2]'), 3, "direct property-wise reading and writing of typed arrays allowed for WebExtensions content scripts"); + + t.wrappedJSObject[2] = 3; + is(t.wrappedJSObject[2], 3, "accessing elements over waivers works"); + t.wrappedJSObject.expando = 'hi'; + is(t.wrappedJSObject.expando, 'hi', "access expandos over waivers works"); + is(Cu.cloneInto(t, window)[2], 3, "cloneInto works"); + is(Cu.cloneInto(t, window).expando, undefined, "cloneInto does not copy expandos"); + is(Object.getOwnPropertyNames(t).sort().toSource(), + '["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]', + "Only indexed properties visible over Xrays"); + Object.defineProperty(t.wrappedJSObject, 'length', {value: 42}); + is(t.wrappedJSObject.length, 42, "Set tricky expando") + is(t.length, 10, "Length accessor works over Xrays") + is(t.byteLength, t.length * window[c].prototype.BYTES_PER_ELEMENT, "byteLength accessor works over Xrays") + + // Can create TypedArray from content ArrayBuffer + var buffer = new iwin.ArrayBuffer(8); + new window[c](buffer); + + var xray = new iwin[c](0); + var xrayTypedArrayProto = Object.getPrototypeOf(Object.getPrototypeOf(xray)); + testProtoCallables(inheritedCallables, new iwin[c](0), xrayTypedArrayProto, typedArrayProto); + + // When testing iterators, make sure to do so from inside our web + // extension sandbox, since from chrome we can't poke their indices. Note + // that we have to actually recreate our functions that touch typed array + // indices inside the sandbox, not just export them, because otherwise + // they'll just run with our principal anyway. + // + // But we do want to export is(), since we want ours called. + wesb.eval(String(arraysEqual)); + wesb.eval(String(testArrayIterators)); + Cu.exportFunction(is, wesb, + { defineAs: "is" }); + wesb.eval('testArrayIterators(t, [0, 0, 3, 0, 0, 0, 0, 0, 0, 0])'); + } + } + + function testErrorObjects() { + // We only invoke testXray with Error, because that function isn't set up + // to deal with dependent classes and fixing it up is more trouble than + // it's worth. + testXray('Error', new iwin.Error('some error message'), new iwin.Error()); + + // Make sure that the dependent classes have their prototypes set up correctly. + for (let c of errorObjectClasses.filter(x => x != "Error")) { + var args = ['some message']; + if (c === 'AggregateError') { + // AggregateError's first argument is the list of aggregated errors. + args.unshift(new iwin.Array('error 1', 'error 2')); + } + var e = new iwin[c](...args); + is(Object.getPrototypeOf(e).name, c, "Prototype has correct name"); + is(Object.getPrototypeOf(Object.getPrototypeOf(e)), iwin.Error.prototype, "Dependent prototype set up correctly"); + is(e.name, c, "Exception name inherited correctly"); + + function testProperty(name, criterion, goodReplacement, faultyReplacement) { + ok(criterion(e[name]), name + " property is correct: " + e[name]); + e.wrappedJSObject[name] = goodReplacement; + is(e[name], goodReplacement, name + " property ok after replacement: " + goodReplacement); + e.wrappedJSObject[name] = faultyReplacement; + is(e[name], name == 'message' ? "" : undefined, name + " property skipped after suspicious replacement"); + } + testProperty('message', x => x == 'some message', 'some other message', 42); + testProperty('fileName', x => x == '', 'otherFilename.html', new iwin.Object()); + testProperty('columnNumber', x => x == 1, 99, 99.5); + testProperty('lineNumber', x => x == 0, 50, 'foo'); + + if (c === 'AggregateError') { + let {errors} = e; + is(errors.length, 2, "errors property has the correct length"); + is(errors[0], 'error 1', "errors[0] has the correct value"); + is(errors[1], 'error 2', "errors[1] has the correct value"); + + e.wrappedJSObject.errors = 42; + is(e.wrappedJSObject.errors, 42, "errors is a plain data property"); + is(e.errors, 42, "visible over Xrays"); + } + + // Note - an Exception newed via Xrays is going to have an empty stack given the + // current semantics and implementation. This tests the current behavior, but that + // may change in bug 1036527 or similar. + // + // Furthermore, xrays should always return an error's original stack, and + // not overwrite it. + var stack = e.stack; + ok(/^\s*$/.test(stack), "stack property should be correct"); + e.wrappedJSObject.stack = "not a stack"; + is(e.stack, stack, "Xrays should never get an overwritten stack property."); + + // Test the .cause property is correctly handled, too. + if (isNightlyBuild) { + let cause = 'error cause'; + let options = new iwin.Object(); + options.cause = cause; + args.push(options); + + let e = new iwin[c](...args); + is(e.cause, cause); + + e.wrappedJSObject.cause = 42; + is(e.wrappedJSObject.cause, 42, "cause is a plain data property"); + is(e.cause, 42, "visible over Xrays"); + } + } + } + + function testRegExp() { + // RegExp statics are very weird, and in particular RegExp has static + // properties that have to do with the last regexp execution in the global. + // Xraying those makes no sense, so we just skip constructor properties for + // RegExp xrays. + // RegExp[@@species] is affected by above skip, but we don't fix it until + // compelling use-case appears, as supporting RegExp[@@species] while + // skipping other static properties makes things complicated. + let ctorPropsToSkip = ["input", "lastMatch", "lastParen", + "leftContext", "rightContext", "$1", "$2", "$3", + "$4", "$5", "$6", "$7", "$8", "$9", "$_", "$&", + "$+", "$`", "$'", Symbol.species]; + testXray('RegExp', new iwin.RegExp('foo'), new iwin.RegExp(), [], + ctorPropsToSkip); + + // Test the self-hosted |flags| property, toString, and toSource. + for (var flags of ["", "g", "i", "m", "y", "gimy"]) { + var re = new iwin.RegExp("foo", flags); + is(re.flags, re.wrappedJSObject.flags, "Results match"); + + isnot(re.toString, Cu.unwaiveXrays(re.wrappedJSObject.toString), "Different function identities"); + is(Cu.getGlobalForObject(re.toString), window, "Xray global is correct"); + is(Cu.getGlobalForObject(re.wrappedJSObject.toString), iwin, "Underlying global is correct"); + is(re.toString(), re.wrappedJSObject.toString(), "Results match"); + + isnot(re.toSource, Cu.unwaiveXrays(re.wrappedJSObject.toSource), "Different function identities"); + is(Cu.getGlobalForObject(re.toSource), window, "Xray global is correct"); + if (re.wrappedJSObject.toSource) { + is(Cu.getGlobalForObject(re.wrappedJSObject.toSource), iwin, "Underlying global is correct"); + is(re.toSource(), re.wrappedJSObject.toSource(), "Results match"); + } + + // Test with modified flags accessors + iwin.eval(` +var props = ["global", "ignoreCase", "multiline", "sticky", "source", "unicode"]; +var origDescs = {}; +for (var prop of props) { + origDescs[prop] = Object.getOwnPropertyDescriptor(RegExp.prototype, prop); + Object.defineProperty(RegExp.prototype, prop, { + get: function() { + throw new Error("modified accessor is called"); + } + }); +} +`); + try { + is(re.flags, flags, "Unmodified flags accessors are called"); + is(re.toString(), "/foo/" + flags, "Unmodified flags and source accessors are called"); + is(re.toSource(), "/foo/" + flags, "Unmodified flags and source accessors are called"); + } finally { + iwin.eval(` +for (var prop of props) { + Object.defineProperty(RegExp.prototype, prop, origDescs[prop]); +} +`); + } + } + } + + // Note: this is a small set of basic tests. More in-depth tests are located + // in test_promise_xrays.html. + function testPromise() { + testXray('Promise', new iwin.Promise(function(){}), new iwin.Promise(function(){})); + + // Test catch and then. + var pr = new iwin.Promise(function(){}); + isnot(pr.catch, Cu.unwaiveXrays(pr.wrappedJSObject.catch), "Different function identities"); + is(Cu.getGlobalForObject(pr.catch), window, "Xray global is correct"); + is(Cu.getGlobalForObject(pr.wrappedJSObject.catch), iwin, "Underlying global is correct"); + + isnot(pr.then, Cu.unwaiveXrays(pr.wrappedJSObject.then), "Different function identities"); + is(Cu.getGlobalForObject(pr.then), window, "Xray global is correct"); + is(Cu.getGlobalForObject(pr.wrappedJSObject.then), iwin, "Underlying global is correct"); + } + + function testArrayBuffer() { + let constructors = ['ArrayBuffer']; + + for (const c of constructors) { + testXray(c, new iwin[c](0), new iwin[c](12)); + + var t = new iwin[c](12); + is(t.byteLength, 12, `${c} byteLength is correct`); + + is(t.slice(4).byteLength, 8, `${c} byteLength is correct after slicing`); + is(Cu.getGlobalForObject(t.slice(4)), iwin, "Slice results lives in the target compartment"); + is(Object.getPrototypeOf(t.slice(4)), iwin[c].prototype, "Slice results proto lives in target compartment") + + var i32Array = new Int32Array(t); + // i32Array is going to be created in the buffer's target compartment, + // but usually this is unobservable, because the proto is set to + // the current compartment's prototype. + // However Xrays ignore the object's proto and claim its proto is + // the default proto for that class in the relevant compartment, + // so see through this proto hack. + todo_is(Object.getPrototypeOf(i32Array), Int32Array.prototype, "Int32Array has correct proto"); + is(i32Array.length, 3, `Int32Array created from Xray ${c} has the correct length`); + is(i32Array.buffer, t, "Int32Array has the correct buffer that we passed in"); + + i32Array = new iwin.Int32Array(t); + is(Object.getPrototypeOf(i32Array), iwin.Int32Array.prototype, "Xray Int32Array has correct proto"); + is(i32Array.length, 3, `Xray Int32Array created from Xray ${c} has the correct length`); + is(i32Array.buffer, t, "Xray Int32Array has the correct buffer that we passed in"); + + t = (new iwin.Int32Array(2)).buffer; + is(t.byteLength, 8, `Can access ${c} returned by buffer property`); + } + } + + function testMap() { + testXray('Map', new iwin.Map(), new iwin.Map()); + + var t = iwin.eval(`new Map([[1, "a"], [null, "b"]])`); + is(t.size, 2, "Map size is correct"); + is(t.get(1), "a", "Key 1 has the correct value"); + is(t.get(null), "b", "Key null has the correct value"); + is(t.has(1), true, "Has Key 1"); + is(t.set(3, 5).get(3), 5, "Correctly sets key"); + is(t.delete(null), true, "Key null can be deleted"); + + let values = []; + t.forEach((value, key) => values.push(value)); + is(values.toString(), "a,5", "forEach enumerates values correctly"); + + t.clear(); + is(t.size, 0, "Map is empty after calling clear"); + } + + function testSet() { + testXray('Set', new iwin.Set(), new iwin.Set()); + + var t = iwin.eval(`new Set([1, null])`); + is(t.size, 2, "Set size is correct"); + is(t.has(1), true, "Contains 1"); + is(t.has(null), true, "Contains null"); + is(t.add(5).has(5), true, "Can add value to set"); + is(t.delete(null), true, "Value null can be deleted"); + + let values = []; + t.forEach(value => values.push(value)); + is(values.toString(), "1,5", "forEach enumerates values correctly"); + + t.clear(); + is(t.size, 0, "Set is empty after calling clear"); + } + + function testWeakMap() { + testXray('WeakMap', new iwin.WeakMap(), new iwin.WeakMap()); + + var key1 = iwin.eval(`var key1 = {}; key1`); + var key2 = iwin.eval(`var key2 = []; key2`); + var key3 = iwin.eval(`var key3 = /a/; key3`); + var key4 = {}; + var key5 = []; + var t = iwin.eval(`new WeakMap([[key1, "a"], [key2, "b"]])`); + is(t.get(key1), "a", "key1 has the correct value"); + is(t.get(key2), "b", "key2 has the correct value"); + is(t.has(key1), true, "Has key1"); + is(t.has(key3), false, "Doesn't have key3"); + is(t.has(key5), false, "Doesn't have key5"); + is(t.set(key4, 5).get(key4), 5, "Correctly sets key"); + is(t.get(key1), "a", "key1 has the correct value after modification"); + is(t.get(key2), "b", "key2 has the correct value after modification"); + is(t.delete(key1), true, "key1 can be deleted"); + is(t.delete(key2), true, "key2 can be deleted"); + is(t.delete(key3), false, "key3 cannot be deleted"); + is(t.delete(key4), true, "key4 can be deleted"); + is(t.delete(key5), false, "key5 cannot be deleted"); + } + + function testWeakSet() { + testXray('WeakSet', new iwin.WeakSet(), new iwin.WeakSet()); + + var key1 = iwin.eval(`var key1 = {}; key1`); + var key2 = iwin.eval(`var key2 = []; key2`); + var key3 = iwin.eval(`var key3 = /a/; key3`); + var key4 = {}; + var key5 = []; + var t = iwin.eval(`new WeakSet([key1, key2])`); + is(t.has(key1), true, "Has key1"); + is(t.has(key2), true, "Has key2"); + is(t.has(key3), false, "Doesn't have key3"); + is(t.has(key5), false, "Doesn't have key5"); + is(t.add(key4, 5).has(key4), true, "Can add value to set"); + is(t.delete(key1), true, "key1 can be deleted"); + is(t.delete(key2), true, "key2 can be deleted"); + is(t.delete(key3), false, "key3 cannot be deleted"); + is(t.delete(key4), true, "key4 can be deleted"); + is(t.delete(key5), false, "key5 cannot be deleted"); + } + + function testProxy() { + let ProxyCtor = iwin.Proxy; + is(Object.getOwnPropertyNames(ProxyCtor).sort().toSource(), + ["length", "name"].sort().toSource(), + "Xrayed Proxy constructor should not have any properties"); + is(ProxyCtor.prototype, undefined, "Proxy.prototype should not be set"); + // Proxy.revocable can safely be exposed, but it is not. + // Until it is supported, check that the property is not set. + is(ProxyCtor.revocable, undefined, "Proxy.reflect is not set"); + } + + function testDataView() { + testXray('DataView', new iwin.DataView(new iwin.ArrayBuffer(4)), + new iwin.DataView(new iwin.ArrayBuffer(8))); + + const versions = [() => iwin.eval(`new DataView(new ArrayBuffer(8))`), + () => new DataView(new iwin.ArrayBuffer(8))]; + + for (const constructor of versions) { + let t = constructor(); + is(t.byteLength, 8, `byteLength correct for "${constructor}"`); + is(t.byteOffset, 0, `byteOffset correct for "${constructor}"`); + is(t.buffer.byteLength, 8, `buffer works for "${constructor}"`); + + const get = ["getInt8", "getUint8", "getInt16", "getUint16", + "getInt32", "getUint32", "getFloat32", "getFloat64"]; + + const set = ["setInt8", "setUint8", "setInt16", "setUint16", + "setInt32", "setUint32", "setFloat32", "setFloat64"]; + + for (const f of get) { + let x = t[f](0); + is(x, 0, `${f} is 0 for "${constructor}"`); + is(typeof x, 'number', `typeof ${f} is number for "${constructor}"`); + } + + for (const f of ["getBigInt64", "getBigUint64"]) { + let x = t[f](0); + is(x, BigInt(0), `${f} is 0n for "${constructor}"`); + is(typeof x, 'bigint', `typeof ${f} is bigint for "${constructor}"`); + } + + for (let i = 0; i < set.length; i++) { + t[set[i]](0, 13); + is(t[get[i]](0), 13, `${get[i]}(0) afer ${set[i]}(0, 13) is 13 for "${constructor}"`); + } + + for (const k of ["BigInt64", "BigUint64"]) { + t["set" + k](0, BigInt(13)); + is(t["get" + k](0), BigInt(13), `get${k}(0) afer set${k}(0, 13n) is 13n for "${constructor}"`); + } + } + } + + function testNumber() { + // We don't actually support Xrays to Number yet. This is testing + // that case. If we add such support, we might have to start + // using a different non-Xrayed class here, if we can find one. + let xrayCtor = iwin.Number; + is(Object.getOwnPropertyNames(xrayCtor).sort().toSource(), + Object.getOwnPropertyNames(function() {}).sort().toSource(), + "We should not have any static properties on a non-Xrayable constructor"); + is(xrayCtor.noSuchProperty, undefined, + "Where did our noSuchProperty property come from?"); + } + + ]]> + </script> + <iframe id="ifr" onload="go();" src="http://example.org/tests/js/xpconnect/tests/mochitest/file_empty.html" /> +</window> |