Bug 1452827: Cleanup a bunch of duplication and cruft in XPIInstall.jsm. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Mon, 09 Apr 2018 17:21:13 -0700
changeset 779510 891b18febb34f5d6a97d93c977f55ff4560da519
parent 779135 04252d4db06a1d1d27407cf7c7726bb6f548ff49
push id105785
push usermaglione.k@gmail.com
push dateTue, 10 Apr 2018 00:22:43 +0000
reviewersaswan
bugs1452827
milestone61.0a1
Bug 1452827: Cleanup a bunch of duplication and cruft in XPIInstall.jsm. r?aswan MozReview-Commit-ID: 4kmYI2t471E
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/addons/webextension_2/install.rdf
toolkit/mozapps/extensions/test/addons/webextension_2/manifest.json
toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -3,22 +3,25 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = [
   "DownloadAddonInstall",
   "LocalAddonInstall",
   "UpdateChecker",
+  "XPIInstall",
   "loadManifestFromFile",
   "verifyBundleSignedState",
 ];
 
 /* globals DownloadAddonInstall, LocalAddonInstall */
 
+Cu.importGlobalProperties(["TextDecoder", "fetch"]);
+
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
 
 ChromeUtils.defineModuleGetter(this, "AddonRepository",
                                "resource://gre/modules/addons/AddonRepository.jsm");
 ChromeUtils.defineModuleGetter(this, "AddonSettings",
                                "resource://gre/modules/addons/AddonSettings.jsm");
@@ -41,23 +44,31 @@ ChromeUtils.defineModuleGetter(this, "OS
                                "resource://gre/modules/osfile.jsm");
 ChromeUtils.defineModuleGetter(this, "ZipUtils",
                                "resource://gre/modules/ZipUtils.jsm");
 
 const {nsIBlocklistService} = Ci;
 
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
                                        "initWithPath");
+const CryptoHash = Components.Constructor("@mozilla.org/security/hash;1",
+                                          "nsICryptoHash", "initWithString");
+const ZipReader = Components.Constructor("@mozilla.org/libjar/zip-reader;1",
+                                         "nsIZipReader", "open");
 
-XPCOMUtils.defineLazyServiceGetter(this, "gCertDB",
-                                   "@mozilla.org/security/x509certdb;1",
-                                   "nsIX509CertDB");
-XPCOMUtils.defineLazyServiceGetter(this, "gRDF", "@mozilla.org/rdf/rdf-service;1",
-                                   Ci.nsIRDFService);
+const RDFDataSource = Components.Constructor(
+  "@mozilla.org/rdf/datasource;1?name=in-memory-datasource", "nsIRDFDataSource");
+const parseRDFString = Components.Constructor(
+  "@mozilla.org/rdf/xml-parser;1", "nsIRDFXMLParser", "parseString");
 
+XPCOMUtils.defineLazyServiceGetters(this, {
+  aomStartup: ["@mozilla.org/addons/addon-manager-startup;1", "amIAddonManagerStartup"],
+  gCertDB: ["@mozilla.org/security/x509certdb;1", "nsIX509CertDB"],
+  gRDF: ["@mozilla.org/rdf/rdf-service;1", "nsIRDFService"],
+});
 
 ChromeUtils.defineModuleGetter(this, "XPIInternal",
                                "resource://gre/modules/addons/XPIProvider.jsm");
 ChromeUtils.defineModuleGetter(this, "XPIProvider",
                                "resource://gre/modules/addons/XPIProvider.jsm");
 
 const PREF_ALLOW_NON_RESTARTLESS      = "extensions.legacy.non-restartless.enabled";
 
@@ -115,17 +126,16 @@ function getFile(path, base = null) {
   file.appendRelativePath(path);
   return file;
 }
 
 const PREF_EM_UPDATE_BACKGROUND_URL   = "extensions.update.background.url";
 const PREF_EM_UPDATE_URL              = "extensions.update.url";
 const PREF_XPI_SIGNATURES_DEV_ROOT    = "xpinstall.signatures.dev-root";
 const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
-const FILE_RDF_MANIFEST               = "install.rdf";
 const FILE_WEB_MANIFEST               = "manifest.json";
 
 const KEY_TEMPDIR                     = "TmpD";
 
 const RDFURI_INSTALL_MANIFEST_ROOT    = "urn:mozilla:install-manifest";
 const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
 
 // Properties that exist in the install manifest
@@ -179,16 +189,192 @@ var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{
 
 ChromeUtils.import("resource://gre/modules/Log.jsm");
 const LOGGER_ID = "addons.xpi";
 
 // Create a new logger for use by all objects in this Addons XPI Provider module
 // (Requires AddonManager.jsm)
 var logger = Log.repository.getLogger(LOGGER_ID);
 
+function getJarURI(file, path = "") {
+  if (file instanceof Ci.nsIFile) {
+    file = Services.io.newFileURI(file);
+  }
+  if (file instanceof Ci.nsIURI) {
+    file = file.spec;
+  }
+  return Services.io.newURI(`jar:${file}!/${path}`);
+}
+
+let DirPackage;
+let XPIPackage;
+class Package {
+  constructor(file, rootURI) {
+    this.file = file;
+    this.filePath = file.path;
+    this.rootURI = rootURI;
+  }
+
+  static get(file) {
+    if (file.isFile()) {
+      return new XPIPackage(file);
+    }
+    return new DirPackage(file);
+  }
+
+  getURI(...path) {
+    return Services.io.newURI(path.join("/"), null, this.rootURI);
+  }
+
+  async getManifestFile() {
+    if (await this.hasResource("manifest.json")) {
+      return "manifest.json";
+    }
+    if (await this.hasResource("install.rdf")) {
+      return "install.rdf";
+    }
+    return null;
+  }
+
+  close() {}
+
+  async readString(...path) {
+    let buffer = await this.readBinary(...path);
+    return new TextDecoder().decode(buffer);
+  }
+
+  async verifySignedState(addon) {
+    if (!shouldVerifySignedState(addon)) {
+      return {
+        signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED,
+        cert: null
+      };
+    }
+
+    let root = Ci.nsIX509CertDB.AddonsPublicRoot;
+    if (!AppConstants.MOZ_REQUIRE_SIGNING && Services.prefs.getBoolPref(PREF_XPI_SIGNATURES_DEV_ROOT, false)) {
+      root = Ci.nsIX509CertDB.AddonsStageRoot;
+    }
+
+    return this.verifySignedStateForRoot(addon, root);
+  }
+}
+
+DirPackage = class DirPackage extends Package {
+  constructor(file) {
+    super(file, Services.io.newFileURI(file));
+  }
+
+  hasResource(...path) {
+    return OS.File.exists(OS.Path.join(this.filePath, ...path));
+  }
+
+  async iterDirectory(path, callback) {
+    let fullPath = OS.Path.join(this.filePath, ...path);
+
+    let iter = new OS.File.DirectoryIterator(fullPath);
+    await iter.forEach(callback);
+    iter.close();
+  }
+
+  iterFiles(callback, path = []) {
+    return this.iterDirectory(path, async entry => {
+      let entryPath = [...path, entry.name];
+      if (entry.isDir) {
+        callback({
+          path: entryPath.join("/"),
+          isDir: true,
+        });
+        await this.iterFiles(callback, entryPath);
+      } else {
+        let stat = await OS.File.stat(OS.Path.join(this.filePath, ...entryPath));
+        callback({
+          path: entryPath.join("/"),
+          isDir: false,
+          size: stat.size,
+        });
+      }
+    });
+  }
+
+  readBinary(...path) {
+    return OS.File.read(OS.Path.join(this.filePath, ...path));
+  }
+
+  verifySignedStateForRoot(addon, root) {
+    return new Promise(resolve => {
+      let callback = {
+        verifySignedDirectoryFinished(aRv, aCert) {
+          resolve({
+            signedState: getSignedStatus(aRv, aCert, addon.id),
+            cert: aCert,
+          });
+        }
+      };
+      // This allows the certificate DB to get the raw JS callback object so the
+      // test code can pass through objects that XPConnect would reject.
+      callback.wrappedJSObject = callback;
+
+      gCertDB.verifySignedDirectoryAsync(root, this.file, callback);
+    });
+  }
+};
+
+XPIPackage = class XPIPackage extends Package {
+  constructor(file) {
+    super(file, getJarURI(file));
+
+    this.zipReader = new ZipReader(file);
+  }
+
+  close() {
+    this.zipReader.close();
+    this.zipReader = null;
+  }
+
+  async hasResource(...path) {
+    return this.zipReader.hasEntry(path.join("/"));
+  }
+
+  async iterFiles(callback) {
+    for (let path of XPCOMUtils.IterStringEnumerator(this.zipReader.findEntries("*"))) {
+      let entry = this.zipReader.getEntry(path);
+      callback({
+        path,
+        isDir: entry.isDirectory,
+        size: entry.realSize,
+      });
+    }
+  }
+
+  async readBinary(...path) {
+    let response = await fetch(this.rootURI.resolve(path.join("/")));
+    return response.arrayBuffer();
+  }
+
+  verifySignedStateForRoot(addon, root) {
+    return new Promise(resolve => {
+      let callback = {
+        openSignedAppFileFinished(aRv, aZipReader, aCert) {
+          if (aZipReader)
+            aZipReader.close();
+          resolve({
+            signedState: getSignedStatus(aRv, aCert, addon.id),
+            cert: aCert
+          });
+        }
+      };
+      // This allows the certificate DB to get the raw JS callback object so the
+      // test code can pass through objects that XPConnect would reject.
+      callback.wrappedJSObject = callback;
+
+      gCertDB.openSignedAppFileAsync(root, this.file, callback);
+    });
+  }
+};
 
 /**
  * Sets permissions on a file
  *
  * @param  aFile
  *         The file or directory to operate on.
  * @param  aPermissions
  *         The permissions to set
@@ -201,34 +387,16 @@ function setFilePermissions(aFile, aPerm
          aFile.path, e);
   }
 }
 
 function EM_R(aProperty) {
   return gRDF.GetResource(PREFIX_NS_EM + aProperty);
 }
 
-function getManifestFileForDir(aDir) {
-  let file = getFile(FILE_RDF_MANIFEST, aDir);
-  if (file.exists() && file.isFile())
-    return file;
-  file.leafName = FILE_WEB_MANIFEST;
-  if (file.exists() && file.isFile())
-    return file;
-  return null;
-}
-
-function getManifestEntryForZipReader(aZipReader) {
-  if (aZipReader.hasEntry(FILE_RDF_MANIFEST))
-    return FILE_RDF_MANIFEST;
-  if (aZipReader.hasEntry(FILE_WEB_MANIFEST))
-    return FILE_WEB_MANIFEST;
-  return null;
-}
-
 /**
  * Converts an RDF literal, resource or integer into a string.
  *
  * @param  aLiteral
  *         The RDF object to convert
  * @return a string if the object could be converted or null
  */
 function getRDFValue(aLiteral) {
@@ -392,17 +560,17 @@ async function loadManifestFromWebManife
  * @param  aUri
  *         The URI that the manifest is being read from
  * @param  aStream
  *         An open stream to read the RDF from
  * @return an AddonInternal object
  * @throws if the install manifest in the RDF stream is corrupt or could not
  *         be read
  */
-async function loadManifestFromRDF(aUri, aStream) {
+async function loadManifestFromRDF(aUri, aData) {
   function getPropertyArray(aDs, aSource, aProperty) {
     let values = [];
     let targets = aDs.GetTargets(aSource, EM_R(aProperty), true);
     while (targets.hasMoreElements())
       values.push(getRDFValue(targets.getNext()));
 
     return values;
   }
@@ -459,43 +627,18 @@ async function loadManifestFromRDF(aUri,
                                    prop.substring(0, prop.length - 1));
       if (props.length > 0)
         locale[prop] = props;
     }
 
     return locale;
   }
 
-  let rdfParser = Cc["@mozilla.org/rdf/xml-parser;1"].
-                  createInstance(Ci.nsIRDFXMLParser);
-  let ds = Cc["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"].
-           createInstance(Ci.nsIRDFDataSource);
-  let listener = rdfParser.parseAsync(ds, aUri);
-  let channel = Cc["@mozilla.org/network/input-stream-channel;1"].
-                createInstance(Ci.nsIInputStreamChannel);
-  channel.setURI(aUri);
-  channel.contentStream = aStream;
-  channel.QueryInterface(Ci.nsIChannel);
-  channel.contentType = "text/xml";
-
-  listener.onStartRequest(channel, null);
-
-  try {
-    let pos = 0;
-    let count = aStream.available();
-    while (count > 0) {
-      listener.onDataAvailable(channel, null, aStream, pos, count);
-      pos += count;
-      count = aStream.available();
-    }
-    listener.onStopRequest(channel, null, Cr.NS_OK);
-  } catch (e) {
-    listener.onStopRequest(channel, null, e.result);
-    throw e;
-  }
+  let ds = new RDFDataSource();
+  parseRDFString(ds, aUri, aData);
 
   let root = gRDF.GetResource(RDFURI_INSTALL_MANIFEST_ROOT);
   let addon = new AddonInternal();
   for (let prop of PROP_METADATA) {
     addon[prop] = getRDFProperty(ds, root, prop);
   }
 
   if (!addon.type) {
@@ -685,239 +828,98 @@ function defineSyncGUID(aAddon) {
     },
     configurable: true,
     enumerable: true,
   });
 }
 
 // Generate a unique ID based on the path to this temporary add-on location.
 function generateTemporaryInstallID(aFile) {
-  const hasher = Cc["@mozilla.org/security/hash;1"]
-        .createInstance(Ci.nsICryptoHash);
-  hasher.init(hasher.SHA1);
+  const hasher = CryptoHash("sha1");
   const data = new TextEncoder().encode(aFile.path);
   // Make it so this ID cannot be guessed.
   const sess = TEMP_INSTALL_ID_GEN_SESSION;
   hasher.update(sess, sess.length);
   hasher.update(data, data.length);
   let id = `${getHashStringForCrypto(hasher)}${TEMPORARY_ADDON_SUFFIX}`;
   logger.info(`Generated temp id ${id} (${sess.join("")}) for ${aFile.path}`);
   return id;
 }
 
-/**
- * Loads an AddonInternal object from an add-on extracted in a directory.
- *
- * @param  aDir
- *         The nsIFile directory holding the add-on
- * @return an AddonInternal object
- * @throws if the directory does not contain a valid install manifest
- */
-var loadManifestFromDir = async function(aDir, aInstallLocation) {
-  function getFileSize(aFile) {
-    if (aFile.isSymlink())
-      return 0;
-
-    if (!aFile.isDirectory())
-      return aFile.fileSize;
+var loadManifest = async function(aPackage, aInstallLocation) {
+  async function loadFromRDF(aUri) {
+    let manifest = await aPackage.readString("install.rdf");
+    let addon = await loadManifestFromRDF(aUri, manifest);
 
-    let size = 0;
-    let entries = aFile.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
-    let entry;
-    while ((entry = entries.nextFile))
-      size += getFileSize(entry);
-    entries.close();
-    return size;
-  }
-
-  async function loadFromRDF(aUri) {
-    let fis = Cc["@mozilla.org/network/file-input-stream;1"].
-              createInstance(Ci.nsIFileInputStream);
-    fis.init(aUri.file, -1, -1, false);
-    let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
-              createInstance(Ci.nsIBufferedInputStream);
-    bis.init(fis, 4096);
-    try {
-      var addon = await loadManifestFromRDF(aUri, bis);
-    } finally {
-      bis.close();
-      fis.close();
-    }
-
-    let iconFile = getFile("icon.png", aDir);
-
-    if (iconFile.exists()) {
+    if (await aPackage.hasResource("icon.png")) {
       addon.icons[32] = "icon.png";
       addon.icons[48] = "icon.png";
     }
 
-    let icon64File = getFile("icon64.png", aDir);
-
-    if (icon64File.exists()) {
+    if (await aPackage.hasResource("icon64.png")) {
       addon.icons[64] = "icon64.png";
     }
+
     return addon;
   }
 
-  let file = getManifestFileForDir(aDir);
-  if (!file) {
-    throw new Error("Directory " + aDir.path + " does not contain a valid " +
+  let entry = await aPackage.getManifestFile();
+  if (!entry) {
+    throw new Error("File " + aPackage.filePath + " does not contain a valid " +
                     "install manifest");
   }
 
-  let uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+  let isWebExtension = entry == FILE_WEB_MANIFEST;
+  let addon = isWebExtension ?
+              await loadManifestFromWebManifest(aPackage.rootURI) :
+              await loadFromRDF(aPackage.getURI("install.rdf"));
+
+  addon._sourceBundle = aPackage.file;
+  addon._installLocation = aInstallLocation;
 
-  let addon;
-  if (file.leafName == FILE_WEB_MANIFEST) {
-    addon = await loadManifestFromWebManifest(uri);
-    if (!addon.id) {
-      if (aInstallLocation.name == KEY_APP_TEMPORARY) {
-        addon.id = generateTemporaryInstallID(aDir);
-      } else {
-        addon.id = aDir.leafName;
+  addon.size = 0;
+  await aPackage.iterFiles(entry => {
+    if (!entry.isDir) {
+      addon.size += entry.size;
+    }
+  });
+
+  let {signedState, cert} = await aPackage.verifySignedState(addon);
+  addon.signedState = signedState;
+
+  if (isWebExtension && !addon.id) {
+    if (cert) {
+      addon.id = cert.commonName;
+      if (!gIDTest.test(addon.id)) {
+        throw new Error(`Webextension is signed with an invalid id (${addon.id})`);
       }
     }
-  } else {
-    addon = await loadFromRDF(uri);
+    if (!addon.id && aInstallLocation.name == KEY_APP_TEMPORARY) {
+      addon.id = generateTemporaryInstallID(aPackage.file);
+    }
   }
 
-  addon._sourceBundle = aDir.clone();
-  addon._installLocation = aInstallLocation;
-  addon.size = getFileSize(aDir);
-  addon.signedState = await verifyDirSignedState(aDir, addon)
-    .then(({signedState}) => signedState);
   addon.updateBlocklistState();
   addon.appDisabled = !isUsableAddon(addon);
 
   defineSyncGUID(addon);
 
   return addon;
 };
 
-/**
- * Loads an AddonInternal object from an nsIZipReader for an add-on.
- *
- * @param  aZipReader
- *         An open nsIZipReader for the add-on's files
- * @return an AddonInternal object
- * @throws if the XPI file does not contain a valid install manifest
- */
-var loadManifestFromZipReader = async function(aZipReader, aInstallLocation) {
-  async function loadFromRDF(aUri) {
-    let zis = aZipReader.getInputStream(entry);
-    let bis = Cc["@mozilla.org/network/buffered-input-stream;1"].
-              createInstance(Ci.nsIBufferedInputStream);
-    bis.init(zis, 4096);
-    try {
-      var addon = await loadManifestFromRDF(aUri, bis);
-    } finally {
-      bis.close();
-      zis.close();
-    }
-
-    if (aZipReader.hasEntry("icon.png")) {
-      addon.icons[32] = "icon.png";
-      addon.icons[48] = "icon.png";
-    }
-
-    if (aZipReader.hasEntry("icon64.png")) {
-      addon.icons[64] = "icon64.png";
-    }
-
+var loadManifestFromFile = async function(aFile, aInstallLocation) {
+  let pkg = Package.get(aFile);
+  try {
+    let addon = await loadManifest(pkg, aInstallLocation);
     return addon;
-  }
-
-  let entry = getManifestEntryForZipReader(aZipReader);
-  if (!entry) {
-    throw new Error("File " + aZipReader.file.path + " does not contain a valid " +
-                    "install manifest");
-  }
-
-  let uri = buildJarURI(aZipReader.file, entry);
-
-  let isWebExtension = (entry == FILE_WEB_MANIFEST);
-
-  let addon = isWebExtension ?
-              await loadManifestFromWebManifest(uri) :
-              await loadFromRDF(uri);
-
-  addon._sourceBundle = aZipReader.file;
-  addon._installLocation = aInstallLocation;
-
-  addon.size = 0;
-  let entries = aZipReader.findEntries(null);
-  while (entries.hasMore())
-    addon.size += aZipReader.getEntry(entries.getNext()).realSize;
-
-  let {signedState, cert} = await verifyZipSignedState(aZipReader.file, addon);
-  addon.signedState = signedState;
-  if (isWebExtension && !addon.id) {
-    if (cert) {
-      addon.id = cert.commonName;
-      if (!gIDTest.test(addon.id)) {
-        throw new Error(`Webextension is signed with an invalid id (${addon.id})`);
-      }
-    }
-    if (!addon.id && aInstallLocation.name == KEY_APP_TEMPORARY) {
-      addon.id = generateTemporaryInstallID(aZipReader.file);
-    }
-  }
-  addon.updateBlocklistState();
-  addon.appDisabled = !isUsableAddon(addon);
-
-  defineSyncGUID(addon);
-
-  return addon;
-};
-
-/**
- * Loads an AddonInternal object from an add-on in an XPI file.
- *
- * @param  aXPIFile
- *         An nsIFile pointing to the add-on's XPI file
- * @return an AddonInternal object
- * @throws if the XPI file does not contain a valid install manifest
- */
-var loadManifestFromZipFile = async function(aXPIFile, aInstallLocation) {
-  let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
-                  createInstance(Ci.nsIZipReader);
-  try {
-    zipReader.open(aXPIFile);
-
-    // Can't return this promise because that will make us close the zip reader
-    // before it has finished loading the manifest. Wait for the result and then
-    // return.
-    let manifest = await loadManifestFromZipReader(zipReader, aInstallLocation);
-    return manifest;
   } finally {
-    zipReader.close();
+    pkg.close();
   }
 };
 
-var loadManifestFromFile = function(aFile, aInstallLocation) {
-  if (aFile.isFile())
-    return loadManifestFromZipFile(aFile, aInstallLocation);
-  return loadManifestFromDir(aFile, aInstallLocation);
-};
-
-/**
- * Creates a jar: URI for a file inside a ZIP file.
- *
- * @param  aJarfile
- *         The ZIP file as an nsIFile
- * @param  aPath
- *         The path inside the ZIP file
- * @return an nsIURI for the file
- */
-function buildJarURI(aJarfile, aPath) {
-  let uri = Services.io.newFileURI(aJarfile);
-  uri = "jar:" + uri.spec + "!/" + aPath;
-  return Services.io.newURI(uri);
-}
-
 /**
  * Sends local and remote notifications to flush a JAR file cache entry
  *
  * @param aJarFile
  *        The ZIP/XPI/JAR file as a nsIFile
  */
 function flushJarCache(aJarFile) {
   Services.obs.notifyObservers(aJarFile, "flush-cache-entry");
@@ -951,24 +953,19 @@ function getTemporaryFile() {
 
 /**
  * Returns the signedState for a given return code and certificate by verifying
  * it against the expected ID.
  */
 function getSignedStatus(aRv, aCert, aAddonID) {
   let expectedCommonName = aAddonID;
   if (aAddonID && aAddonID.length > 64) {
-    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
-                    createInstance(Ci.nsIScriptableUnicodeConverter);
-    converter.charset = "UTF-8";
-    let data = converter.convertToByteArray(aAddonID, {});
+    let data = new Uint8Array(new TextEncoder().encode(aAddonID));
 
-    let crypto = Cc["@mozilla.org/security/hash;1"].
-                 createInstance(Ci.nsICryptoHash);
-    crypto.init(Ci.nsICryptoHash.SHA256);
+    let crypto = CryptoHash("sha256");
     crypto.update(data, data.length);
     expectedCommonName = getHashStringForCrypto(crypto);
   }
 
   switch (aRv) {
     case Cr.NS_OK:
       if (expectedCommonName && expectedCommonName != aCert.commonName)
         return AddonManager.SIGNEDSTATE_BROKEN;
@@ -1010,111 +1007,33 @@ function shouldVerifySignedState(aAddon)
     return false;
 
   // Otherwise only check signatures if signing is enabled and the add-on is one
   // of the signed types.
   return AddonSettings.ADDON_SIGNING && SIGNED_TYPES.has(aAddon.type);
 }
 
 /**
- * Verifies that a zip file's contents are all correctly signed by an
- * AMO-issued certificate
- *
- * @param  aFile
- *         the xpi file to check
- * @param  aAddon
- *         the add-on object to verify
- * @return a Promise that resolves to an object with properties:
- *         signedState: an AddonManager.SIGNEDSTATE_* constant
- *         cert: an nsIX509Cert
- */
-function verifyZipSignedState(aFile, aAddon) {
-  if (!shouldVerifySignedState(aAddon))
-    return Promise.resolve({
-      signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED,
-      cert: null
-    });
-
-  let root = Ci.nsIX509CertDB.AddonsPublicRoot;
-  if (!AppConstants.MOZ_REQUIRE_SIGNING && Services.prefs.getBoolPref(PREF_XPI_SIGNATURES_DEV_ROOT, false))
-    root = Ci.nsIX509CertDB.AddonsStageRoot;
-
-  return new Promise(resolve => {
-    let callback = {
-      openSignedAppFileFinished(aRv, aZipReader, aCert) {
-        if (aZipReader)
-          aZipReader.close();
-        resolve({
-          signedState: getSignedStatus(aRv, aCert, aAddon.id),
-          cert: aCert
-        });
-      }
-    };
-    // This allows the certificate DB to get the raw JS callback object so the
-    // test code can pass through objects that XPConnect would reject.
-    callback.wrappedJSObject = callback;
-
-    gCertDB.openSignedAppFileAsync(root, aFile, callback);
-  });
-}
-
-/**
- * Verifies that a directory's contents are all correctly signed by an
- * AMO-issued certificate
- *
- * @param  aDir
- *         the directory to check
- * @param  aAddon
- *         the add-on object to verify
- * @return a Promise that resolves to an object with properties:
- *         signedState: an AddonManager.SIGNEDSTATE_* constant
- *         cert: an nsIX509Cert
- */
-function verifyDirSignedState(aDir, aAddon) {
-  if (!shouldVerifySignedState(aAddon))
-    return Promise.resolve({
-      signedState: AddonManager.SIGNEDSTATE_NOT_REQUIRED,
-      cert: null,
-    });
-
-  let root = Ci.nsIX509CertDB.AddonsPublicRoot;
-  if (!AppConstants.MOZ_REQUIRE_SIGNING && Services.prefs.getBoolPref(PREF_XPI_SIGNATURES_DEV_ROOT, false))
-    root = Ci.nsIX509CertDB.AddonsStageRoot;
-
-  return new Promise(resolve => {
-    let callback = {
-      verifySignedDirectoryFinished(aRv, aCert) {
-        resolve({
-          signedState: getSignedStatus(aRv, aCert, aAddon.id),
-          cert: null,
-        });
-      }
-    };
-    // This allows the certificate DB to get the raw JS callback object so the
-    // test code can pass through objects that XPConnect would reject.
-    callback.wrappedJSObject = callback;
-
-    gCertDB.verifySignedDirectoryAsync(root, aDir, callback);
-  });
-}
-
-/**
  * Verifies that a bundle's contents are all correctly signed by an
  * AMO-issued certificate
  *
  * @param  aBundle
  *         the nsIFile for the bundle to check, either a directory or zip file
  * @param  aAddon
  *         the add-on object to verify
  * @return a Promise that resolves to an AddonManager.SIGNEDSTATE_* constant.
  */
-var verifyBundleSignedState = function(aBundle, aAddon) {
-  let promise = aBundle.isFile() ? verifyZipSignedState(aBundle, aAddon)
-      : verifyDirSignedState(aBundle, aAddon);
-  return promise.then(({signedState}) => signedState);
+var verifyBundleSignedState = async function(aBundle, aAddon) {
+  let pkg = Package.get(aBundle);
+  try {
+    let {signedState} = await pkg.verifySignedState(aAddon);
+    return signedState;
+  } finally {
+    pkg.close();
+  }
 };
 
 /**
  * Replaces %...% strings in an addon url (update and updateInfo) with
  * appropriate values.
  *
  * @param  aAddon
  *         The AddonInternal representing the add-on
@@ -1497,72 +1416,67 @@ class AddonInstall {
    * manifest can be read.
    *
    * @param  aCallback
    *         A function to call when the manifest has been loaded
    * @throws if the add-on does not contain a valid install manifest or the
    *         XPI is incorrectly signed
    */
   async loadManifest(file) {
-    let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
-        createInstance(Ci.nsIZipReader);
+    let pkg;
     try {
-      zipreader.open(file);
+      pkg = Package.get(file);
     } catch (e) {
-      zipreader.close();
       return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
     }
 
     try {
-      // loadManifestFromZipReader performs the certificate verification for us
-      this.addon = await loadManifestFromZipReader(zipreader, this.installLocation);
-    } catch (e) {
-      zipreader.close();
-      return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
-    }
+      try {
+        this.addon = await loadManifest(pkg, this.installLocation);
+      } catch (e) {
+        return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
+      }
 
-    if (!this.addon.id) {
-      let err = new Error(`Cannot find id for addon ${file.path}`);
-      return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, err]);
-    }
-
-    if (this.existingAddon) {
-      // Check various conditions related to upgrades
-      if (this.addon.id != this.existingAddon.id) {
-        zipreader.close();
-        return Promise.reject([AddonManager.ERROR_INCORRECT_ID,
-                               `Refusing to upgrade addon ${this.existingAddon.id} to different ID ${this.addon.id}`]);
+      if (!this.addon.id) {
+        let err = new Error(`Cannot find id for addon ${file.path}`);
+        return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, err]);
       }
 
-      if (isWebExtension(this.existingAddon.type) && !isWebExtension(this.addon.type)) {
-        zipreader.close();
-        return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
-                               "WebExtensions may not be updated to other extension types"]);
+      if (this.existingAddon) {
+        // Check various conditions related to upgrades
+        if (this.addon.id != this.existingAddon.id) {
+          return Promise.reject([AddonManager.ERROR_INCORRECT_ID,
+                                 `Refusing to upgrade addon ${this.existingAddon.id} to different ID ${this.addon.id}`]);
+        }
+
+        if (isWebExtension(this.existingAddon.type) && !isWebExtension(this.addon.type)) {
+          return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
+                                 "WebExtensions may not be updated to other extension types"]);
+        }
       }
-    }
 
-    if (mustSign(this.addon.type)) {
-      if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
-        // This add-on isn't properly signed by a signature that chains to the
-        // trusted root.
-        let state = this.addon.signedState;
-        this.addon = null;
-        zipreader.close();
+      if (mustSign(this.addon.type)) {
+        if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+          // This add-on isn't properly signed by a signature that chains to the
+          // trusted root.
+          let state = this.addon.signedState;
+          this.addon = null;
 
-        if (state == AddonManager.SIGNEDSTATE_MISSING)
-          return Promise.reject([AddonManager.ERROR_SIGNEDSTATE_REQUIRED,
-                                 "signature is required but missing"]);
+          if (state == AddonManager.SIGNEDSTATE_MISSING)
+            return Promise.reject([AddonManager.ERROR_SIGNEDSTATE_REQUIRED,
+                                   "signature is required but missing"]);
 
-        return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
-                               "signature verification failed"]);
+          return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
+                                 "signature verification failed"]);
+        }
       }
+    } finally {
+      pkg.close();
     }
 
-    zipreader.close();
-
     this.updateAddonURIs();
 
     this.addon._install = this;
     this.name = this.addon.selectedLocale.name;
     this.type = this.addon.type;
     this.version = this.addon.version;
 
     // Setting the iconURL to something inside the XPI locks the XPI and
@@ -1590,17 +1504,17 @@ class AddonInstall {
     if (!this.addon.icons || !this.file) {
       return null;
     }
 
     let {icon} = IconDetails.getPreferredIcon(this.addon.icons, null, desiredSize);
     if (icon.startsWith("chrome://")) {
       return icon;
     }
-    return buildJarURI(this.file, icon).spec;
+    return getJarURI(this.file, icon).spec;
   }
 
   /**
    * This method should be called when the XPI is ready to be installed,
    * i.e., when a download finishes or when a local file has been verified.
    * It should only be called from install() when the install is in
    * STATE_DOWNLOADED (which actually means that the file is available
    * and has been verified).
@@ -1934,20 +1848,19 @@ var LocalAddonInstall = class extends Ad
       return;
     }
 
     this.state = AddonManager.STATE_DOWNLOADED;
     this.progress = this.file.fileSize;
     this.maxProgress = this.file.fileSize;
 
     if (this.hash) {
-      let crypto = Cc["@mozilla.org/security/hash;1"].
-          createInstance(Ci.nsICryptoHash);
+      let crypto;
       try {
-        crypto.initWithString(this.hash.algorithm);
+        crypto = CryptoHash(this.hash.algorithm);
       } catch (e) {
         logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
         this.state = AddonManager.STATE_DOWNLOAD_FAILED;
         this.error = AddonManager.ERROR_INCORRECT_HASH;
         XPIProvider.removeActiveInstall(this);
         return;
       }
 
@@ -2232,35 +2145,33 @@ var DownloadAddonInstall = class extends
   }
 
   /**
    * This is the first chance to get at real headers on the channel.
    *
    * @see nsIStreamListener
    */
   onStartRequest(aRequest, aContext) {
-    this.crypto = Cc["@mozilla.org/security/hash;1"].
-                  createInstance(Ci.nsICryptoHash);
     if (this.hash) {
       try {
-        this.crypto.initWithString(this.hash.algorithm);
+        this.crypto = CryptoHash(this.hash.algorithm);
       } catch (e) {
         logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
         this.state = AddonManager.STATE_DOWNLOAD_FAILED;
         this.error = AddonManager.ERROR_INCORRECT_HASH;
         XPIProvider.removeActiveInstall(this);
         AddonManagerPrivate.callInstallListeners("onDownloadFailed",
                                                  this.listeners, this.wrapper);
         aRequest.cancel(Cr.NS_BINDING_ABORTED);
         return;
       }
     } else {
       // We always need something to consume data from the inputstream passed
       // to onDataAvailable so just create a dummy cryptohasher to do that.
-      this.crypto.initWithString("sha1");
+      this.crypto = CryptoHash("sha1");
     }
 
     this.progress = 0;
     if (aRequest instanceof Ci.nsIChannel) {
       try {
         this.maxProgress = aRequest.contentLength;
       } catch (e) {
       }
@@ -2739,8 +2650,14 @@ UpdateChecker.prototype = {
     let parser = this._parser;
     if (parser) {
       this._parser = null;
       // This will call back to onUpdateCheckError with a CANCELLED error
       parser.cancel();
     }
   }
 };
+
+var XPIInstall = {
+  flushChromeCaches,
+  flushJarCache,
+  recursiveRemove,
+};
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -30,16 +30,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   JSONFile: "resource://gre/modules/JSONFile.jsm",
   LegacyExtensionsUtils: "resource://gre/modules/LegacyExtensionsUtils.jsm",
   setTimeout: "resource://gre/modules/Timer.jsm",
   clearTimeout: "resource://gre/modules/Timer.jsm",
 
   DownloadAddonInstall: "resource://gre/modules/addons/XPIInstall.jsm",
   LocalAddonInstall: "resource://gre/modules/addons/XPIInstall.jsm",
   UpdateChecker: "resource://gre/modules/addons/XPIInstall.jsm",
+  XPIInstall: "resource://gre/modules/addons/XPIInstall.jsm",
   loadManifestFromFile: "resource://gre/modules/addons/XPIInstall.jsm",
   verifyBundleSignedState: "resource://gre/modules/addons/XPIInstall.jsm",
 });
 
 const {nsIBlocklistService} = Ci;
 
 XPCOMUtils.defineLazyServiceGetters(this, {
   Blocklist: ["@mozilla.org/extensions/blocklist;1", "nsIBlocklistService"],
@@ -247,19 +248,16 @@ const XPI_BEFORE_UI_STARTUP = "BeforeFin
 // event happened after final-ui-startup
 const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup";
 
 const COMPATIBLE_BY_DEFAULT_TYPES = {
   extension: true,
   dictionary: true
 };
 
-const MSG_JAR_FLUSH = "AddonJarFlush";
-const MSG_MESSAGE_MANAGER_CACHES_FLUSH = "AddonMessageManagerCachesFlush";
-
 var gGlobalScope = this;
 
 /**
  * Valid IDs fit this pattern.
  */
 var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
 
 ChromeUtils.import("resource://gre/modules/Log.jsm");
@@ -300,17 +298,17 @@ function loadLazyObjects() {
     DB_SCHEMA,
     DEFAULT_SKIN,
     AddonInternal,
     XPIProvider,
     XPIStates,
     syncLoadManifestFromFile,
     isUsableAddon,
     recordAddonTelemetry,
-    flushChromeCaches,
+    flushChromeCaches: XPIInstall.flushChromeCaches,
     descriptorToPath,
   });
 
   Services.scriptloader.loadSubScript(uri, scope);
 
   for (let name of LAZY_OBJECTS) {
     delete gGlobalScope[name];
     gGlobalScope[name] = scope[name];
@@ -735,17 +733,17 @@ SafeInstallOperation.prototype = {
         // No old file means this was a copied file
         move.newFile.remove(true);
       } else {
         move.newFile.moveTo(move.oldFile.parent, null);
       }
     }
 
     while (this._createdDirs.length > 0)
-      recursiveRemove(this._createdDirs.pop());
+      XPIInstall.recursiveRemove(this._createdDirs.pop());
   }
 };
 
 /**
  * Evaluates whether an add-on is allowed to run in safe mode.
  *
  * @param  aAddon
  *         The add-on to check
@@ -977,87 +975,16 @@ function getURIForResourceInFile(aFile, 
  */
 function buildJarURI(aJarfile, aPath) {
   let uri = Services.io.newFileURI(aJarfile);
   uri = "jar:" + uri.spec + "!/" + aPath;
   return Services.io.newURI(uri);
 }
 
 /**
- * Sends local and remote notifications to flush a JAR file cache entry
- *
- * @param aJarFile
- *        The ZIP/XPI/JAR file as a nsIFile
- */
-function flushJarCache(aJarFile) {
-  Services.obs.notifyObservers(aJarFile, "flush-cache-entry");
-  Services.mm.broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path);
-}
-
-function flushChromeCaches() {
-  // Init this, so it will get the notification.
-  Services.obs.notifyObservers(null, "startupcache-invalidate");
-  // Flush message manager cached scripts
-  Services.obs.notifyObservers(null, "message-manager-flush-caches");
-  // Also dispatch this event to child processes
-  Services.mm.broadcastAsyncMessage(MSG_MESSAGE_MANAGER_CACHES_FLUSH, null);
-}
-
-/**
- * Recursively removes a directory or file fixing permissions when necessary.
- *
- * @param  aFile
- *         The nsIFile to remove
- */
-function recursiveRemove(aFile) {
-  let isDir = null;
-
-  try {
-    isDir = aFile.isDirectory();
-  } catch (e) {
-    // If the file has already gone away then don't worry about it, this can
-    // happen on OSX where the resource fork is automatically moved with the
-    // data fork for the file. See bug 733436.
-    if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
-      return;
-    if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND)
-      return;
-
-    throw e;
-  }
-
-  setFilePermissions(aFile, isDir ? FileUtils.PERMS_DIRECTORY
-                                  : FileUtils.PERMS_FILE);
-
-  try {
-    aFile.remove(true);
-    return;
-  } catch (e) {
-    if (!aFile.isDirectory() || aFile.isSymlink()) {
-      logger.error("Failed to remove file " + aFile.path, e);
-      throw e;
-    }
-  }
-
-  // Use a snapshot of the directory contents to avoid possible issues with
-  // iterating over a directory while removing files from it (the YAFFS2
-  // embedded filesystem has this issue, see bug 772238), and to remove
-  // normal files before their resource forks on OSX (see bug 733436).
-  let entries = getDirectoryEntries(aFile, true);
-  entries.forEach(recursiveRemove);
-
-  try {
-    aFile.remove(true);
-  } catch (e) {
-    logger.error("Failed to remove empty directory " + aFile.path, e);
-    throw e;
-  }
-}
-
-/**
  * Gets a snapshot of directory entries.
  *
  * @param  aDir
  *         Directory to look at
  * @param  aSortEntries
  *         True to sort entries by filename
  * @return An array of nsIFile, or an empty array if aDir is not a readable directory
  */
@@ -2779,17 +2706,17 @@ var XPIProvider = {
               let newVersion = addon.version;
               let oldVersion = existingAddon;
               let uninstallReason = newVersionReason(oldVersion, newVersion);
 
               this.callBootstrapMethod(existingAddon,
                                        file, "uninstall", uninstallReason,
                                        { newVersion });
               this.unloadBootstrapScope(id);
-              flushChromeCaches();
+              XPIInstall.flushChromeCaches();
             }
           } catch (e) {
             Cu.reportError(e);
           }
         }
 
         try {
           addon._sourceBundle = location.installAddon({
@@ -3335,17 +3262,17 @@ var XPIProvider = {
    *        (see MutableDirectoryInstallLocation.installAddon)
    *
    * @return a Promise that resolves to an Addon object on success, or rejects
    *         if the add-on is not a valid restartless add-on or if the
    *         same ID is already installed.
    */
   async installAddonFromLocation(aFile, aInstallLocation, aInstallAction) {
     if (aFile.exists() && aFile.isFile()) {
-      flushJarCache(aFile);
+      XPIInstall.flushJarCache(aFile);
     }
     let addon = await loadManifestFromFile(aFile, aInstallLocation);
 
     aInstallLocation.installAddon({ id: addon.id, source: aFile, action: aInstallAction });
 
     if (addon.appDisabled) {
       let message = `Add-on ${addon.id} is not compatible with application version.`;
 
@@ -3405,17 +3332,17 @@ var XPIProvider = {
                                           extraParams);
         }
 
         if (!callUpdate) {
           this.callBootstrapMethod(oldAddon, existingAddon,
                                    "uninstall", uninstallReason, extraParams);
         }
         this.unloadBootstrapScope(existingAddonID);
-        flushChromeCaches();
+        XPIInstall.flushChromeCaches();
       }
     } else {
       addon.installDate = Date.now();
     }
 
     let file = addon._sourceBundle;
 
     let method = callUpdate ? "update" : "install";
@@ -4204,17 +4131,17 @@ var XPIProvider = {
         }
 
         if (!callUpdate) {
           this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "uninstall",
                                    reason);
         }
         XPIStates.disableAddon(aAddon.id);
         this.unloadBootstrapScope(aAddon.id);
-        flushChromeCaches();
+        XPIInstall.flushChromeCaches();
       }
       aAddon._installLocation.uninstallAddon(aAddon.id);
       XPIDatabase.removeAddonMetadata(aAddon);
       XPIStates.removeAddon(aAddon.location, aAddon.id);
       AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
 
       if (existingAddon) {
         XPIDatabase.getAddonInLocation(aAddon.id, existingAddon.location.name, existing => {
@@ -5639,17 +5566,17 @@ class MutableDirectoryInstallLocation ex
    *         An array of file or directory to remove from the directory, the
    *         array may be empty
    */
   cleanStagingDir(aLeafNames = []) {
     let dir = this.getStagingDir();
 
     for (let name of aLeafNames) {
       let file = getFile(name, dir);
-      recursiveRemove(file);
+      XPIInstall.recursiveRemove(file);
     }
 
     if (this._stagingDirLock > 0)
       return;
 
     let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
     try {
       if (dirEntries.nextFile)
@@ -5675,17 +5602,17 @@ class MutableDirectoryInstallLocation ex
    *
    * @return an nsIFile
    */
   getTrashDir() {
     let trashDir = getFile(DIR_TRASH, this._directory);
     let trashDirExists = trashDir.exists();
     try {
       if (trashDirExists)
-        recursiveRemove(trashDir);
+        XPIInstall.recursiveRemove(trashDir);
       trashDirExists = false;
     } catch (e) {
       logger.warn("Failed to remove trash directory", e);
     }
     if (!trashDirExists)
       trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 
     return trashDir;
@@ -5718,17 +5645,17 @@ class MutableDirectoryInstallLocation ex
 
     let moveOldAddon = aId => {
       let file = getFile(aId, this._directory);
       if (file.exists())
         transaction.moveUnder(file, trashDir);
 
       file = getFile(`${aId}.xpi`, this._directory);
       if (file.exists()) {
-        flushJarCache(file);
+        XPIInstall.flushJarCache(file);
         transaction.moveUnder(file, trashDir);
       }
     };
 
     // If any of these operations fails the finally block will clean up the
     // temporary directory
     try {
       moveOldAddon(id);
@@ -5758,26 +5685,26 @@ class MutableDirectoryInstallLocation ex
           }
         }
       }
 
       if (action == "copy") {
         transaction.copy(source, this._directory);
       } else if (action == "move") {
         if (source.isFile())
-          flushJarCache(source);
+          XPIInstall.flushJarCache(source);
 
         transaction.moveUnder(source, this._directory);
       }
       // Do nothing for the proxy file as we sideload an addon permanently
     } finally {
       // It isn't ideal if this cleanup fails but it isn't worth rolling back
       // the install because of it.
       try {
-        recursiveRemove(trashDir);
+        XPIInstall.recursiveRemove(trashDir);
       } catch (e) {
         logger.warn("Failed to remove trash directory when installing " + id, e);
       }
     }
 
     let newFile = this._directory.clone();
 
     if (action == "proxy") {
@@ -5831,28 +5758,28 @@ class MutableDirectoryInstallLocation ex
       delete this._IDToFileMap[aId];
       return;
     }
 
     let trashDir = this.getTrashDir();
 
     if (file.leafName != aId) {
       logger.debug("uninstallAddon: flushing jar cache " + file.path + " for addon " + aId);
-      flushJarCache(file);
+      XPIInstall.flushJarCache(file);
     }
 
     let transaction = new SafeInstallOperation();
 
     try {
       transaction.moveUnder(file, trashDir);
     } finally {
       // It isn't ideal if this cleanup fails, but it is probably better than
       // rolling back the uninstall at this point
       try {
-        recursiveRemove(trashDir);
+        XPIInstall.recursiveRemove(trashDir);
       } catch (e) {
         logger.warn("Failed to remove trash directory when uninstalling " + aId, e);
       }
     }
 
     XPIStates.removeAddon(this.name, aId);
 
     delete this._IDToFileMap[aId];
@@ -6283,17 +6210,17 @@ class SystemAddonInstallLocation extends
    *
    * @return an nsIFile
    */
   getTrashDir() {
     let trashDir = getFile(DIR_TRASH, this._directory);
     let trashDirExists = trashDir.exists();
     try {
       if (trashDirExists)
-        recursiveRemove(trashDir);
+        XPIInstall.recursiveRemove(trashDir);
       trashDirExists = false;
     } catch (e) {
       logger.warn("Failed to remove trash directory", e);
     }
     if (!trashDirExists)
       trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 
     return trashDir;
@@ -6311,25 +6238,25 @@ class SystemAddonInstallLocation extends
   installAddon({id, source}) {
     let trashDir = this.getTrashDir();
     let transaction = new SafeInstallOperation();
 
     // If any of these operations fails the finally block will clean up the
     // temporary directory
     try {
       if (source.isFile()) {
-        flushJarCache(source);
+        XPIInstall.flushJarCache(source);
       }
 
       transaction.moveUnder(source, this._directory);
     } finally {
       // It isn't ideal if this cleanup fails but it isn't worth rolling back
       // the install because of it.
       try {
-        recursiveRemove(trashDir);
+        XPIInstall.recursiveRemove(trashDir);
       } catch (e) {
         logger.warn("Failed to remove trash directory when installing " + id, e);
       }
     }
 
     let newFile = getFile(source.leafName, this._directory);
 
     try {
deleted file mode 100644
--- a/toolkit/mozapps/extensions/test/addons/webextension_2/install.rdf
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0"?>
-
-<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
-
-  <Description about="urn:mozilla:install-manifest">
-    <em:id>first-webextension2@tests.mozilla.org</em:id>
-    <em:version>2.0</em:version>
-    <em:bootstrap>true</em:bootstrap>
-
-    <em:targetApplication>
-      <Description>
-        <em:id>xpcshell@tests.mozilla.org</em:id>
-        <em:minVersion>1</em:minVersion>
-        <em:maxVersion>1</em:maxVersion>
-      </Description>
-    </em:targetApplication>
-
-    <!-- Front End MetaData -->
-    <em:name>XPI Add-on 1</em:name>
-    <em:description>XPI Add-on 1 - Description</em:description>
-    <em:creator>XPI Add-on 1 - Creator</em:creator>
-    <em:developer>XPI Add-on 1 - First Developer</em:developer>
-    <em:translator>XPI Add-on 1 - First Translator</em:translator>
-    <em:contributor>XPI Add-on 1 - First Contributor</em:contributor>
-    <em:homepageURL>http://localhost/xpi/1/homepage.html</em:homepageURL>
-    <em:optionsURL>http://localhost/xpi/1/options.html</em:optionsURL>
-    <em:aboutURL>http://localhost/xpi/1/about.html</em:aboutURL>
-    <em:iconURL>http://localhost/xpi/1/icon.png</em:iconURL>
-  </Description>
-</RDF>
deleted file mode 100644
--- a/toolkit/mozapps/extensions/test/addons/webextension_2/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "name": "Web Extension Name",
-  "version": "1.0",
-  "manifest_version": 2,
-  "applications": {
-    "gecko": {
-      "id": "last-webextension2@tests.mozilla.org"
-    }
-  }
-}
--- a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
@@ -183,17 +183,18 @@ add_task(async function setup() {
   // Install an add-on so can check that it isn't returned in the results
   await promiseInstallFile(xpis[0]);
   await promiseRestartManager();
 
   // Create an active AddonInstall so can check that it isn't returned in the results
   let install = await AddonManager.getInstallForURL(BASE_URL + INSTALL_URL2,
                                                     undefined,
                                                     "application/x-xpinstall");
-  install.install();
+  let promise = promiseCompleteInstall(install);
+  registerCleanupFunction(() => promise);
 
   // Create a non-active AddonInstall so can check that it is returned in the results
   await AddonManager.getInstallForURL(BASE_URL + INSTALL_URL3,
                                       undefined, "application/x-xpinstall");
 });
 
 // Tests homepageURL and getSearchURL()
 add_task(async function test_1() {
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
@@ -207,39 +207,16 @@ add_task(async function() {
   Assert.equal(addon, null);
 
   let file = getFileForAddon(profileDir, ID);
   Assert.ok(!file.exists());
 
   await promiseRestartManager();
 });
 
-// install.rdf should be read before manifest.json
-add_task(async function() {
-
-  await Promise.all([
-    promiseInstallAllFiles([do_get_addon("webextension_2")], true)
-  ]);
-
-  await promiseRestartManager();
-
-  let installrdf_id = "first-webextension2@tests.mozilla.org";
-  let first_addon = await promiseAddonByID(installrdf_id);
-  Assert.notEqual(first_addon, null);
-  Assert.ok(!first_addon.appDisabled);
-  Assert.ok(first_addon.isActive);
-  Assert.ok(!first_addon.isSystem);
-
-  let manifestjson_id = "last-webextension2@tests.mozilla.org";
-  let last_addon = await promiseAddonByID(manifestjson_id);
-  Assert.equal(last_addon, null);
-
-  await promiseRestartManager();
-});
-
 // Test that the "options_ui" manifest section is processed correctly.
 add_task(async function test_options_ui() {
   let OPTIONS_RE = /^moz-extension:\/\/[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}\/options\.html$/;
 
   const extensionId = "webextension@tests.mozilla.org";
   await promiseInstallWebExtension({
     manifest: {
       applications: {gecko: {id: extensionId}},