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.


Reply via email to