Bug 1410839 - Add the ChromeMigrationUtils.jsm module and implement the getExtensionList and related functions to export all extensions information installed in a specific profile. r=Gijs
authorEvan Tseng <evan@tseng.io>
Thu, 23 Nov 2017 10:48:18 +0800
changeset 438127 be627b364b313dec76534463eecb42e5738429f2
parent 438126 172e6c7b3adad03e0ad1467f92ef50a5673a87cb
child 438128 eb8da591a5225538c3a5fbc05d0a3ea001e9186e
push id117
push userfmarier@mozilla.com
push dateTue, 28 Nov 2017 20:17:16 +0000
reviewersGijs
bugs1410839
milestone59.0a1
Bug 1410839 - Add the ChromeMigrationUtils.jsm module and implement the getExtensionList and related functions to export all extensions information installed in a specific profile. r=Gijs MozReview-Commit-ID: AqlJzKkTjWp
browser/components/migration/ChromeMigrationUtils.jsm
browser/components/migration/ChromeProfileMigrator.js
browser/components/migration/moz.build
browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json
browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json
browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json
browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json
browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json
browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js
browser/components/migration/tests/unit/xpcshell.ini
toolkit/components/osfile/modules/osfile_async_front.jsm
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/ChromeMigrationUtils.jsm
@@ -0,0 +1,288 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ChromeMigrationUtils"];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1";
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.ChromeMigrationUtils = {
+  _chromeUserDataPath: null,
+
+  _extensionVersionDirectoryNames: {},
+
+  // The cache for the locale strings.
+  // For example, the data could be:
+  // {
+  //   "profile-id-1": {
+  //     "extension-id-1": {
+  //       "name": {
+  //         "message": "Fake App 1"
+  //       }
+  //   },
+  // }
+  _extensionLocaleStrings: {},
+
+  /**
+   * Get all extensions installed in a specific profile.
+   * @param {String} profileId - A Chrome user profile ID. For example, "Profile 1".
+   * @returns {Array} All installed Chrome extensions information.
+   */
+  async getExtensionList(profileId = this.getLastUsedProfileId()) {
+    let path = this.getExtensionPath(profileId);
+    let iterator = new OS.File.DirectoryIterator(path);
+    let extensionList = [];
+    await iterator.forEach(async entry => {
+      if (entry.isDir) {
+        let extensionInformation = await this.getExtensionInformation(entry.name, profileId);
+        if (extensionInformation) {
+          extensionList.push(extensionInformation);
+        }
+      }
+    }).catch(ex => Cu.reportError(ex));
+    return extensionList;
+  },
+
+  /**
+   * Get information of a specific Chrome extension.
+   * @param {String} extensionId - The extension ID.
+   * @param {String} profileId - The user profile's ID.
+   * @retruns {Object} The Chrome extension information.
+   */
+  async getExtensionInformation(extensionId, profileId = this.getLastUsedProfileId()) {
+    let extensionInformation = null;
+    try {
+      let manifestPath = this.getExtensionPath(profileId);
+      manifestPath = OS.Path.join(manifestPath, extensionId);
+      // If there are multiple sub-directories in the extension directory,
+      // read the files in the latest directory.
+      let directories = await this._getSortedByVersionSubDirectoryNames(manifestPath);
+      if (!directories[0]) {
+        return null;
+      }
+
+      manifestPath = OS.Path.join(manifestPath, directories[0], "manifest.json");
+      let manifest = await OS.File.read(manifestPath, { encoding: "utf-8" });
+      manifest = JSON.parse(manifest);
+      // No app attribute means this is a Chrome extension not a Chrome app.
+      if (!manifest.app) {
+        const DEFAULT_LOCALE = manifest.default_locale;
+        let name = await this._getLocaleString(manifest.name, DEFAULT_LOCALE, extensionId, profileId);
+        let description = await this._getLocaleString(manifest.description, DEFAULT_LOCALE, extensionId, profileId);
+        if (name) {
+          extensionInformation = {
+            id: extensionId,
+            name,
+            description,
+          };
+        } else {
+          throw new Error("Cannot read the Chrome extension's name property.");
+        }
+      }
+    } catch (ex) {
+      Cu.reportError(ex);
+    }
+    return extensionInformation;
+  },
+
+  /**
+   * Get the manifest's locale string.
+   * @param {String} key - The key of a locale string, for example __MSG_name__.
+   * @param {String} locale - The specific language of locale string.
+   * @param {String} extensionId - The extension ID.
+   * @param {String} profileId - The user profile's ID.
+   * @retruns {String} The locale string.
+   */
+  async _getLocaleString(key, locale, extensionId, profileId) {
+    // Return the key string if it is not a locale key.
+    // The key string starts with "__MSG_" and ends with "__".
+    // For example, "__MSG_name__".
+    // https://developer.chrome.com/apps/i18n
+    if (!key.startsWith("__MSG_") || !key.endsWith("__")) {
+      return key;
+    }
+
+    let localeString = null;
+    try {
+      let localeFile;
+      if (this._extensionLocaleStrings[profileId] &&
+          this._extensionLocaleStrings[profileId][extensionId]) {
+        localeFile = this._extensionLocaleStrings[profileId][extensionId];
+      } else {
+        if (!this._extensionLocaleStrings[profileId]) {
+          this._extensionLocaleStrings[profileId] = {};
+        }
+        let localeFilePath = this.getExtensionPath(profileId);
+        localeFilePath = OS.Path.join(localeFilePath, extensionId);
+        let directories = await this._getSortedByVersionSubDirectoryNames(localeFilePath);
+        // If there are multiple sub-directories in the extension directory,
+        // read the files in the latest directory.
+        localeFilePath = OS.Path.join(localeFilePath, directories[0], "_locales", locale, "messages.json");
+        localeFile = await OS.File.read(localeFilePath, { encoding: "utf-8" });
+        localeFile = JSON.parse(localeFile);
+        this._extensionLocaleStrings[profileId][extensionId] = localeFile;
+      }
+      const PREFIX_LENGTH = 6;
+      const SUFFIX_LENGTH = 2;
+      // Get the locale key from the string with locale prefix and suffix.
+      // For example, it will get the "name" sub-string from the "__MSG_name__" string.
+      key = key.substring(PREFIX_LENGTH, key.length - SUFFIX_LENGTH);
+      if (localeFile[key] && localeFile[key].message) {
+        localeString = localeFile[key].message;
+      }
+    } catch (ex) {
+      Cu.reportError(ex);
+    }
+    return localeString;
+  },
+
+  /**
+   * Check that a specific extension is installed or not.
+   * @param {String} extensionId - The extension ID.
+   * @param {String} profileId - The user profile's ID.
+   * @returns {Boolean} Return true if the extension is installed otherwise return false.
+   */
+  async isExtensionInstalled(extensionId, profileId = this.getLastUsedProfileId()) {
+    let extensionPath = this.getExtensionPath(profileId);
+    let isInstalled = await OS.File.exists(OS.Path.join(extensionPath, extensionId));
+    return isInstalled;
+  },
+
+  /**
+   * Get the last used user profile's ID.
+   * @returns {String} The last used user profile's ID.
+   */
+  getLastUsedProfileId() {
+    let localState = this.getLocalState();
+    return localState ? localState.profile.last_used : "Default";
+  },
+
+  /**
+   * Get the local state file content.
+   * @returns {Object} The JSON-based content.
+   */
+  getLocalState() {
+    let localStateFile = new FileUtils.File(this.getChromeUserDataPath());
+    localStateFile.append("Local State");
+    if (!localStateFile.exists())
+      throw new Error("Chrome's 'Local State' file does not exist.");
+    if (!localStateFile.isReadable())
+      throw new Error("Chrome's 'Local State' file could not be read.");
+
+    let localState = null;
+    try {
+      let fstream = Cc[FILE_INPUT_STREAM_CID].createInstance(Ci.nsIFileInputStream);
+      fstream.init(localStateFile, -1, 0, 0);
+      let inputStream = NetUtil.readInputStreamToString(fstream, fstream.available(),
+                                                        { charset: "UTF-8" });
+      localState = JSON.parse(inputStream);
+    } catch (ex) {
+      Cu.reportError(ex);
+      throw ex;
+    }
+    return localState;
+  },
+
+  /**
+   * Get the path of Chrome extension directory.
+   * @param {String} profileId - The user profile's ID.
+   * @returns {String} The path of Chrome extension directory.
+   */
+  getExtensionPath(profileId) {
+    return OS.Path.join(this.getChromeUserDataPath(), profileId, "Extensions");
+  },
+
+  /**
+   * Get the path of the Chrome user data directory.
+   * @returns {String} The path of the Chrome user data directory.
+   */
+  getChromeUserDataPath() {
+    if (!this._chromeUserDataPath) {
+      this._chromeUserDataPath = this.getDataPath("Chrome");
+    }
+    return this._chromeUserDataPath;
+  },
+
+  /**
+   * Get the path of an application data directory.
+   * @param {String} chromeProjectName - The Chrome project name, e.g. "Chrome", "Chromium" or "Canary".
+   * @returns {String} The path of application data directory.
+   */
+  getDataPath(chromeProjectName) {
+    const SUB_DIRECTORIES = {
+      win: {
+        Chrome: ["Google", "Chrome"],
+        Chromium: ["Chromium"],
+        Canary: ["Google", "Chrome SxS"],
+      },
+      macosx: {
+        Chrome: ["Google", "Chrome"],
+        Chromium: ["Chromium"],
+        Canary: ["Google", "Chrome Canary"],
+      },
+      linux: {
+        Chrome: ["google-chrome"],
+        Chromium: ["chromium"],
+        // Canary is not available on Linux.
+      },
+    };
+    let dirKey, subfolders;
+    subfolders = SUB_DIRECTORIES[AppConstants.platform][chromeProjectName];
+    if (!subfolders) {
+      return null;
+    }
+
+    if (AppConstants.platform == "win") {
+      dirKey = "winLocalAppDataDir";
+      subfolders = subfolders.concat(["User Data"]);
+    } else if (AppConstants.platform == "macosx") {
+      dirKey = "macUserLibDir";
+      subfolders = ["Application Support"].concat(subfolders);
+    } else {
+      dirKey = "homeDir";
+      subfolders = [".config"].concat(subfolders);
+    }
+    subfolders.unshift(OS.Constants.Path[dirKey]);
+    return OS.Path.join(...subfolders);
+  },
+
+  /**
+   * Get the directory objects sorted by version number.
+   * @param {String} path - The path to the extension directory.
+   * otherwise return all file/directory object.
+   * @returns {Array} The file/directory object array.
+   */
+  async _getSortedByVersionSubDirectoryNames(path) {
+    if (this._extensionVersionDirectoryNames[path]) {
+      return this._extensionVersionDirectoryNames[path];
+    }
+
+    let iterator = new OS.File.DirectoryIterator(path);
+    let entries = [];
+    await iterator.forEach(async entry => {
+      if (entry.isDir) {
+        entries.push(entry.name);
+      }
+    }).catch(ex => {
+      Cu.reportError(ex);
+      entries = [];
+    });
+    // The directory name is the version number string of the extension.
+    // For example, it could be "1.0_0", "1.0_1", "1.0_2", 1.1_0, 1.1_1, or 1.1_2.
+    // The "1.0_1" strings mean that the "1.0_0" directory is existed and you install the version 1.0 again.
+    // https://chromium.googlesource.com/chromium/src/+/0b58a813992b539a6b555ad7959adfad744b095a/chrome/common/extensions/extension_file_util_unittest.cc
+    entries.sort((a, b) => Services.vc.compare(b, a));
+
+    this._extensionVersionDirectoryNames[path] = entries;
+    return entries;
+  },
+};
--- a/browser/components/migration/ChromeProfileMigrator.js
+++ b/browser/components/migration/ChromeProfileMigrator.js
@@ -15,49 +15,28 @@ const S100NS_PER_MS = 10;
 
 const AUTH_TYPE = {
   SCHEME_HTML: 0,
   SCHEME_BASIC: 1,
   SCHEME_DIGEST: 2
 };
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
-Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/ChromeMigrationUtils.jsm");
 Cu.import("resource:///modules/MigrationUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
                                   "resource://gre/modules/OSCrypto.jsm");
-/**
- * Get an nsIFile instance representing the expected location of user data
- * for this copy of Chrome/Chromium/Canary on different OSes.
- * @param subfoldersWin {Array} an array of subfolders to use for Windows
- * @param subfoldersOSX {Array} an array of subfolders to use for OS X
- * @param subfoldersUnix {Array} an array of subfolders to use for *nix systems
- * @returns {nsIFile} the place we expect data to live. Might not actually exist!
- */
-function getDataFolder(subfoldersWin, subfoldersOSX, subfoldersUnix) {
-  let dirServiceID, subfolders;
-  if (AppConstants.platform == "win") {
-    dirServiceID = "LocalAppData";
-    subfolders = subfoldersWin.concat(["User Data"]);
-  } else if (AppConstants.platform == "macosx") {
-    dirServiceID = "ULibDir";
-    subfolders = ["Application Support"].concat(subfoldersOSX);
-  } else {
-    dirServiceID = "Home";
-    subfolders = [".config"].concat(subfoldersUnix);
-  }
-  return FileUtils.getDir(dirServiceID, subfolders, false);
-}
 
 /**
  * Convert Chrome time format to Date object
  *
  * @param   aTime
  *          Chrome time
  * @return  converted Date object
  * @note    Google Chrome uses FILETIME / 10 as time.
@@ -108,18 +87,18 @@ function convertBookmarks(items, errorAc
       Cu.reportError(ex);
       errorAccumulator(ex);
     }
   }
   return itemsToInsert;
 }
 
 function ChromeProfileMigrator() {
-  let chromeUserDataFolder =
-    getDataFolder(["Google", "Chrome"], ["Google", "Chrome"], ["google-chrome"]);
+  let path = ChromeMigrationUtils.getDataPath("Chrome");
+  let chromeUserDataFolder = new FileUtils.File(path);
   this._chromeUserDataFolder = chromeUserDataFolder.exists() ?
     chromeUserDataFolder : null;
 }
 
 ChromeProfileMigrator.prototype = Object.create(MigratorPrototype);
 
 ChromeProfileMigrator.prototype.getResources =
   function Chrome_getResources(aProfile) {
@@ -166,29 +145,17 @@ Object.defineProperty(ChromeProfileMigra
     if ("__sourceProfiles" in this)
       return this.__sourceProfiles;
 
     if (!this._chromeUserDataFolder)
       return [];
 
     let profiles = [];
     try {
-      // Local State is a JSON file that contains profile info.
-      let localState = this._chromeUserDataFolder.clone();
-      localState.append("Local State");
-      if (!localState.exists())
-        throw new Error("Chrome's 'Local State' file does not exist.");
-      if (!localState.isReadable())
-        throw new Error("Chrome's 'Local State' file could not be read.");
-
-      let fstream = Cc[FILE_INPUT_STREAM_CID].createInstance(Ci.nsIFileInputStream);
-      fstream.init(localState, -1, 0, 0);
-      let inputStream = NetUtil.readInputStreamToString(fstream, fstream.available(),
-                                                        { charset: "UTF-8" });
-      let info_cache = JSON.parse(inputStream).profile.info_cache;
+      let info_cache = ChromeMigrationUtils.getLocalState().profile.info_cache;
       for (let profileFolderName in info_cache) {
         let profileFolder = this._chromeUserDataFolder.clone();
         profileFolder.append(profileFolderName);
         profiles.push({
           id: profileFolderName,
           name: info_cache[profileFolderName].name || profileFolderName,
         });
       }
@@ -498,33 +465,35 @@ ChromeProfileMigrator.prototype.classDes
 ChromeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome";
 ChromeProfileMigrator.prototype.classID = Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}");
 
 
 /**
  *  Chromium migration
  **/
 function ChromiumProfileMigrator() {
-  let chromiumUserDataFolder = getDataFolder(["Chromium"], ["Chromium"], ["chromium"]);
+  let path = ChromeMigrationUtils.getDataPath("Chromium");
+  let chromiumUserDataFolder = new FileUtils.File(path);
   this._chromeUserDataFolder = chromiumUserDataFolder.exists() ? chromiumUserDataFolder : null;
 }
 
 ChromiumProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
 ChromiumProfileMigrator.prototype.classDescription = "Chromium Profile Migrator";
 ChromiumProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chromium";
 ChromiumProfileMigrator.prototype.classID = Components.ID("{8cece922-9720-42de-b7db-7cef88cb07ca}");
 
 var componentsArray = [ChromeProfileMigrator, ChromiumProfileMigrator];
 
 /**
  * Chrome Canary
  * Not available on Linux
  **/
 function CanaryProfileMigrator() {
-  let chromeUserDataFolder = getDataFolder(["Google", "Chrome SxS"], ["Google", "Chrome Canary"]);
+  let path = ChromeMigrationUtils.getDataPath("Canary");
+  let chromeUserDataFolder = new FileUtils.File(path);
   this._chromeUserDataFolder = chromeUserDataFolder.exists() ? chromeUserDataFolder : null;
 }
 CanaryProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
 CanaryProfileMigrator.prototype.classDescription = "Chrome Canary Profile Migrator";
 CanaryProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=canary";
 CanaryProfileMigrator.prototype.classID = Components.ID("{4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}");
 
 if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
--- a/browser/components/migration/moz.build
+++ b/browser/components/migration/moz.build
@@ -25,16 +25,17 @@ EXTRA_COMPONENTS += [
 ]
 
 EXTRA_PP_COMPONENTS += [
     'BrowserProfileMigrators.manifest',
 ]
 
 EXTRA_JS_MODULES += [
     'AutoMigrate.jsm',
+    'ChromeMigrationUtils.jsm',
     'MigrationUtils.jsm',
 ]
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     SOURCES += [
         'nsIEHistoryEnumerator.cpp',
     ]
     EXTRA_COMPONENTS += [
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/_locales/en_US/messages.json	
@@ -0,0 +1,9 @@
+{
+   "description": {
+      "description": "Extension description in manifest. Should not exceed 132 characters.",
+      "message": "It is the description of fake app 1."
+   },
+   "name": {
+      "message": "Fake App 1"
+   }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-app-1/1.0_0/manifest.json	
@@ -0,0 +1,10 @@
+{
+   "app": {
+      "launch": {
+         "local_path": "main.html"
+      }
+   },
+   "default_locale": "en_US",
+   "description": "__MSG_description__",
+   "name": "__MSG_name__"
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/_locales/en_US/messages.json	
@@ -0,0 +1,9 @@
+{
+   "description": {
+      "description": "Extension description in manifest. Should not exceed 132 characters.",
+      "message": "It is the description of fake extension 1."
+   },
+   "name": {
+      "message": "Fake Extension 1"
+   }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-1/1.0_0/manifest.json	
@@ -0,0 +1,5 @@
+{
+   "default_locale": "en_US",
+   "description": "__MSG_description__",
+   "name": "__MSG_name__"
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Extensions/fake-extension-2/1.0_0/manifest.json	
@@ -0,0 +1,5 @@
+{
+   "default_locale": "en_US",
+   "description": "It is the description of fake extension 2.",
+   "name": "Fake Extension 2"
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils.js
@@ -0,0 +1,49 @@
+"use strict";
+
+var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource:///modules/ChromeMigrationUtils.jsm");
+
+// Setup chrome user data path for all platforms.
+ChromeMigrationUtils.getChromeUserDataPath = () => {
+  return do_get_file("Library/Application Support/Google/Chrome/").path;
+};
+
+add_task(async function test_getExtensionList_function() {
+  let extensionList = await ChromeMigrationUtils.getExtensionList("Default");
+  Assert.equal(extensionList.length, 2, "There should be 2 extensions installed.");
+  Assert.deepEqual(extensionList.find(extension => extension.id == "fake-extension-1"), {
+    id: "fake-extension-1",
+    name: "Fake Extension 1",
+    description: "It is the description of fake extension 1.",
+  }, "First extension should match expectations.");
+  Assert.deepEqual(extensionList.find(extension => extension.id == "fake-extension-2"), {
+    id: "fake-extension-2",
+    name: "Fake Extension 2",
+    description: "It is the description of fake extension 2.",
+  }, "Second extension should match expectations.");
+});
+
+add_task(async function test_getExtensionInformation_function() {
+  let extension = await ChromeMigrationUtils.getExtensionInformation("fake-extension-1", "Default");
+  Assert.deepEqual(extension, {
+    id: "fake-extension-1",
+    name: "Fake Extension 1",
+    description: "It is the description of fake extension 1.",
+  }, "Should get the extension information correctly.");
+});
+
+add_task(async function test_getLocaleString_function() {
+  let name = await ChromeMigrationUtils._getLocaleString("__MSG_name__", "en_US", "fake-extension-1", "Default");
+  Assert.deepEqual(name, "Fake Extension 1", "The value of __MSG_name__ locale key is Fake Extension 1.");
+});
+
+add_task(async function test_isExtensionInstalled_function() {
+  let isInstalled = await ChromeMigrationUtils.isExtensionInstalled("fake-extension-1", "Default");
+  Assert.ok(isInstalled, "The fake-extension-1 extension should be installed.");
+});
+
+add_task(async function test_getLastUsedProfileId_function() {
+  let profileId = ChromeMigrationUtils.getLastUsedProfileId();
+  Assert.equal(profileId, "Default", "The last used profile ID should be Default.");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_ChromeMigrationUtils_path.js
@@ -0,0 +1,81 @@
+"use strict";
+
+var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource:///modules/ChromeMigrationUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+function getRootPath() {
+  let dirKey;
+  if (AppConstants.platform == "win") {
+    dirKey = "winLocalAppDataDir";
+  } else if (AppConstants.platform == "macosx") {
+    dirKey = "macUserLibDir";
+  } else {
+    dirKey = "homeDir";
+  }
+  return OS.Constants.Path[dirKey];
+}
+
+add_task(async function test_getDataPath_function() {
+  let chromeUserDataPath = ChromeMigrationUtils.getDataPath("Chrome");
+  let chromiumUserDataPath = ChromeMigrationUtils.getDataPath("Chromium");
+  let canaryUserDataPath = ChromeMigrationUtils.getDataPath("Canary");
+  if (AppConstants.platform == "win") {
+    Assert.equal(chromeUserDataPath,
+      OS.Path.join(getRootPath(), "Google", "Chrome", "User Data"),
+      "Should get the path of Chrome data directory.");
+    Assert.equal(chromiumUserDataPath,
+      OS.Path.join(getRootPath(), "Chromium", "User Data"),
+      "Should get the path of Chromium data directory.");
+    Assert.equal(canaryUserDataPath,
+      OS.Path.join(getRootPath(), "Google", "Chrome SxS", "User Data"),
+      "Should get the path of Canary data directory.");
+  } else if (AppConstants.platform == "macosx") {
+    Assert.equal(chromeUserDataPath,
+      OS.Path.join(getRootPath(), "Application Support", "Google", "Chrome"),
+      "Should get the path of Chrome data directory.");
+    Assert.equal(chromiumUserDataPath,
+      OS.Path.join(getRootPath(), "Application Support", "Chromium"),
+      "Should get the path of Chromium data directory.");
+    Assert.equal(canaryUserDataPath,
+      OS.Path.join(getRootPath(), "Application Support", "Google", "Chrome Canary"),
+      "Should get the path of Canary data directory.");
+  } else {
+    Assert.equal(chromeUserDataPath,
+      OS.Path.join(getRootPath(), ".config", "google-chrome"),
+      "Should get the path of Chrome data directory.");
+    Assert.equal(chromiumUserDataPath,
+      OS.Path.join(getRootPath(), ".config", "chromium"),
+      "Should get the path of Chromium data directory.");
+    Assert.equal(canaryUserDataPath, null,
+      "Should get null for Canary.");
+  }
+});
+
+add_task(async function test_getChromeUserDataPath_function() {
+  let chromeUserDataPath = ChromeMigrationUtils.getChromeUserDataPath();
+  let expectedPath;
+  if (AppConstants.platform == "win") {
+    expectedPath = OS.Path.join(getRootPath(), "Google", "Chrome", "User Data");
+  } else if (AppConstants.platform == "macosx") {
+    expectedPath = OS.Path.join(getRootPath(), "Application Support", "Google", "Chrome");
+  } else {
+    expectedPath = OS.Path.join(getRootPath(), ".config", "google-chrome");
+  }
+  Assert.equal(chromeUserDataPath, expectedPath, "Should get the path of Chrome user data directory.");
+});
+
+add_task(async function test_getExtensionPath_function() {
+  let extensionPath = ChromeMigrationUtils.getExtensionPath("Default");
+  let expectedPath;
+  if (AppConstants.platform == "win") {
+    expectedPath = OS.Path.join(getRootPath(), "Google", "Chrome", "User Data", "Default", "Extensions");
+  } else if (AppConstants.platform == "macosx") {
+    expectedPath = OS.Path.join(getRootPath(), "Application Support", "Google", "Chrome", "Default", "Extensions");
+  } else {
+    expectedPath = OS.Path.join(getRootPath(), ".config", "google-chrome", "Default", "Extensions");
+  }
+  Assert.equal(extensionPath, expectedPath, "Should get the path of extensions directory.");
+});
--- a/browser/components/migration/tests/unit/xpcshell.ini
+++ b/browser/components/migration/tests/unit/xpcshell.ini
@@ -9,16 +9,18 @@ support-files =
 [test_360se_bookmarks.js]
 skip-if = os != "win"
 [test_automigration.js]
 [test_Chrome_bookmarks.js]
 [test_Chrome_cookies.js]
 skip-if = os != "mac" # Relies on ULibDir
 [test_Chrome_passwords.js]
 skip-if = os != "win"
+[test_ChromeMigrationUtils.js]
+[test_ChromeMigrationUtils_path.js]
 [test_Edge_db_migration.js]
 skip-if = os != "win"
 [test_fx_telemetry.js]
 [test_IE_bookmarks.js]
 skip-if = !(os == "win" && bits == 64) # bug 1392396
 [test_IE_cookies.js]
 skip-if = os != "win"
 [test_IE7_passwords.js]
--- a/toolkit/components/osfile/modules/osfile_async_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_async_front.jsm
@@ -83,16 +83,17 @@ function lazyPathGetter(constProp, dirKe
   };
 }
 
 for (let [constProp, dirKey] of [
   ["localProfileDir", "ProfLD"],
   ["profileDir", "ProfD"],
   ["userApplicationDataDir", "UAppData"],
   ["winAppDataDir", "AppData"],
+  ["winLocalAppDataDir", "LocalAppData"],
   ["winStartMenuProgsDir", "Progs"],
   ]) {
 
   if (constProp in SharedAll.Constants.Path) {
     continue;
   }
 
   LOG("Installing lazy getter for OS.Constants.Path." + constProp +