Revision: 5353
Author: [email protected]
Date: Mon Apr 15 14:31:05 2013
Log: Work around WeakMap not accepting some object keys.
https://codereview.appspot.com/8612048
* If the browser (particularly Firefox 20 & 21) provides a WeakMap which
does not accept certain objects as keys (particularly events), then
define a dual-implementation WeakMap constructor which uses the
browser's WeakMap unless it rejects the key, then falls back to our
WeakMap emulation.
* Domado contained code for working around being able to see the
emulated WeakMap's magic property lookups using Proxy handlers, which
had gotten out of date since it was never exercised as we have had no
browsers which have Proxy but not WeakMap; update it to use results
from actual proxies to obtain the magic property name.
* Domado's ProxyHandler.prototype now has a .constructor property so
that tamperProof knows its properties should be overridable.
[email protected], [email protected]
http://code.google.com/p/google-caja/source/detail?r=5353
Modified:
/trunk/src/com/google/caja/es53.js
/trunk/src/com/google/caja/parser/quasiliteral/ReservedNames.java
/trunk/src/com/google/caja/plugin/domado.js
/trunk/src/com/google/caja/ses/WeakMap.js
/trunk/tests/com/google/caja/plugin/es53-test-language-guest.html
=======================================
--- /trunk/src/com/google/caja/es53.js Wed Apr 10 14:03:43 2013
+++ /trunk/src/com/google/caja/es53.js Mon Apr 15 14:31:05 2013
@@ -26,7 +26,7 @@
* </ul>
*
* @author [email protected]
- * @requires json_sans_eval, cajaBuildVersion, taming, this
+ * @requires json_sans_eval, cajaBuildVersion, taming, this, document
* @provides ___, safeJSON, WeakMap, cajaVM
* @overrides Error, EvalError, RangeError, ReferenceError, SyntaxError,
* TypeError, URIError, ArrayLike, window
@@ -1415,44 +1415,88 @@
});
}
- WeakMap = WeakMap ?
- (function (WeakMap) {
- return markFunc(function () {
- var result = WeakMap();
- // DefineOwnProperty___ may not be defined yet.
- markFunc(result.get);
- result.get_v___ = result;
- result.get_c___ = false;
- result.get_w___ = false;
- result.get_gw___ = result;
- result.get_e___ = result;
- result.get_m___ = false;
- result.get_g___ = false;
- result.get_s___ = false;
+ var HostWeakMap = WeakMap;
+ if (typeof HostWeakMap === 'function') {
+ var hostWeakMapOK;
+ // There is a WeakMap -- is it good enough?
+ if (typeof document !== 'undefined') {
+ hostWeakMapOK = false;
+ var problematic = document.createEvent('HTMLEvents');
+ var testHostMap = new HostWeakMap();
+ try {
+ testHostMap.set(problematic, 1); // Firefox 20 will throw here
+ if (testHostMap.get(problematic) === 1) {
+ hostWeakMapOK = true;
+ }
+ } catch (e) {}
+ } else {
+ hostWeakMapOK = true;
+ }
+
+ if (hostWeakMapOK) {
+ // Whitelist WeakMap methods.
+ WeakMap = markFunc(function() {
+ var result = HostWeakMap();
+ // DefineOwnProperty___ may not be defined yet.
+ markFunc(result.get);
+ result.get_v___ = result;
+ result.get_c___ = false;
+ result.get_w___ = false;
+ result.get_gw___ = result;
+ result.get_e___ = result;
+ result.get_m___ = false;
+ result.get_g___ = false;
+ result.get_s___ = false;
- markFunc(result.set);
- result.set_v___ = result;
- result.set_c___ = false;
- result.set_w___ = false;
- result.set_gw___ = result;
- result.set_e___ = result;
- result.set_m___ = false;
- result.set_g___ = false;
- result.set_s___ = false;
+ markFunc(result.set);
+ result.set_v___ = result;
+ result.set_c___ = false;
+ result.set_w___ = false;
+ result.set_gw___ = result;
+ result.set_e___ = result;
+ result.set_m___ = false;
+ result.set_g___ = false;
+ result.set_s___ = false;
- markFunc(result.has);
- result.has_v___ = result;
- result.has_c___ = false;
- result.has_w___ = false;
- result.has_gw___ = result;
- result.has_e___ = result;
- result.has_m___ = false;
- result.has_g___ = false;
- result.has_s___ = false;
- return result;
+ markFunc(result.has);
+ result.has_v___ = result;
+ result.has_c___ = false;
+ result.has_w___ = false;
+ result.has_gw___ = result;
+ result.has_e___ = result;
+ result.has_m___ = false;
+ result.has_g___ = false;
+ result.has_s___ = false;
+ return result;
+ });
+ } else {
+ // Take advantage of both implementations.
+ WeakMap = markFunc(function DoubleWeakMap() {
+ var hmap = HostWeakMap();
+ var omap = newTable(true);
+
+ return snowWhite({
+ get: markFuncFreeze(function(key, opt_default) {
+ return hmap.has(key) ? hmap.get(key) :
+ omap.has(key) ? omap.get(key) : opt_default;
+ }),
+ set: markFuncFreeze(function(key, value) {
+ try {
+ hmap.set(key, value);
+ } catch (e) {
+ omap.set(key, value);
+ }
+ }),
+ has: markFuncFreeze(function(key) {
+ return hmap.has(key) || omap.has(key);
+ })
});
- })(WeakMap) :
- markFunc(function () { return newTable(true); });
+ });
+ }
+ } else {
+ // No platform WeakMap; build our own.
+ WeakMap = markFunc(function () { return newTable(true); });
+ }
var registeredImports = [];
=======================================
--- /trunk/src/com/google/caja/parser/quasiliteral/ReservedNames.java Thu
Jul 22 10:23:41 2010
+++ /trunk/src/com/google/caja/parser/quasiliteral/ReservedNames.java Mon
Apr 15 14:31:05 2013
@@ -55,6 +55,7 @@
/**
* Applies the class policy at runtime, making sure the argument is a
valid
* value for a node's class attribute.
+ * TODO(kpreid): Pretty sure this comment is false or obsolete.
*/
public static final String IDENT = "ident___";
/** Coerces the argument to a CSS number. */
=======================================
--- /trunk/src/com/google/caja/plugin/domado.js Thu Apr 11 19:15:10 2013
+++ /trunk/src/com/google/caja/plugin/domado.js Mon Apr 15 14:31:05 2013
@@ -233,6 +233,8 @@
this.target = target;
};
domitaModules.ProxyHandler.prototype = {
+ constructor: domitaModules.ProxyHandler,
+
// == fundamental traps ==
// Object.getOwnPropertyDescriptor(proxy, name) -> pd | undefined
@@ -557,6 +559,50 @@
function innocuous(f) {
return cajaVM.constFunc(f);
}
+
+ /**
+ * The Caja WeakMap emulation magic property name.
+ *
+ * This name can only be seen by using a proxy handler, but we need our
proxy
+ * handlers to permit it even on frozen-seeming proxies, so we have to
obtain
+ * it here, which we do by using a proxy to observe it.
+ *
+ * If proxies are unavailable or if WeakMaps do not use a magic
property, then
+ * weakMapMagicName will be a value unequal to any string.
+ */
+ var weakMapMagicName = {};
+ (function() {
+ if (!domitaModules.proxiesAvailable) {
+ // unobservable so doesn't matter
+ return;
+ }
+
+ var ProxyHandler = domitaModules.ProxyHandler;
+
+ // Create a proxy whose handler will stash away the magic name.
+ var handler = new ProxyHandler({});
+ handler.getOwnPropertyDescriptor = function(name) {
+ if (/^weakmap:/.test(name)) { weakMapMagicName = name; }
+ return ProxyHandler.prototype.getOwnPropertyDescriptor.call(this,
name);
+ };
+ handler.get = domitaModules.permuteProxyGetSet.getter(function(name) {
+ if (/^weakmap:/.test(name)) { weakMapMagicName = name; }
+ return ProxyHandler.prototype.get.call(this, name);
+ });
+ var proxy = Proxy.create(handler);
+
+ // Cause the proxy to be used as a key.
+ var w = new WeakMap();
+ try {
+ w.get(proxy);
+ } catch (e) {
+ console.error('Domado internal error: failed in WeakMap name
setup:', e);
+ }
+
+ // At this point, we have either obtained the magic name, or there is
no
+ // observable magic name, in which case weakMapMagicName is left at its
+ // initial {} value which is not === to any property name.
+ })();
var ExpandoProxyHandler = domitaModules.ExpandoProxyHandler =
(function() {
var getPropertyDescriptor = domitaModules.getPropertyDescriptor;
@@ -615,9 +661,9 @@
};
ExpandoProxyHandler.prototype.getOwnPropertyDescriptor = function
(name) {
- if (name === "ident___") {
+ if (name === weakMapMagicName) {
// Caja WeakMap emulation internal property
- return Object.getOwnPropertyDescriptor(this, "ident");
+ return Object.getOwnPropertyDescriptor(this, 'weakMapMagic');
} else {
return Object.getOwnPropertyDescriptor(this.storage, name);
}
@@ -632,9 +678,9 @@
this.storage || {});
};
ExpandoProxyHandler.prototype.defineProperty = function (name,
descriptor) {
- if (name === "ident___") {
+ if (name === weakMapMagicName) {
if (descriptor.set === null) descriptor.set = undefined;
- Object.defineProperty(this, "ident", descriptor);
+ Object.defineProperty(this, 'weakMapMagic', descriptor);
} else if (Object.prototype.hasOwnProperty(this.target, name)) {
// Forwards everything already defined (not expando).
return ProxyHandler.prototype.defineProperty.call(this, name,
@@ -647,7 +693,7 @@
return false;
};
ExpandoProxyHandler.prototype['delete'] = function (name) {
- if (name === "ident___") {
+ if (name === weakMapMagicName) {
return false;
} else if (Object.prototype.hasOwnProperty(this.target, name)) {
// Forwards everything already defined (not expando).
@@ -668,9 +714,9 @@
ExpandoProxyHandler.prototype.get =
domitaModules.permuteProxyGetSet.getter(
function (name) {
// Written for an ES5/3 bug, but probably useful for efficiency too.
- if (name === "ident___") {
+ if (name === weakMapMagicName) {
// Caja WeakMap emulation internal property
- return this.ident;
+ return this.weakMapMagic;
} else if (Object.prototype.hasOwnProperty.call(this.storage, name))
{
return this.storage[name];
} else {
@@ -755,7 +801,7 @@
CollectionProxyHandler.prototype.getOwnPropertyDescriptor =
function (name) {
var lookup;
- if (name !== 'ident___' && (lookup = this.col_lookup(name))) {
+ if (name !== weakMapMagicName && (lookup = this.col_lookup(name))) {
return {
configurable: true, // proxy invariant check
enumerable: true, // TODO(kpreid): may vary
@@ -770,7 +816,7 @@
CollectionProxyHandler.prototype.get =
domitaModules.permuteProxyGetSet.getter(function(name) {
var lookup;
- if (name !== 'ident___' && (lookup = this.col_lookup(name))) {
+ if (name !== weakMapMagicName && (lookup = this.col_lookup(name))) {
return this.col_evaluate(lookup);
} else {
return ExpandoProxyHandler.prototype.get.unpermuted.call(this,
name);
@@ -784,7 +830,7 @@
};
CollectionProxyHandler.prototype['delete'] = function(name) {
var lookup;
- if (name === 'ident___') {
+ if (name === weakMapMagicName) {
return false;
} else if ((lookup = this.col_lookup(name))) {
return false;
=======================================
--- /trunk/src/com/google/caja/ses/WeakMap.js Wed Mar 6 20:18:30 2013
+++ /trunk/src/com/google/caja/ses/WeakMap.js Mon Apr 15 14:31:05 2013
@@ -23,7 +23,7 @@
* quite conform, run <code>repairES5.js</code> first.
*
* @author Mark S. Miller
- * @requires ses, crypto, ArrayBuffer, Uint8Array
+ * @requires ses, crypto, ArrayBuffer, Uint8Array, document
* @overrides WeakMap, WeakMapModule
*/
@@ -81,6 +81,9 @@
* >secureable ES5</a> platform and the ES-Harmony {@code WeakMap} is
* absent, install an approximate emulation.
*
+ * <p>If WeakMap is present but cannot store some objects, use our
approximate
+ * emulation as a wrapper.
+ *
* <p>If this is almost a secureable ES5 platform, then WeakMap.js
* should be run after repairES5.js.
*
@@ -95,9 +98,23 @@
return;
}
- if (typeof WeakMap === 'function') {
- // assumed fine, so we're done.
- return;
+ // Check if there is already a good-enough WeakMap implementation, and
if so
+ // exit without replacing it.
+ var HostWeakMap = WeakMap;
+ if (typeof HostWeakMap === 'function') {
+ // There is a WeakMap -- is it good enough?
+ if (typeof document !== 'undefined') {
+ var problematic = document.createEvent('HTMLEvents');
+ var testHostMap = new HostWeakMap();
+ try {
+ testHostMap.set(problematic, 1); // Firefox 20 will throw here
+ if (testHostMap.get(problematic) === 1) {
+ return;
+ }
+ } catch (e) {}
+ } else {
+ return;
+ }
}
var hop = Object.prototype.hasOwnProperty;
@@ -305,7 +322,7 @@
// constant time representation is easy.
// var histogram = [];
- WeakMap = function() {
+ var OurWeakMap = function() {
// We are currently (12/25/2012) never encountering any prematurely
// non-extensible keys.
var keys = []; // brute force for prematurely non-extensible keys.
@@ -378,14 +395,14 @@
return true;
}
- return Object.create(WeakMap.prototype, {
+ return Object.create(OurWeakMap.prototype, {
get___: { value: constFunc(get___) },
has___: { value: constFunc(has___) },
set___: { value: constFunc(set___) },
delete___: { value: constFunc(delete___) }
});
};
- WeakMap.prototype = Object.create(Object.prototype, {
+ OurWeakMap.prototype = Object.create(Object.prototype, {
get: {
/**
* Return the value most recently associated with key, or
@@ -442,4 +459,49 @@
}
});
+ if (typeof HostWeakMap === 'function') {
+ (function() {
+ // If we got here, then the platform has a WeakMap but it refuses to
store
+ // some key types. Therefore, make a map implementation which makes
use of
+ // both as possible.
+
+ function DoubleWeakMap() {
+ var hmap = new HostWeakMap();
+ var omap = new OurWeakMap();
+
+ function dget(key, opt_default) {
+ return hmap.has(key) ? hmap.get(key) : omap.get___(key,
opt_default);
+ }
+
+ function dhas(key) {
+ return hmap.has(key) || omap.has___(key);
+ }
+
+ function dset(key, value) {
+ try {
+ hmap.set(key, value);
+ } catch (e) {
+ omap.set___(key, value);
+ }
+ }
+
+ function ddelete(key) {
+ hmap['delete'](key);
+ omap.delete___(key);
+ }
+
+ return Object.create(OurWeakMap.prototype, {
+ get___: { value: constFunc(dget) },
+ has___: { value: constFunc(dhas) },
+ set___: { value: constFunc(dset) },
+ delete___: { value: constFunc(ddelete) }
+ });
+ }
+ DoubleWeakMap.prototype = OurWeakMap.prototype;
+ WeakMap = DoubleWeakMap;
+ WeakMap.prototype.constructor = WeakMap; // hides OurWeakMap
+ })();
+ } else {
+ WeakMap = OurWeakMap;
+ }
})();
=======================================
--- /trunk/tests/com/google/caja/plugin/es53-test-language-guest.html Wed
Mar 20 11:26:38 2013
+++ /trunk/tests/com/google/caja/plugin/es53-test-language-guest.html Mon
Apr 15 14:31:05 2013
@@ -752,3 +752,90 @@
test(env);
});
</script>
+
+<!-- WeakMap tests:
+
+We have several different ways of implementing WeakMap (whitelisted browser
+provided, emulated ES5/3, emulated SES, 'double' maps for working around
+problems with browser provided WeakMap), so we have these end-to-end tests.
+
+-->
+
+<script type="text/javascript">
+ jsunitRegister('testWeakMapType', function testWeakMapType() {
+ assertEquals('function', typeof WeakMap);
+
+ var w = new WeakMap();
+
+ // TODO(kpreid): arrange so that w.constructor === WeakMap for all
impls
+ // Right now we're just testing that it's not a _wrong_ constructor.
+ assertTrue('constructor', w.constructor === WeakMap ||
+ w.constructor === Object || w.constructor === undefined);
+
+ if (w.constructor === WeakMap) {
+ assertTrue('prototype', WeakMap.prototype ===
Object.getPrototypeOf(w));
+ }
+
+ pass('testWeakMapType');
+ });
+</script>
+
+<script type="text/javascript">
+ jsunitRegister('testWeakMapAccess', function testWeakMapAccess() {
+ var w = new WeakMap();
+
+ var k1 = {};
+ var k2 = Object.create(k1);
+ var k3 = document.createEvent('HTMLEvents'); // problematic in Firefox
+
+ w.set(k1, 'a');
+ assertEquals('a', w.get(k1));
+ assertTrue('a', w.has(k1));
+ assertEquals(undefined, w.get(k2)); // don't inherit membership
+ assertFalse('early has', w.has(k2));
+
+ w.set(k2, 'b');
+ w.set(k3, 'c');
+ assertEquals('b', w.get(k2));
+ assertEquals('c', w.get(k3));
+ assertTrue('b', w.has(k2));
+ assertTrue('c', w.has(k3));
+
+ // failed lookups
+ assertFalse('no obj', w.has({}));
+ assertFalse('no event', w.has(document.createEvent('HTMLEvents')));
+ assertEquals(undefined, w.get({}));
+ assertEquals(undefined, w.get(document.createEvent('HTMLEvents')));
+
+ // defaulted failed lookups
+ // TODO(kpreid): Not supported by Chrome native WeakMap.
+ if (false) {
+ assertEquals('oops', w.get({}, 'oops'));
+ }
+
+ // primitives should not be silently accepted
+ expectFailure(function() {
+ w.set(1, 'nope');
+ }, 'WeakMap on primitive', function(e) {
+ return e.name === 'TypeError';
+ });
+
+ // deletion
+ // TODO(kpreid): not all of our implementations support .delete
or .clear;
+ // fix that and enable these tests
+ if (false) {
+ w.delete(k2);
+ w.delete(k3);
+ assertFalse('deleted k2', w.has(k2));
+ assertFalse('deleted k3', w.has(k3));
+ assertEquals('deleted k2', undefined, w.get(k2));
+ assertEquals('deleted k3', undefined, w.get(k3));
+
+ w.clear();
+ assertFalse('deleted k1', w.has(k1));
+ assertEquals('deleted k1', undefined, w.get(k1));
+ }
+
+ pass('testWeakMapAccess');
+ });
+</script>
--
---
You received this message because you are subscribed to the Google Groups "Google Caja Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/groups/opt_out.