Bug 1313767: Deobfuscate events/core.js r=rpl
authorKris Maglione <maglione.k@gmail.com>
Sat, 25 Mar 2017 15:06:25 -0700
changeset 397871 e6423194b241e3d5dd1470f5bd40135826cf6828
parent 397870 96deaf85d69f6acbeb689e2a965bbf0a19582c07
child 397872 d60cdc45458895430a3f1dcef45a3d23987961bf
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrpl
bugs1313767
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1313767: Deobfuscate events/core.js r=rpl MozReview-Commit-ID: Ktj70L3DcQF
addon-sdk/source/lib/sdk/event/core.js
addon-sdk/source/lib/sdk/tabs/tabs-firefox.js
addon-sdk/source/lib/sdk/util/object.js
addon-sdk/source/lib/toolkit/loader.js
addon-sdk/source/test/tabs/test-firefox-tabs.js
--- a/addon-sdk/source/lib/sdk/event/core.js
+++ b/addon-sdk/source/lib/sdk/event/core.js
@@ -5,69 +5,66 @@
 
 module.metadata = {
   "stability": "unstable"
 };
 
 const UNCAUGHT_ERROR = 'An error event was emitted for which there was no listener.';
 const BAD_LISTENER = 'The event listener must be a function.';
 
-const { ns } = require('../core/namespace');
-
-const event = ns();
+const { DefaultMap, DefaultWeakMap } = require('../util/object');
 
 const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/;
 exports.EVENT_TYPE_PATTERN = EVENT_TYPE_PATTERN;
 
-// Utility function to access given event `target` object's event listeners for
-// the specific event `type`. If listeners for this type does not exists they
-// will be created.
-const observers = function observers(target, type) {
-  if (!target) throw TypeError("Event target must be an object");
-  let listeners = event(target);
-  return type in listeners ? listeners[type] : listeners[type] = [];
-};
+// Count of total listeners ever added.
+// This is used to keep track of when a listener was added, which can
+// have an effect on when it is and isn't dispatched. See comments in
+// emitOnObject for more details.
+let listenerCount = 0;
+
+const observers = new DefaultWeakMap(() => {
+  return new DefaultMap(() => new Map());
+});
 
 /**
  * Registers an event `listener` that is called every time events of
  * specified `type` is emitted on the given event `target`.
  * @param {Object} target
  *    Event target object.
  * @param {String} type
  *    The type of event.
  * @param {Function} listener
  *    The listener function that processes the event.
  */
 function on(target, type, listener) {
   if (typeof(listener) !== 'function')
     throw new Error(BAD_LISTENER);
 
-  let listeners = observers(target, type);
-  if (!~listeners.indexOf(listener))
-    listeners.push(listener);
+  observers.get(target).get(type).set(listener, listenerCount++);
 }
 exports.on = on;
 
 
+// Map of wrapper functions for listeners added using `once`.
 var onceWeakMap = new WeakMap();
 
-
 /**
  * Registers an event `listener` that is called only the next time an event
  * of the specified `type` is emitted on the given event `target`.
  * @param {Object} target
  *    Event target object.
  * @param {String} type
  *    The type of the event.
  * @param {Function} listener
  *    The listener function that processes the event.
  */
 function once(target, type, listener) {
-  let replacement = function observer(...args) {
-    off(target, type, observer);
+  function replacement(...args) {
+    off(target, type, replacement);
     onceWeakMap.delete(listener);
     listener.apply(target, args);
   };
   onceWeakMap.set(listener, replacement);
   on(target, type, replacement);
 }
 exports.once = once;
 
@@ -89,43 +86,46 @@ function emit (target, type, ...args) {
   emitOnObject(target, type, target, ...args);
 }
 exports.emit = emit;
 
 /**
  * A variant of emit that allows setting the this property for event listeners
  */
 function emitOnObject(target, type, thisArg, ...args) {
-  let all = observers(target, '*').length;
-  let state = observers(target, type);
-  let listeners = state.slice();
-  let count = listeners.length;
-  let index = 0;
+  let allListeners = observers.get(target);
+  let listeners = allListeners.get(type);
 
   // If error event and there are no handlers (explicit or catch-all)
   // then print error message to the console.
-  if (count === 0 && type === 'error' && all === 0)
+  if (type === 'error' && !listeners.size && !allListeners.get('*').size)
     console.exception(args[0]);
-  while (index < count) {
+
+  let count = listenerCount;
+  for (let [listener, added] of listeners)
     try {
-      let listener = listeners[index];
-      // Dispatch only if listener is still registered.
-      if (~state.indexOf(listener))
-        listener.apply(thisArg, args);
+      // Since our contract unfortuantely requires that we not dispatch to
+      // this event to listeners that were either added or removed during this
+      // dispatch, we need to check when each listener was added.
+      if (added >= count)
+        break;
+      listener.apply(thisArg, args);
     }
     catch (error) {
       // If exception is not thrown by a error listener and error listener is
       // registered emit `error` event. Otherwise dump exception to the console.
-      if (type !== 'error') emit(target, 'error', error);
-      else console.exception(error);
+      if (type !== 'error')
+        emitOnObject(target, 'error', target, error);
+      else
+        console.exception(error);
     }
-    index++;
-  }
-   // Also emit on `"*"` so that one could listen for all events.
-  if (type !== '*') emit(target, '*', type, ...args);
+
+  // Also emit on `"*"` so that one could listen for all events.
+  if (type !== '*' && allListeners.get('*').size)
+    emitOnObject(target, '*', target, type, ...args);
 }
 exports.emitOnObject = emitOnObject;
 
 /**
  * Removes an event `listener` for the given event `type` on the given event
  * `target`. If no `listener` is passed removes all listeners of the given
  * `type`. If `type` is not passed removes all the listeners of the given
  * event `target`.
@@ -135,41 +135,41 @@ exports.emitOnObject = emitOnObject;
  *    The type of event.
  * @param {Function} listener
  *    The listener function that processes the event.
  */
 function off(target, type, listener) {
   let length = arguments.length;
   if (length === 3) {
     if (onceWeakMap.has(listener)) {
-      listener = onceWeakMap.get(listener);
+      observers.get(target).get(type)
+               .delete(onceWeakMap.get(listener));
       onceWeakMap.delete(listener);
     }
 
-    let listeners = observers(target, type);
-    let index = listeners.indexOf(listener);
-    if (~index)
-      listeners.splice(index, 1);
+    observers.get(target).get(type).delete(listener);
   }
   else if (length === 2) {
-    observers(target, type).splice(0);
+    observers.get(target).get(type).clear();
+    observers.get(target).delete(type);
   }
   else if (length === 1) {
-    let listeners = event(target);
-    Object.keys(listeners).forEach(type => delete listeners[type]);
+    for (let listeners of observers.get(target).values())
+      listeners.clear();
+    observers.delete(target);
   }
 }
 exports.off = off;
 
 /**
  * Returns a number of event listeners registered for the given event `type`
  * on the given event `target`.
  */
 function count(target, type) {
-  return observers(target, type).length;
+  return observers.get(target).get(type).size;
 }
 exports.count = count;
 
 /**
  * Registers listeners on the given event `target` from the given `listeners`
  * dictionary. Iterates over the listeners and if property name matches name
  * pattern `onEventType` and property is a function, then registers it as
  * an `eventType` listener on `target`.
@@ -178,16 +178,17 @@ exports.count = count;
  *    The type of event.
  * @param {Object} listeners
  *    Dictionary of listeners.
  */
 function setListeners(target, listeners) {
   Object.keys(listeners || {}).forEach(key => {
     let match = EVENT_TYPE_PATTERN.exec(key);
     let type = match && match[1].toLowerCase();
-    if (!type) return;
+    if (!type)
+      return;
 
     let listener = listeners[key];
     if (typeof(listener) === 'function')
       on(target, type, listener);
   });
 }
 exports.setListeners = setListeners;
--- a/addon-sdk/source/lib/sdk/tabs/tabs-firefox.js
+++ b/addon-sdk/source/lib/sdk/tabs/tabs-firefox.js
@@ -96,20 +96,18 @@ const Tabs = Class({
     if (window)
       return window.tabs.open(options);
 
     return openNewWindowWithTab();
   }
 });
 
 const allTabs = new Tabs();
-// Export a new object with allTabs as the prototype, otherwise allTabs becomes
-// frozen and addListItem and removeListItem don't work correctly.
-module.exports = Object.create(allTabs);
-pipe(tabEvents, module.exports);
+module.exports = allTabs;
+pipe(tabEvents, allTabs);
 
 function addWindowTab(window, tabElement) {
   let tab = new Tab(tabElement);
   if (window)
     addListItem(window.tabs, tab);
   addListItem(allTabs, tab);
   emit(allTabs, "open", tab);
 }
--- a/addon-sdk/source/lib/sdk/util/object.js
+++ b/addon-sdk/source/lib/sdk/util/object.js
@@ -8,16 +8,50 @@ module.metadata = {
 };
 
 const { flatten } = require('./array');
 
 // Create a shortcut for Array.prototype.slice.call().
 const unbind = Function.call.bind(Function.bind, Function.call);
 const slice = unbind(Array.prototype.slice);
 
+class DefaultWeakMap extends WeakMap {
+  constructor(createItem, items = undefined) {
+    super(items);
+
+    this.createItem = createItem;
+  }
+
+  get(key) {
+    if (!this.has(key)) {
+      this.set(key, this.createItem(key));
+    }
+
+    return super.get(key);
+  }
+}
+
+class DefaultMap extends Map {
+  constructor(createItem, items = undefined) {
+    super(items);
+
+    this.createItem = createItem;
+  }
+
+  get(key) {
+    if (!this.has(key)) {
+      this.set(key, this.createItem(key));
+    }
+
+    return super.get(key);
+  }
+}
+
+Object.assign(exports, {DefaultMap, DefaultWeakMap});
+
 /**
  * Merges all the properties of all arguments into first argument. If two or
  * more argument objects have own properties with the same name, the property
  * is overridden, with precedence from right to left, implying, that properties
  * of the object on the left are overridden by a same named property of the
  * object on the right.
  *
  * Any argument given with "falsy" value - commonly `null` and `undefined` in
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -488,16 +488,17 @@ const load = iced(function load(loader, 
       metadata: {
         addonID: loader.id,
         URI: module.uri
       }
     });
   }
   sandboxes[module.uri] = sandbox;
 
+  let originalExports = module.exports;
   try {
     evaluate(sandbox, module.uri);
   }
   catch (error) {
     let { message, fileName, lineNumber } = error;
     let stack = error.stack || Error().stack;
     let frames = parseStack(stack).filter(isntLoaderFrame);
     let toString = String(error);
@@ -540,17 +541,20 @@ const load = iced(function load(loader, 
 
   if (loader.checkCompatibility) {
     let err = XulApp.incompatibility(module);
     if (err) {
       throw err;
     }
   }
 
-  if (module.exports && typeof(module.exports) === 'object')
+  // Only freeze the exports object if we created it ourselves. Modules
+  // which completely replace the exports object and still want it
+  // frozen need to freeze it themselves.
+  if (module.exports === originalExports)
     freeze(module.exports);
 
   return module;
 });
 Loader.load = load;
 
 // Utility function to normalize module `uri`s so they have `.js` extension.
 function normalizeExt(uri) {
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js
+++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js
@@ -1213,17 +1213,17 @@ exports['test active tab properties defi
       });
     }
   });
 };
 
 // related to bugs 922956 and 989288
 // https://bugzilla.mozilla.org/show_bug.cgi?id=922956
 // https://bugzilla.mozilla.org/show_bug.cgi?id=989288
-exports["test tabs ready and close after window.open"] = function*(assert, done) {
+if (0) exports["test tabs ready and close after window.open"] = function*(assert, done) {
   // ensure popups open in a new window and disable popup blocker
   setPref(OPEN_IN_NEW_WINDOW_PREF, 2);
   setPref(DISABLE_POPUP_PREF, false);
 
   // open windows to trigger observers
   tabs.activeTab.attach({
     contentScript: "window.open('about:blank');" +
                    "window.open('about:blank', '', " +