Bug 1452827: Cleanup a bunch of duplication and cruft in XPIInstall.jsm. r=aswan
authorKris Maglione <maglione.k@gmail.com>
Mon, 09 Apr 2018 17:21:13 -0700
changeset 412741 bc804d75911a04cc104898a0cee538e761f5582b
parent 412740 19d7d934850c1c3efcda7b1227cfa5ffcd2ce63b
child 412742 ac40ae7983a0235ca4d98e3f769ae3c60b8b6d55
push id33818
push userapavel@mozilla.com
push dateWed, 11 Apr 2018 14:36:40 +0000
treeherdermozilla-central@cfe6399e142c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan
bugs1452827
milestone61.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 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", "TextEncoder", "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,30 @@ 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, {
+  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 +125,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 +188,193 @@ 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 {
+  static get(file) {
+    if (file.isFile()) {
+      return new XPIPackage(file);
+    }
+    return new DirPackage(file);
+  }
+
+  constructor(file, rootURI) {
+    this.file = file;
+    this.filePath = file.path;
+    this.rootURI = rootURI;
+  }
+
+  close() {}
+
+  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;
+  }
+
+  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) {
@@ -386,23 +554,23 @@ async function loadManifestFromWebManife
   return addon;
 }
 
 /**
  * Reads an AddonInternal object from an RDF stream.
  *
  * @param  aUri
  *         The URI that the manifest is being read from
- * @param  aStream
- *         An open stream to read the RDF from
+ * @param  aData
+ *         The manifest text
  * @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
  */
@@ -2781,17 +2708,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({
@@ -3337,17 +3264,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.`;
 
@@ -3407,17 +3334,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";
@@ -4223,17 +4150,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 => {
@@ -5658,17 +5585,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)
@@ -5694,17 +5621,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;
@@ -5737,17 +5664,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);
@@ -5777,26 +5704,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") {
@@ -5850,28 +5777,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];
@@ -6302,17 +6229,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;
@@ -6330,25 +6257,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}},