Bug 1312690: Lazily initialize schema bindings. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Tue, 01 Nov 2016 17:11:32 -0700
changeset 432418 401d4c882f96ab701991593fa98239cde2e02ca5
parent 432417 faa4f385d262554c3962e597e1dbd82b739a66a4
child 432419 7c17754418b38059c0f366c27b3b68001581db85
child 432813 95afa0fbed8c8dd1d9f40a3123392be2a60a872e
push id34306
push usermaglione.k@gmail.com
push dateWed, 02 Nov 2016 00:14:25 +0000
reviewersaswan
bugs1312690
milestone52.0a1
Bug 1312690: Lazily initialize schema bindings. r?aswan MozReview-Commit-ID: GoVUlANCgHg
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/Schemas.jsm
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -2185,16 +2185,17 @@ this.ExtensionUtils = {
   promiseEvent,
   promiseObserved,
   runSafe,
   runSafeSync,
   runSafeSyncWithoutClone,
   runSafeWithoutClone,
   stylesheetMap,
   BaseContext,
+  DefaultMap,
   DefaultWeakMap,
   EventEmitter,
   EventManager,
   IconDetails,
   LocalAPIImplementation,
   LocaleData,
   Messenger,
   Port,
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -14,19 +14,30 @@ const global = this;
 Cu.importGlobalProperties(["URL"]);
 
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
+  DefaultMap,
   instanceOf,
 } = ExtensionUtils;
 
+class DeepMap extends DefaultMap {
+  constructor() {
+    super(() => new DeepMap());
+  }
+
+  getPath(...keys) {
+    return keys.reduce((map, prop) => map.get(prop), this);
+  }
+}
+
 XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService",
                                    "@mozilla.org/addons/content-policy;1",
                                    "nsIAddonContentPolicy");
 
 this.EXPORTED_SYMBOLS = ["Schemas"];
 
 /* globals Schemas, URL */
 
@@ -52,16 +63,68 @@ function readJSON(url) {
         resolve(JSON.parse(text));
       } catch (e) {
         reject(e);
       }
     });
   });
 }
 
+/**
+ * Defines a lazy getter for the given property on the given object. Any
+ * security wrappers are waived on the object before the property is
+ * defined, and the getter and setter methods are wrapped for the target
+ * scope.
+ *
+ * The given getter function is guaranteed to be called only once, even
+ * if the target scope retrieves the wrapped getter from the property
+ * descriptor and calls it directly.
+ *
+ * @param {object} object
+ *        The object on which to define the getter.
+ * @param {string|Symbol} prop
+ *        The property name for which to define the getter.
+ * @param {function} getter
+ *        The function to call in order to generate the final property
+ *        value.
+ */
+function exportLazyGetter(object, prop, getter) {
+  object = Cu.waiveXrays(object);
+
+  let redefine = value => {
+    if (value === undefined) {
+      delete object[prop];
+    } else {
+      Object.defineProperty(object, prop, {
+        enumerable: true,
+        configurable: true,
+        writable: true,
+        value,
+      });
+    }
+
+    getter = null;
+
+    return value;
+  };
+
+  Object.defineProperty(object, prop, {
+    enumerable: true,
+    configurable: true,
+
+    get: Cu.exportFunction(function() {
+      return redefine(getter.call(this));
+    }, object),
+
+    set: Cu.exportFunction(value => {
+      redefine(value);
+    }, object),
+  });
+}
+
 // Parses a regular expression, with support for the Python extended
 // syntax that allows setting flags by including the string (?im)
 function parsePattern(pattern) {
   let flags = "";
   let match = /^\(\?([im]*)\)(.*)/.exec(pattern);
   if (match) {
     [, flags, pattern] = match;
   }
@@ -869,21 +932,23 @@ class StringType extends Type {
   }
 
   checkBaseType(baseType) {
     return baseType == "string";
   }
 
   inject(apiImpl, path, name, dest, context) {
     if (this.enumeration) {
-      let obj = Cu.createObjectIn(dest, {defineAs: name});
-      for (let e of this.enumeration) {
-        let key = e.toUpperCase();
-        obj[key] = e;
-      }
+      exportLazyGetter(dest, name, () => {
+        let obj = Cu.createObjectIn(dest);
+        for (let e of this.enumeration) {
+          obj[e.toUpperCase()] = e;
+        }
+        return obj;
+      });
     }
   }
 }
 
 let SubModuleType;
 class ObjectType extends Type {
   static get EXTRA_PROPERTIES() {
     return ["properties", "patternProperties", ...super.EXTRA_PROPERTIES];
@@ -1418,41 +1483,45 @@ class SubModuleProperty extends Entry {
     super(schema);
     this.name = name;
     this.namespaceName = namespaceName;
     this.reference = reference;
     this.properties = properties;
   }
 
   inject(apiImpl, path, name, dest, context) {
-    let obj = Cu.createObjectIn(dest, {defineAs: name});
+    exportLazyGetter(dest, name, () => {
+      let obj = Cu.createObjectIn(dest);
 
-    let ns = Schemas.namespaces.get(this.namespaceName);
-    let type = ns.get(this.reference);
-    if (!type && this.reference.includes(".")) {
-      let [namespaceName, ref] = this.reference.split(".");
-      ns = Schemas.namespaces.get(namespaceName);
-      type = ns.get(ref);
-    }
-    if (!type || !(type instanceof SubModuleType)) {
-      throw new Error(`Internal error: ${this.namespaceName}.${this.reference} is not a sub-module`);
-    }
+      let ns = Schemas.namespaces.get(this.namespaceName);
+      let type = ns.get(this.reference);
+      if (!type && this.reference.includes(".")) {
+        let [namespaceName, ref] = this.reference.split(".");
+        ns = Schemas.namespaces.get(namespaceName);
+        type = ns.get(ref);
+      }
+      if (!type || !(type instanceof SubModuleType)) {
+        throw new Error(`Internal error: ${this.namespaceName}.${this.reference} is not a sub-module`);
+      }
 
-    let functions = type.functions;
-    for (let fun of functions) {
-      let subpath = path.concat(name);
-      let namespace = subpath.join(".");
-      let allowedContexts = fun.allowedContexts.length ? fun.allowedContexts : ns.defaultContexts;
-      if (context.shouldInject(namespace, fun.name, allowedContexts)) {
-        let apiImpl = context.getImplementation(namespace, fun.name);
-        fun.inject(apiImpl, subpath, fun.name, obj, context);
+      let functions = type.functions;
+      for (let fun of functions) {
+        let subpath = path.concat(name);
+        let namespace = subpath.join(".");
+        let allowedContexts = fun.allowedContexts.length ? fun.allowedContexts : ns.defaultContexts;
+        if (context.shouldInject(namespace, fun.name, allowedContexts)) {
+          let apiImpl = context.getImplementation(namespace, fun.name);
+          fun.inject(apiImpl, subpath, fun.name, obj, context);
+        }
       }
-    }
+
+      // TODO: Inject this.properties.
 
-    // TODO: Inject this.properties.
+      return obj;
+    });
   }
 }
 
 // This class is a base class for FunctionEntrys and Events. It takes
 // care of validating parameter lists (i.e., handling of optional
 // parameters and parameter type checking).
 class CallEntry extends Entry {
   constructor(schema, path, name, parameters, allowAmbiguousOptionalArguments) {
@@ -1557,47 +1626,49 @@ class FunctionEntry extends CallEntry {
     if (this.unsupported) {
       return;
     }
 
     if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
       return;
     }
 
-    let stub;
-    if (this.isAsync) {
-      stub = (...args) => {
-        this.checkDeprecated(context);
-        let actuals = this.checkParameters(args, context);
-        let callback = null;
-        if (this.hasAsyncCallback) {
-          callback = actuals.pop();
-        }
-        if (callback === null && context.isChromeCompat) {
-          // We pass an empty stub function as a default callback for
-          // the `chrome` API, so promise objects are not returned,
-          // and lastError values are reported immediately.
-          callback = () => {};
-        }
-        return apiImpl.callAsyncFunction(actuals, callback);
-      };
-    } else if (!this.returns) {
-      stub = (...args) => {
-        this.checkDeprecated(context);
-        let actuals = this.checkParameters(args, context);
-        return apiImpl.callFunctionNoReturn(actuals);
-      };
-    } else {
-      stub = (...args) => {
-        this.checkDeprecated(context);
-        let actuals = this.checkParameters(args, context);
-        return apiImpl.callFunction(actuals);
-      };
-    }
-    Cu.exportFunction(stub, dest, {defineAs: name});
+    exportLazyGetter(dest, name, () => {
+      let stub;
+      if (this.isAsync) {
+        stub = (...args) => {
+          this.checkDeprecated(context);
+          let actuals = this.checkParameters(args, context);
+          let callback = null;
+          if (this.hasAsyncCallback) {
+            callback = actuals.pop();
+          }
+          if (callback === null && context.isChromeCompat) {
+            // We pass an empty stub function as a default callback for
+            // the `chrome` API, so promise objects are not returned,
+            // and lastError values are reported immediately.
+            callback = () => {};
+          }
+          return apiImpl.callAsyncFunction(actuals, callback);
+        };
+      } else if (!this.returns) {
+        stub = (...args) => {
+          this.checkDeprecated(context);
+          let actuals = this.checkParameters(args, context);
+          return apiImpl.callFunctionNoReturn(actuals);
+        };
+      } else {
+        stub = (...args) => {
+          this.checkDeprecated(context);
+          let actuals = this.checkParameters(args, context);
+          return apiImpl.callFunction(actuals);
+        };
+      }
+      return Cu.exportFunction(stub, dest);
+    });
   }
 }
 
 // Represents an "event" defined in a schema namespace.
 class Event extends CallEntry {
   constructor(schema, path, name, type, extraParameters, unsupported, permissions) {
     super(schema, path, name, extraParameters);
     this.type = type;
@@ -1617,36 +1688,41 @@ class Event extends CallEntry {
     if (this.unsupported) {
       return;
     }
 
     if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
       return;
     }
 
-    let addStub = (listener, ...args) => {
-      listener = this.checkListener(listener, context);
-      let actuals = this.checkParameters(args, context);
-      apiImpl.addListener(listener, actuals);
-    };
+    exportLazyGetter(dest, name, () => {
+      let addStub = (listener, ...args) => {
+        listener = this.checkListener(listener, context);
+        let actuals = this.checkParameters(args, context);
+        apiImpl.addListener(listener, actuals);
+      };
+
+      let removeStub = (listener) => {
+        listener = this.checkListener(listener, context);
+        apiImpl.removeListener(listener);
+      };
 
-    let removeStub = (listener) => {
-      listener = this.checkListener(listener, context);
-      apiImpl.removeListener(listener);
-    };
+      let hasStub = (listener) => {
+        listener = this.checkListener(listener, context);
+        return apiImpl.hasListener(listener);
+      };
+
+      let obj = Cu.createObjectIn(dest);
 
-    let hasStub = (listener) => {
-      listener = this.checkListener(listener, context);
-      return apiImpl.hasListener(listener);
-    };
+      Cu.exportFunction(addStub, obj, {defineAs: "addListener"});
+      Cu.exportFunction(removeStub, obj, {defineAs: "removeListener"});
+      Cu.exportFunction(hasStub, obj, {defineAs: "hasListener"});
 
-    let obj = Cu.createObjectIn(dest, {defineAs: name});
-    Cu.exportFunction(addStub, obj, {defineAs: "addListener"});
-    Cu.exportFunction(removeStub, obj, {defineAs: "removeListener"});
-    Cu.exportFunction(hasStub, obj, {defineAs: "hasListener"});
+      return obj;
+    });
   }
 }
 
 const TYPES = Object.freeze(Object.assign(Object.create(null), {
   any: AnyType,
   array: ArrayType,
   boolean: BooleanType,
   function: FunctionType,
@@ -1666,16 +1742,17 @@ this.Schemas = {
   // Map[<schema-name> -> Map[<symbol-name> -> Entry]]
   // This keeps track of all the schemas that have been loaded so far.
   namespaces: new Map(),
 
   register(namespaceName, symbol, value) {
     let ns = this.namespaces.get(namespaceName);
     if (!ns) {
       ns = new Map();
+      ns.name = namespaceName;
       ns.permissions = null;
       ns.allowedContexts = [];
       ns.defaultContexts = [];
       this.namespaces.set(namespaceName, ns);
     }
     ns.set(symbol, value);
   },
 
@@ -1916,74 +1993,89 @@ this.Schemas = {
   checkPermissions(namespace, wrapperFuncs) {
     let ns = this.namespaces.get(namespace);
     if (ns && ns.permissions) {
       return ns.permissions.some(perm => wrapperFuncs.hasPermission(perm));
     }
     return true;
   },
 
+  exportLazyGetter,
+
   /**
    * Inject registered extension APIs into `dest`.
    *
    * @param {object} dest The root namespace for the APIs.
    *     This object is usually exposed to extensions as "chrome" or "browser".
    * @param {object} wrapperFuncs An implementation of the InjectionContext
    *     interface, which runs the actual functionality of the generated API.
    */
   inject(dest, wrapperFuncs) {
     let context = new InjectionContext(wrapperFuncs);
 
-    for (let [namespace, ns] of this.namespaces) {
-      if (ns.permissions && !ns.permissions.some(perm => context.hasPermission(perm))) {
-        continue;
-      }
+    let createNamespace = ns => {
+      let obj = Cu.createObjectIn(dest);
 
-      if (!wrapperFuncs.shouldInject(namespace, null, ns.allowedContexts)) {
-        continue;
-      }
+      for (let [name, entry] of ns) {
+        let allowedContexts = entry.allowedContexts;
+        if (!allowedContexts.length) {
+          allowedContexts = ns.defaultContexts;
+        }
 
-      let obj = Cu.createObjectIn(dest, {defineAs: namespace});
-      for (let [name, entry] of ns) {
-        let allowedContexts = entry.allowedContexts.length ? entry.allowedContexts : ns.defaultContexts;
-        if (context.shouldInject(namespace, name, allowedContexts)) {
-          let apiImpl = context.getImplementation(namespace, name);
-          entry.inject(apiImpl, [namespace], name, obj, context);
+        if (context.shouldInject(ns.name, name, allowedContexts)) {
+          let apiImpl = context.getImplementation(ns.name, name);
+          entry.inject(apiImpl, [ns.name], name, obj, context);
         }
       }
 
       // Remove the namespace object if it is empty
-      if (!Object.keys(obj).length) {
-        delete dest[namespace];
-        // process the next namespace.
+      if (Object.keys(obj).length) {
+        return obj;
+      }
+    };
+
+    let createNestedNamespaces = (parent, namespaces) => {
+      for (let [prop, namespace] of namespaces) {
+        if (namespace instanceof DeepMap) {
+          exportLazyGetter(parent, prop, () => {
+            let obj = Cu.createObjectIn(parent);
+            createNestedNamespaces(obj, namespace);
+            return obj;
+          });
+        } else {
+          exportLazyGetter(parent, prop,
+                           () => createNamespace(namespace));
+        }
+      }
+    };
+
+    let nestedNamespaces = new DeepMap();
+    for (let ns of this.namespaces.values()) {
+      if (ns.permissions && !ns.permissions.some(perm => context.hasPermission(perm))) {
         continue;
       }
 
-      // If the nested namespaced API object (e.g devtools.inspectedWindow) is not empty,
-      // then turn `dest["nested.namespace"]` into `dest["nested"]["namespace"]`.
-      if (namespace.includes(".")) {
-        let apiObj = dest[namespace];
-        delete dest[namespace];
+      if (!wrapperFuncs.shouldInject(ns.name, null, ns.allowedContexts)) {
+        continue;
+      }
 
-        let nsLevels = namespace.split(".");
-        let currentObj = dest;
-        for (let nsLevel of nsLevels.slice(0, -1)) {
-          if (!currentObj[nsLevel]) {
-            // Create the namespace level if it doesn't exist yet.
-            currentObj = Cu.createObjectIn(currentObj, {defineAs: nsLevel});
-          } else {
-            // Move currentObj to the nested object if it already exists.
-            currentObj = currentObj[nsLevel];
-          }
-        }
+      if (ns.name.includes(".")) {
+        let path = ns.name.split(".");
+        let leafName = path.pop();
 
-        // Copy the apiObj as the final nested level.
-        currentObj[nsLevels.pop()] = apiObj;
+        let parent = nestedNamespaces.getPath(...path);
+
+        parent.set(leafName, ns);
+      } else {
+        exportLazyGetter(dest, ns.name,
+                         () => createNamespace(ns));
       }
     }
+
+    createNestedNamespaces(dest, nestedNamespaces);
   },
 
   /**
    * Normalize `obj` according to the loaded schema for `typeName`.
    *
    * @param {object} obj The object to normalize against the schema.
    * @param {string} typeName The name in the format namespace.propertyname
    * @param {object} context An implementation of Context. Any validation errors