Bug 1193387 - import reading list from MS Edge, r?jaws draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Wed, 26 Aug 2015 15:34:02 +0100
changeset 287984 c32fc20982b1d61571b4be066fd5b63416738e89
parent 287983 42d684a76eceecb28f5da0722de4bcc8b4217a0e
child 508708 dd6595799c0e6e753a3d4c7c34ce0d58e410797c
push id4775
push usergijskruitbosch@gmail.com
push dateWed, 26 Aug 2015 15:00:41 +0000
reviewersjaws
bugs1193387
milestone43.0a1
Bug 1193387 - import reading list from MS Edge, r?jaws
browser/components/build/moz.build
browser/components/build/nsBrowserCompsCID.h
browser/components/build/nsModule.cpp
browser/components/migration/EdgeProfileMigrator.js
browser/components/migration/IEProfileMigrator.js
browser/components/migration/MSMigrationUtils.jsm
browser/components/migration/moz.build
browser/components/migration/nsEdgeReadingListExtractor.cpp
browser/components/migration/nsEdgeReadingListExtractor.h
browser/components/migration/nsIEHistoryEnumerator.cpp
browser/components/migration/nsIEdgeReadingListExtractor.idl
browser/components/migration/nsWindowsMigrationUtils.h
browser/locales/en-US/chrome/browser/migration/migration.properties
--- a/browser/components/build/moz.build
+++ b/browser/components/build/moz.build
@@ -19,16 +19,17 @@ LOCAL_INCLUDES += [
     '../dirprovider',
     '../feeds',
     '../migration',
     '../shell',
 ]
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     OS_LIBS += [
+        'esent',
         'ole32',
         'shell32',
         'shlwapi',
         'version',
     ]
 
 # Mac: Need to link with CoreFoundation for Mac Migrators (PList reading code)
 # GTK2: Need to link with glib for GNOME shell service
--- a/browser/components/build/nsBrowserCompsCID.h
+++ b/browser/components/build/nsBrowserCompsCID.h
@@ -5,16 +5,22 @@
 /////////////////////////////////////////////////////////////////////////////
 
 #ifdef XP_WIN
 #define NS_WINIEHISTORYENUMERATOR_CID \
 { 0x93480624, 0x806e, 0x4756, { 0xb7, 0xcb, 0x0f, 0xb7, 0xdd, 0x74, 0x6a, 0x8f } }
 
 #define NS_IEHISTORYENUMERATOR_CONTRACTID \
   "@mozilla.org/profile/migrator/iehistoryenumerator;1"
+
+#define NS_EDGEREADINGLISTEXTRACTOR_CID \
+{ 0xeeff77b1, 0xdb98, 0x4241, { 0x94, 0x36, 0x14, 0xf7, 0xa2, 0x28, 0x84, 0xc1 } }
+
+#define NS_EDGEREADINGLISTEXTRACTOR_CONTRACTID \
+  "@mozilla.org/profile/migrator/edgereadinglistextractor;1"
 #endif
 
 #define NS_SHELLSERVICE_CID \
 { 0x63c7b9f4, 0xcc8, 0x43f8, { 0xb6, 0x66, 0xa, 0x66, 0x16, 0x55, 0xcb, 0x73 } }
 
 #define NS_SHELLSERVICE_CONTRACTID \
   "@mozilla.org/browser/shell-service;1"
 
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -13,16 +13,17 @@
 #elif defined(XP_MACOSX)
 #include "nsMacShellService.h"
 #elif defined(MOZ_WIDGET_GTK)
 #include "nsGNOMEShellService.h"
 #endif
 
 #if defined(XP_WIN)
 #include "nsIEHistoryEnumerator.h"
+#include "nsEdgeReadingListExtractor.h"
 #endif
 
 #include "rdf.h"
 #include "nsFeedSniffer.h"
 #include "AboutRedirector.h"
 #include "nsIAboutModule.h"
 
 #include "nsNetCID.h"
@@ -37,45 +38,48 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsWindows
 #elif defined(XP_MACOSX)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsMacShellService)
 #elif defined(MOZ_WIDGET_GTK)
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsGNOMEShellService, Init)
 #endif
 
 #if defined(XP_WIN)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsIEHistoryEnumerator)
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsEdgeReadingListExtractor)
 #endif
 
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsFeedSniffer)
 
 NS_DEFINE_NAMED_CID(NS_BROWSERDIRECTORYPROVIDER_CID);
 #if defined(XP_WIN)
 NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID);
 #elif defined(MOZ_WIDGET_GTK)
 NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID);
 #endif
 NS_DEFINE_NAMED_CID(NS_FEEDSNIFFER_CID);
 NS_DEFINE_NAMED_CID(NS_BROWSER_ABOUT_REDIRECTOR_CID);
 #if defined(XP_WIN)
 NS_DEFINE_NAMED_CID(NS_WINIEHISTORYENUMERATOR_CID);
+NS_DEFINE_NAMED_CID(NS_EDGEREADINGLISTEXTRACTOR_CID);
 #elif defined(XP_MACOSX)
 NS_DEFINE_NAMED_CID(NS_SHELLSERVICE_CID);
 #endif
 
 static const mozilla::Module::CIDEntry kBrowserCIDs[] = {
     { &kNS_BROWSERDIRECTORYPROVIDER_CID, false, nullptr, DirectoryProviderConstructor },
 #if defined(XP_WIN)
     { &kNS_SHELLSERVICE_CID, false, nullptr, nsWindowsShellServiceConstructor },
 #elif defined(MOZ_WIDGET_GTK)
     { &kNS_SHELLSERVICE_CID, false, nullptr, nsGNOMEShellServiceConstructor },
 #endif
     { &kNS_FEEDSNIFFER_CID, false, nullptr, nsFeedSnifferConstructor },
     { &kNS_BROWSER_ABOUT_REDIRECTOR_CID, false, nullptr, AboutRedirector::Create },
 #if defined(XP_WIN)
     { &kNS_WINIEHISTORYENUMERATOR_CID, false, nullptr, nsIEHistoryEnumeratorConstructor },
+    { &kNS_EDGEREADINGLISTEXTRACTOR_CID, false, nullptr, nsEdgeReadingListExtractorConstructor },
 #elif defined(XP_MACOSX)
     { &kNS_SHELLSERVICE_CID, false, nullptr, nsMacShellServiceConstructor },
 #endif
     { nullptr }
 };
 
 static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
     { NS_BROWSERDIRECTORYPROVIDER_CONTRACTID, &kNS_BROWSERDIRECTORYPROVIDER_CID },
@@ -114,16 +118,17 @@ static const mozilla::Module::ContractID
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "customizing", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "looppanel", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "loopconversation", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "reader", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "pocket-saved", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "pocket-signup", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #if defined(XP_WIN)
     { NS_IEHISTORYENUMERATOR_CONTRACTID, &kNS_WINIEHISTORYENUMERATOR_CID },
+    { NS_EDGEREADINGLISTEXTRACTOR_CONTRACTID, &kNS_EDGEREADINGLISTEXTRACTOR_CID },
 #elif defined(XP_MACOSX)
     { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
 #endif
     { nullptr }
 };
 
 static const mozilla::Module::CategoryEntry kBrowserCategories[] = {
     { XPCOM_DIRECTORY_PROVIDER_CATEGORY, "browser-directory-provider", NS_BROWSERDIRECTORYPROVIDER_CONTRACTID },
--- a/browser/components/migration/EdgeProfileMigrator.js
+++ b/browser/components/migration/EdgeProfileMigrator.js
@@ -1,30 +1,30 @@
-/* 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/. */
-
-const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource:///modules/MigrationUtils.jsm");
-Cu.import("resource:///modules/MSMigrationUtils.jsm");
-
-function EdgeProfileMigrator() {
-}
-
-EdgeProfileMigrator.prototype = Object.create(MigratorPrototype);
-
-EdgeProfileMigrator.prototype.getResources = function() {
-  let resources = [
-    MSMigrationUtils.getBookmarksMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE),
-    MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE),
-  ];
-  return resources.filter(r => r.exists);
-};
-
-EdgeProfileMigrator.prototype.classDescription = "Edge Profile Migrator";
-EdgeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=edge";
-EdgeProfileMigrator.prototype.classID = Components.ID("{62e8834b-2d17-49f5-96ff-56344903a2ae}");
-
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([EdgeProfileMigrator]);
+/* 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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource:///modules/MigrationUtils.jsm");
+Cu.import("resource:///modules/MSMigrationUtils.jsm");
+
+function EdgeProfileMigrator() {
+}
+
+EdgeProfileMigrator.prototype = Object.create(MigratorPrototype);
+
+EdgeProfileMigrator.prototype.getResources = function() {
+  let resources = [
+    MSMigrationUtils.getBookmarksMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE),
+    MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE),
+  ];
+  return resources.filter(r => r.exists);
+};
+
+EdgeProfileMigrator.prototype.classDescription = "Edge Profile Migrator";
+EdgeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=edge";
+EdgeProfileMigrator.prototype.classID = Components.ID("{62e8834b-2d17-49f5-96ff-56344903a2ae}");
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([EdgeProfileMigrator]);
--- a/browser/components/migration/IEProfileMigrator.js
+++ b/browser/components/migration/IEProfileMigrator.js
@@ -79,17 +79,18 @@ History.prototype = {
       if (title.length == 0) {
         continue;
       }
 
       // The typed urls are already fixed-up, so we can use them for comparison.
       let transitionType = this._typedURLs[uri.spec] ?
                              Ci.nsINavHistoryService.TRANSITION_TYPED :
                              Ci.nsINavHistoryService.TRANSITION_LINK;
-      let lastVisitTime = entry.get("time");
+      // use the current date if we have no visits for this entry
+      let lastVisitTime = entry.get("time") || Date.now();
 
       places.push(
         { uri: uri,
           title: title,
           visits: [{ transitionType: transitionType,
                      visitDate: lastVisitTime }]
         }
       );
--- a/browser/components/migration/MSMigrationUtils.jsm
+++ b/browser/components/migration/MSMigrationUtils.jsm
@@ -1,485 +1,560 @@
-/* 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 = ["MSMigrationUtils"];
-
-const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource:///modules/MigrationUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
-                                  "resource://gre/modules/PlacesUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
-                                  "resource://gre/modules/WindowsRegistry.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
-                                  "resource://gre/modules/ctypes.jsm");
-
-const EDGE_COOKIE_PATH_OPTIONS = ["", "#!001\\", "#!002\\"];
-const EDGE_COOKIES_SUFFIX = "MicrosoftEdge\\Cookies";
-const EDGE_FAVORITES = "AC\\MicrosoftEdge\\User\\Default\\Favorites";
-
-Cu.importGlobalProperties(["File"]);
-
-////////////////////////////////////////////////////////////////////////////////
-//// Helpers.
-
-let CtypesHelpers = {
-  _structs: {},
-  _functions: {},
-  _libs: {},
-
-  /**
-   * Must be invoked once before first use of any of the provided helpers.
-   */
-  initialize() {
-    const WORD = ctypes.uint16_t;
-    const DWORD = ctypes.uint32_t;
-    const BOOL = ctypes.int;
-
-    this._structs.SYSTEMTIME = new ctypes.StructType('SYSTEMTIME', [
-      {wYear: WORD},
-      {wMonth: WORD},
-      {wDayOfWeek: WORD},
-      {wDay: WORD},
-      {wHour: WORD},
-      {wMinute: WORD},
-      {wSecond: WORD},
-      {wMilliseconds: WORD}
-    ]);
-
-    this._structs.FILETIME = new ctypes.StructType('FILETIME', [
-      {dwLowDateTime: DWORD},
-      {dwHighDateTime: DWORD}
-    ]);
-
-    try {
-      this._libs.kernel32 = ctypes.open("Kernel32");
-      this._functions.FileTimeToSystemTime =
-        this._libs.kernel32.declare("FileTimeToSystemTime",
-                                    ctypes.default_abi,
-                                    BOOL,
-                                    this._structs.FILETIME.ptr,
-                                    this._structs.SYSTEMTIME.ptr);
-    } catch (ex) {
-      this.finalize();
-    }
-  },
-
-  /**
-   * Must be invoked once after last use of any of the provided helpers.
-   */
-  finalize() {
-    this._structs = {};
-    this._functions = {};
-    for each (let lib in this._libs) {
-      try {
-        lib.close();
-      } catch (ex) {}
-    }
-    this._libs = {};
-  },
-
-  /**
-   * Converts a FILETIME struct (2 DWORDS), to a SYSTEMTIME struct,
-   * and then deduces the number of seconds since the epoch (which
-   * is the data we want for the cookie expiry date).
-   *
-   * @param aTimeHi
-   *        Least significant DWORD.
-   * @param aTimeLo
-   *        Most significant DWORD.
-   * @return the number of seconds since the epoch
-   */
-  fileTimeToSecondsSinceEpoch(aTimeHi, aTimeLo) {
-    let fileTime = this._structs.FILETIME();
-    fileTime.dwLowDateTime = aTimeLo;
-    fileTime.dwHighDateTime = aTimeHi;
-    let systemTime = this._structs.SYSTEMTIME();
-    let result = this._functions.FileTimeToSystemTime(fileTime.address(),
-                                                      systemTime.address());
-    if (result == 0)
-      throw new Error(ctypes.winLastError);
-
-    // System time is in UTC, so we use Date.UTC to get milliseconds from epoch,
-    // then divide by 1000 to get seconds, and round down:
-    return Math.floor(Date.UTC(systemTime.wYear,
-                               systemTime.wMonth - 1,
-                               systemTime.wDay,
-                               systemTime.wHour,
-                               systemTime.wMinute,
-                               systemTime.wSecond,
-                               systemTime.wMilliseconds) / 1000);
-  }
-};
-
-/**
- * Checks whether an host is an IP (v4 or v6) address.
- *
- * @param aHost
- *        The host to check.
- * @return whether aHost is an IP address.
- */
-function hostIsIPAddress(aHost) {
-  try {
-    Services.eTLD.getBaseDomainFromHost(aHost);
-  } catch (e if e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
-    return true;
-  } catch (e) {}
-  return false;
-}
-
-let gEdgeDir;
-function getEdgeLocalDataFolder() {
-  if (gEdgeDir) {
-    return gEdgeDir.clone();
-  }
-  let packages = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
-  packages.append("Packages");
-  let edgeDir = packages.clone();
-  edgeDir.append("Microsoft.MicrosoftEdge_8wekyb3d8bbwe");
-  try {
-    if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
-      gEdgeDir = edgeDir;
-      return edgeDir.clone();
+/* 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 = ["MSMigrationUtils"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource:///modules/MigrationUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
+                                  "resource://gre/modules/WindowsRegistry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
+                                  "resource://gre/modules/ctypes.jsm");
+
+const EDGE_COOKIE_PATH_OPTIONS = ["", "#!001\\", "#!002\\"];
+const EDGE_COOKIES_SUFFIX = "MicrosoftEdge\\Cookies";
+const EDGE_FAVORITES = "AC\\MicrosoftEdge\\User\\Default\\Favorites";
+const EDGE_READINGLIST = "AC\\MicrosoftEdge\\User\\Default\\DataStore\\Data\\";
+
+Cu.importGlobalProperties(["File"]);
+
+////////////////////////////////////////////////////////////////////////////////
+//// Helpers.
+
+let CtypesHelpers = {
+  _structs: {},
+  _functions: {},
+  _libs: {},
+
+  /**
+   * Must be invoked once before first use of any of the provided helpers.
+   */
+  initialize() {
+    const WORD = ctypes.uint16_t;
+    const DWORD = ctypes.uint32_t;
+    const BOOL = ctypes.int;
+
+    this._structs.SYSTEMTIME = new ctypes.StructType('SYSTEMTIME', [
+      {wYear: WORD},
+      {wMonth: WORD},
+      {wDayOfWeek: WORD},
+      {wDay: WORD},
+      {wHour: WORD},
+      {wMinute: WORD},
+      {wSecond: WORD},
+      {wMilliseconds: WORD}
+    ]);
+
+    this._structs.FILETIME = new ctypes.StructType('FILETIME', [
+      {dwLowDateTime: DWORD},
+      {dwHighDateTime: DWORD}
+    ]);
+
+    try {
+      this._libs.kernel32 = ctypes.open("Kernel32");
+      this._functions.FileTimeToSystemTime =
+        this._libs.kernel32.declare("FileTimeToSystemTime",
+                                    ctypes.default_abi,
+                                    BOOL,
+                                    this._structs.FILETIME.ptr,
+                                    this._structs.SYSTEMTIME.ptr);
+    } catch (ex) {
+      this.finalize();
+    }
+  },
+
+  /**
+   * Must be invoked once after last use of any of the provided helpers.
+   */
+  finalize() {
+    this._structs = {};
+    this._functions = {};
+    for each (let lib in this._libs) {
+      try {
+        lib.close();
+      } catch (ex) {}
+    }
+    this._libs = {};
+  },
+
+  /**
+   * Converts a FILETIME struct (2 DWORDS), to a SYSTEMTIME struct,
+   * and then deduces the number of seconds since the epoch (which
+   * is the data we want for the cookie expiry date).
+   *
+   * @param aTimeHi
+   *        Least significant DWORD.
+   * @param aTimeLo
+   *        Most significant DWORD.
+   * @return the number of seconds since the epoch
+   */
+  fileTimeToSecondsSinceEpoch(aTimeHi, aTimeLo) {
+    let fileTime = this._structs.FILETIME();
+    fileTime.dwLowDateTime = aTimeLo;
+    fileTime.dwHighDateTime = aTimeHi;
+    let systemTime = this._structs.SYSTEMTIME();
+    let result = this._functions.FileTimeToSystemTime(fileTime.address(),
+                                                      systemTime.address());
+    if (result == 0)
+      throw new Error(ctypes.winLastError);
+
+    // System time is in UTC, so we use Date.UTC to get milliseconds from epoch,
+    // then divide by 1000 to get seconds, and round down:
+    return Math.floor(Date.UTC(systemTime.wYear,
+                               systemTime.wMonth - 1,
+                               systemTime.wDay,
+                               systemTime.wHour,
+                               systemTime.wMinute,
+                               systemTime.wSecond,
+                               systemTime.wMilliseconds) / 1000);
+  }
+};
+
+/**
+ * Checks whether an host is an IP (v4 or v6) address.
+ *
+ * @param aHost
+ *        The host to check.
+ * @return whether aHost is an IP address.
+ */
+function hostIsIPAddress(aHost) {
+  try {
+    Services.eTLD.getBaseDomainFromHost(aHost);
+  } catch (e if e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS) {
+    return true;
+  } catch (e) {}
+  return false;
+}
+
+let gEdgeDir;
+function getEdgeLocalDataFolder() {
+  if (gEdgeDir) {
+    return gEdgeDir.clone();
+  }
+  let packages = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
+  packages.append("Packages");
+  let edgeDir = packages.clone();
+  edgeDir.append("Microsoft.MicrosoftEdge_8wekyb3d8bbwe");
+  try {
+    if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
+      gEdgeDir = edgeDir;
+      return edgeDir.clone();
+    }
+
+    // Let's try the long way:
+    let dirEntries = packages.directoryEntries;
+    while (dirEntries.hasMoreElements()) {
+      let subDir = dirEntries.getNext();
+      subDir.QueryInterface(Ci.nsIFile);
+      if (subDir.leafName.startsWith("Microsoft.MicrosoftEdge") && subDir.isReadable() &&
+          subDir.isDirectory()) {
+        gEdgeDir = subDir;
+        return subDir.clone();
+      }
+    }
+  } catch (ex) {
+    Cu.reportError("Exception trying to find the Edge favorites directory: " + ex);
+  }
+  return null;
+}
+
+
+function Bookmarks(migrationType) {
+  this._migrationType = migrationType;
+}
+
+Bookmarks.prototype = {
+  type: MigrationUtils.resourceTypes.BOOKMARKS,
+
+  get exists() !!this._favoritesFolder,
+
+  get importedAppLabel() this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE ? "IE" : "Edge",
+
+  __favoritesFolder: null,
+  get _favoritesFolder() {
+    if (!this.__favoritesFolder) {
+      if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
+        let favoritesFolder = Services.dirsvc.get("Favs", Ci.nsIFile);
+        if (favoritesFolder.exists() && favoritesFolder.isReadable())
+          return this.__favoritesFolder = favoritesFolder;
+      }
+      if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE) {
+        let edgeDir = getEdgeLocalDataFolder();
+        if (edgeDir) {
+          edgeDir.appendRelativePath(EDGE_FAVORITES);
+          if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
+            return this.__favoritesFolder = edgeDir;
+          }
+        }
+      }
+    }
+    return this.__favoritesFolder;
+  },
+
+  __toolbarFolderName: null,
+  get _toolbarFolderName() {
+    if (!this.__toolbarFolderName) {
+      if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
+        // Retrieve the name of IE's favorites subfolder that holds the bookmarks
+        // in the toolbar. This was previously stored in the registry and changed
+        // in IE7 to always be called "Links".
+        let folderName = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+                                                    "Software\\Microsoft\\Internet Explorer\\Toolbar",
+                                                    "LinksFolderName");
+        this.__toolbarFolderName = folderName || "Links";
+      } else {
+        this.__toolbarFolderName = "Links";
+      }
+    }
+    return this.__toolbarFolderName;
+  },
+
+  migrate: function B_migrate(aCallback) {
+    return Task.spawn(function* () {
+      // Import to the bookmarks menu.
+      let folderGuid = PlacesUtils.bookmarks.menuGuid;
+      if (!MigrationUtils.isStartupMigration) {
+        folderGuid =
+          yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid);
+      }
+      yield this._migrateFolder(this._favoritesFolder, folderGuid);
+
+      if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE) {
+        yield this._migrateEdgeReadingList(PlacesUtils.bookmarks.menuGuid);
+      }
+    }.bind(this)).then(() => aCallback(true),
+                        e => { Cu.reportError(e); aCallback(false) });
+  },
+
+  _migrateFolder: Task.async(function* (aSourceFolder, aDestFolderGuid) {
+    // TODO (bug 741993): the favorites order is stored in the Registry, at
+    // HCU\Software\Microsoft\Windows\CurrentVersion\Explorer\MenuOrder\Favorites
+    // for IE, and in a similar location for Edge.
+    // Until we support it, bookmarks are imported in alphabetical order.
+    let entries = aSourceFolder.directoryEntries;
+    while (entries.hasMoreElements()) {
+      let entry = entries.getNext().QueryInterface(Ci.nsIFile);
+      try {
+        // Make sure that entry.path == entry.target to not follow .lnk folder
+        // shortcuts which could lead to infinite cycles.
+        // Don't use isSymlink(), since it would throw for invalid
+        // lnk files pointing to URLs or to unresolvable paths.
+        if (entry.path == entry.target && entry.isDirectory()) {
+          let folderGuid;
+          if (entry.leafName == this._toolbarFolderName &&
+              entry.parent.equals(this._favoritesFolder)) {
+            // Import to the bookmarks toolbar.
+            folderGuid = PlacesUtils.bookmarks.toolbarGuid;
+            if (!MigrationUtils.isStartupMigration) {
+              folderGuid =
+                yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid);
+            }
+          }
+          else {
+            // Import to a new folder.
+            folderGuid = (yield PlacesUtils.bookmarks.insert({
+              type: PlacesUtils.bookmarks.TYPE_FOLDER,
+              parentGuid: aDestFolderGuid,
+              title: entry.leafName
+            })).guid;
+          }
+
+          if (entry.isReadable()) {
+            // Recursively import the folder.
+            yield this._migrateFolder(entry, folderGuid);
+          }
+        }
+        else {
+          // Strip the .url extension, to both check this is a valid link file,
+          // and get the associated title.
+          let matches = entry.leafName.match(/(.+)\.url$/i);
+          if (matches) {
+            let fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"].
+                              getService(Ci.nsIFileProtocolHandler);
+            let uri = fileHandler.readURLFile(entry);
+            let title = matches[1];
+
+            yield PlacesUtils.bookmarks.insert({
+              parentGuid: aDestFolderGuid, url: uri, title
+            });
+          }
+        }
+      } catch (ex) {
+        Components.utils.reportError("Unable to import " + this.importedAppLabel + " favorite (" + entry.leafName + "): " + ex);
+      }
+    }
+  }),
+
+  _migrateEdgeReadingList: Task.async(function*(parentGuid) {
+    let edgeDir = getEdgeLocalDataFolder();
+    if (!edgeDir) {
+      return;
     }
 
-    // Let's try the long way:
-    let dirEntries = packages.directoryEntries;
-    while (dirEntries.hasMoreElements()) {
-      let subDir = dirEntries.getNext();
-      subDir.QueryInterface(Ci.nsIFile);
-      if (subDir.leafName.startsWith("Microsoft.MicrosoftEdge") && subDir.isReadable() &&
-          subDir.isDirectory()) {
-        gEdgeDir = subDir;
-        return subDir.clone();
-      }
-    }
-  } catch (ex) {
-    Cu.reportError("Exception trying to find the Edge favorites directory: " + ex);
-  }
-  return null;
-}
-
-
-function Bookmarks(migrationType) {
-  this._migrationType = migrationType;
-}
-
-Bookmarks.prototype = {
-  type: MigrationUtils.resourceTypes.BOOKMARKS,
-
-  get exists() !!this._favoritesFolder,
-
-  get importedAppLabel() this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE ? "IE" : "Edge",
-
-  __favoritesFolder: null,
-  get _favoritesFolder() {
-    if (!this.__favoritesFolder) {
-      if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
-        let favoritesFolder = Services.dirsvc.get("Favs", Ci.nsIFile);
-        if (favoritesFolder.exists() && favoritesFolder.isReadable())
-          return this.__favoritesFolder = favoritesFolder;
-      }
-      if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE) {
-        let edgeDir = getEdgeLocalDataFolder();
-        if (edgeDir) {
-          edgeDir.appendRelativePath(EDGE_FAVORITES);
-          if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
-            return this.__favoritesFolder = edgeDir;
+    this._readingListExtractor = Cc["@mozilla.org/profile/migrator/edgereadinglistextractor;1"].
+                                 createInstance(Ci.nsIEdgeReadingListExtractor);
+    edgeDir.appendRelativePath(EDGE_READINGLIST);
+    if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
+      let expectedDir = edgeDir.clone();
+      expectedDir.appendRelativePath("nouser1\\120712-0049");
+      if (expectedDir.exists() && expectedDir.isReadable() && expectedDir.isDirectory()) {
+        yield this._migrateEdgeReadingListDB(expectedDir, parentGuid);
+      } else {
+        let getSubdirs = someDir => {
+          let subdirs = someDir.directoryEntries;
+          let rv = [];
+          while (subdirs.hasMoreElements()) {
+            let subdir = subdirs.getNext().QueryInterface(Ci.nsIFile);
+            if (subdir.isDirectory() && subdir.isReadable()) {
+              rv.push(subdir);
+            }
           }
+          return rv;
+        };
+        let dirs = getSubdirs(edgeDir).map(getSubdirs);
+        for (let dir of dirs) {
+          yield this._migrateEdgeReadingListDB(dir, parentGuid);
         }
       }
     }
-    return this.__favoritesFolder;
-  },
-
-  __toolbarFolderName: null,
-  get _toolbarFolderName() {
-    if (!this.__toolbarFolderName) {
-      if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
-        // Retrieve the name of IE's favorites subfolder that holds the bookmarks
-        // in the toolbar. This was previously stored in the registry and changed
-        // in IE7 to always be called "Links".
-        let folderName = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
-                                                    "Software\\Microsoft\\Internet Explorer\\Toolbar",
-                                                    "LinksFolderName");
-        this.__toolbarFolderName = folderName || "Links";
-      } else {
-        this.__toolbarFolderName = "Links";
-      }
+  }),
+  _migrateEdgeReadingListDB: Task.async(function*(dbFile, parentGuid) {
+    dbFile.appendRelativePath("DBStore\\spartan.edb");
+    if (!dbFile.exists() || !dbFile.isReadable() || !dbFile.isFile()) {
+      return;
+    }
+    let readingListItems;
+    try {
+      readingListItems = this._readingListExtractor.extract(dbFile.path);
+    } catch (ex) {
+      Cu.reportError("Failed to extract Edge reading list information from the database at " + dbPath + " due to the following error: " + ex);
+      return;
     }
-    return this.__toolbarFolderName;
-  },
-
-  migrate: function B_migrate(aCallback) {
-    return Task.spawn(function* () {
-      // Import to the bookmarks menu.
-      let folderGuid = PlacesUtils.bookmarks.menuGuid;
-      if (!MigrationUtils.isStartupMigration) {
-        folderGuid =
-          yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid);
-      }
-      yield this._migrateFolder(this._favoritesFolder, folderGuid);
-    }.bind(this)).then(() => aCallback(true),
-                        e => { Cu.reportError(e); aCallback(false) });
-  },
-
-  _migrateFolder: Task.async(function* (aSourceFolder, aDestFolderGuid) {
-    // TODO (bug 741993): the favorites order is stored in the Registry, at
-    // HCU\Software\Microsoft\Windows\CurrentVersion\Explorer\MenuOrder\Favorites
-    // for IE, and in a similar location for Edge.
-    // Until we support it, bookmarks are imported in alphabetical order.
-    let entries = aSourceFolder.directoryEntries;
-    while (entries.hasMoreElements()) {
-      let entry = entries.getNext().QueryInterface(Ci.nsIFile);
-      try {
-        // Make sure that entry.path == entry.target to not follow .lnk folder
-        // shortcuts which could lead to infinite cycles.
-        // Don't use isSymlink(), since it would throw for invalid
-        // lnk files pointing to URLs or to unresolvable paths.
-        if (entry.path == entry.target && entry.isDirectory()) {
-          let folderGuid;
-          if (entry.leafName == this._toolbarFolderName &&
-              entry.parent.equals(this._favoritesFolder)) {
-            // Import to the bookmarks toolbar.
-            folderGuid = PlacesUtils.bookmarks.toolbarGuid;
-            if (!MigrationUtils.isStartupMigration) {
-              folderGuid =
-                yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid);
-            }
-          }
-          else {
-            // Import to a new folder.
-            folderGuid = (yield PlacesUtils.bookmarks.insert({
-              type: PlacesUtils.bookmarks.TYPE_FOLDER,
-              parentGuid: aDestFolderGuid,
-              title: entry.leafName
-            })).guid;
-          }
-
-          if (entry.isReadable()) {
-            // Recursively import the folder.
-            yield this._migrateFolder(entry, folderGuid);
-          }
-        }
-        else {
-          // Strip the .url extension, to both check this is a valid link file,
-          // and get the associated title.
-          let matches = entry.leafName.match(/(.+)\.url$/i);
-          if (matches) {
-            let fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"].
-                              getService(Ci.nsIFileProtocolHandler);
-            let uri = fileHandler.readURLFile(entry);
-            let title = matches[1];
-
-            yield PlacesUtils.bookmarks.insert({
-              parentGuid: aDestFolderGuid, url: uri, title
-            });
-          }
-        }
-      } catch (ex) {
-        Components.utils.reportError("Unable to import " + this.importedAppLabel + " favorite (" + entry.leafName + "): " + ex);
-      }
+    if (!readingListItems.length) {
+      return;
     }
-  })
-};
-
-function Cookies(migrationType) {
-  this._migrationType = migrationType;
-}
-
-Cookies.prototype = {
-  type: MigrationUtils.resourceTypes.COOKIES,
-
-  get exists() {
-    if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
-      return !!this._cookiesFolder;
-    }
-    return !!this._cookiesFolders;
-  },
-
-  __cookiesFolder: null,
-  get _cookiesFolder() {
-    // Edge stores cookies in a number of places, and this shouldn't get called:
-    if (this._migrationType != MSMigrationUtils.MIGRATION_TYPE_IE) {
-      throw new Error("Shouldn't be looking for a single cookie folder unless we're migrating IE");
+    let destFolderGuid = yield this._ensureEdgeReadingListFolder(parentGuid);
+    for (let i = 0; i < readingListItems.length; i++) {
+      let readingListItem = readingListItems.queryElementAt(i, Ci.nsIPropertyBag2);
+      let url = readingListItem.get("uri");
+      let title = readingListItem.get("title");
+      let time = readingListItem.get("time");
+      let dateAdded = time ? new Date(time / 1000) : new Date();
+      yield PlacesUtils.bookmarks.insert({
+        parentGuid: destFolderGuid, url: url, title, dateAdded
+      });
     }
-
-    // Cookies are stored in txt files, in a Cookies folder whose path varies
-    // across the different OS versions.  CookD takes care of most of these
-    // cases, though, in Windows Vista/7, UAC makes a difference.
-    // If UAC is enabled, the most common destination is CookD/Low.  Though,
-    // if the user runs the application in administrator mode or disables UAC,
-    // cookies are stored in the original CookD destination.  Cause running the
-    // browser in administrator mode is unsafe and discouraged, we just care
-    // about the UAC state.
-    if (!this.__cookiesFolder) {
-      let cookiesFolder = Services.dirsvc.get("CookD", Ci.nsIFile);
-      if (cookiesFolder.exists() && cookiesFolder.isReadable()) {
-        // Check if UAC is enabled.
-        if (Services.appinfo.QueryInterface(Ci.nsIWinAppHelper).userCanElevate) {
-          cookiesFolder.append("Low");
-        }
-        this.__cookiesFolder = cookiesFolder;
-      }
-    }
-    return this.__cookiesFolder;
-  },
-
-  __cookiesFolders: null,
-  get _cookiesFolders() {
-    if (this._migrationType != MSMigrationUtils.MIGRATION_TYPE_EDGE) {
-      throw new Error("Shouldn't be looking for multiple cookie folders unless we're migrating Edge");
-    }
+  }),
 
-    let folders = [];
-    let edgeDir = getEdgeLocalDataFolder();
-    if (edgeDir) {
-      edgeDir.append("AC");
-      for (let path of EDGE_COOKIE_PATH_OPTIONS) {
-        let folder = edgeDir.clone();
-        let fullPath = path + EDGE_COOKIES_SUFFIX;
-        folder.appendRelativePath(fullPath);
-        if (folder.exists() && folder.isReadable() && folder.isDirectory()) {
-          folders.push(folder);
-        }
-      }
+  _ensureEdgeReadingListFolder: Task.async(function*(parentGuid) {
+    if (!this.__edgeReadingListFolderGuid) {
+      let folderTitle = MigrationUtils.getLocalizedString("importedEdgeReadingList");
+      let folderSpec = {type: PlacesUtils.bookmarks.TYPE_FOLDER, parentGuid, title: folderTitle};
+      this.__edgeReadingListFolderGuid = (yield PlacesUtils.bookmarks.insert(folderSpec)).guid;
     }
-    return this.__cookiesFolders = folders.length ? folders : null;
-  },
-
-  migrate(aCallback) {
-    CtypesHelpers.initialize();
-
-    let cookiesGenerator = (function genCookie() {
-      let success = false;
-      let folders = this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE ?
-                      this.__cookiesFolders : [this.__cookiesFolder];
-      for (let folder of folders) {
-        let entries = folder.directoryEntries;
-        while (entries.hasMoreElements()) {
-          let entry = entries.getNext().QueryInterface(Ci.nsIFile);
-          // Skip eventual bogus entries.
-          if (!entry.isFile() || !/\.txt$/.test(entry.leafName))
-            continue;
-
-          this._readCookieFile(entry, function(aSuccess) {
-            // Importing even a single cookie file is considered a success.
-            if (aSuccess)
-              success = true;
-            try {
-              cookiesGenerator.next();
-            } catch (ex) {}
-          });
-
-          yield undefined;
-        }
-      }
-
-      CtypesHelpers.finalize();
-
-      aCallback(success);
-    }).apply(this);
-    cookiesGenerator.next();
-  },
-
-  _readCookieFile(aFile, aCallback) {
-    let fileReader = Cc["@mozilla.org/files/filereader;1"].
-                     createInstance(Ci.nsIDOMFileReader);
-    let onLoadEnd = () => {
-      fileReader.removeEventListener("loadend", onLoadEnd, false);
-
-      if (fileReader.readyState != fileReader.DONE) {
-        Cu.reportError("Could not read cookie contents: " + fileReader.error);
-        aCallback(false);
-        return;
-      }
-
-      let success = true;
-      try {
-        this._parseCookieBuffer(fileReader.result);
-      } catch (ex) {
-        Components.utils.reportError("Unable to migrate cookie: " + ex);
-        success = false;
-      } finally {
-        aCallback(success);
-      }
-    };
-    fileReader.addEventListener("loadend", onLoadEnd, false);
-    fileReader.readAsText(new File(aFile));
-  },
-
-  /**
-   * Parses a cookie file buffer and returns an array of the contained cookies.
-   *
-   * The cookie file format is a newline-separated-values with a "*" used as
-   * delimeter between multiple records.
-   * Each cookie has the following fields:
-   *  - name
-   *  - value
-   *  - host/path
-   *  - flags
-   *  - Expiration time most significant integer
-   *  - Expiration time least significant integer
-   *  - Creation time most significant integer
-   *  - Creation time least significant integer
-   *  - Record delimiter "*"
-   *
-   * @note All the times are in FILETIME format.
-   */
-  _parseCookieBuffer(aTextBuffer) {
-    // Note the last record is an empty string.
-    let records = [r for each (r in aTextBuffer.split("*\n")) if (r)];
-    for (let record of records) {
-      let [name, value, hostpath, flags,
-           expireTimeLo, expireTimeHi] = record.split("\n");
-
-      // IE stores deleted cookies with a zero-length value, skip them.
-      if (value.length == 0)
-        continue;
-
-      let hostLen = hostpath.indexOf("/");
-      let host = hostpath.substr(0, hostLen);
-      let path = hostpath.substr(hostLen);
-
-      // For a non-null domain, assume it's what Mozilla considers
-      // a domain cookie.  See bug 222343.
-      if (host.length > 0) {
-        // Fist delete any possible extant matching host cookie.
-        Services.cookies.remove(host, name, path, false);
-        // Now make it a domain cookie.
-        if (host[0] != "." && !hostIsIPAddress(host))
-          host = "." + host;
-      }
-
-      let expireTime = CtypesHelpers.fileTimeToSecondsSinceEpoch(Number(expireTimeHi),
-                                                                 Number(expireTimeLo));
-      Services.cookies.add(host,
-                           path,
-                           name,
-                           value,
-                           Number(flags) & 0x1, // secure
-                           false, // httpOnly
-                           false, // session
-                           expireTime);
-    }
-  }
-};
-
-
-let MSMigrationUtils = {
-  MIGRATION_TYPE_IE: 1,
-  MIGRATION_TYPE_EDGE: 2,
-  getBookmarksMigrator(migrationType = this.MIGRATION_TYPE_IE) {
-    return new Bookmarks(migrationType);
-  },
-  getCookiesMigrator(migrationType = this.MIGRATION_TYPE_IE) {
-    return new Cookies(migrationType);
-  },
-};
+    return this.__edgeReadingListFolderGuid;
+  }),
+};
+
+function Cookies(migrationType) {
+  this._migrationType = migrationType;
+}
+
+Cookies.prototype = {
+  type: MigrationUtils.resourceTypes.COOKIES,
+
+  get exists() {
+    if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
+      return !!this._cookiesFolder;
+    }
+    return !!this._cookiesFolders;
+  },
+
+  __cookiesFolder: null,
+  get _cookiesFolder() {
+    // Edge stores cookies in a number of places, and this shouldn't get called:
+    if (this._migrationType != MSMigrationUtils.MIGRATION_TYPE_IE) {
+      throw new Error("Shouldn't be looking for a single cookie folder unless we're migrating IE");
+    }
+
+    // Cookies are stored in txt files, in a Cookies folder whose path varies
+    // across the different OS versions.  CookD takes care of most of these
+    // cases, though, in Windows Vista/7, UAC makes a difference.
+    // If UAC is enabled, the most common destination is CookD/Low.  Though,
+    // if the user runs the application in administrator mode or disables UAC,
+    // cookies are stored in the original CookD destination.  Cause running the
+    // browser in administrator mode is unsafe and discouraged, we just care
+    // about the UAC state.
+    if (!this.__cookiesFolder) {
+      let cookiesFolder = Services.dirsvc.get("CookD", Ci.nsIFile);
+      if (cookiesFolder.exists() && cookiesFolder.isReadable()) {
+        // Check if UAC is enabled.
+        if (Services.appinfo.QueryInterface(Ci.nsIWinAppHelper).userCanElevate) {
+          cookiesFolder.append("Low");
+        }
+        this.__cookiesFolder = cookiesFolder;
+      }
+    }
+    return this.__cookiesFolder;
+  },
+
+  __cookiesFolders: null,
+  get _cookiesFolders() {
+    if (this._migrationType != MSMigrationUtils.MIGRATION_TYPE_EDGE) {
+      throw new Error("Shouldn't be looking for multiple cookie folders unless we're migrating Edge");
+    }
+
+    let folders = [];
+    let edgeDir = getEdgeLocalDataFolder();
+    if (edgeDir) {
+      edgeDir.append("AC");
+      for (let path of EDGE_COOKIE_PATH_OPTIONS) {
+        let folder = edgeDir.clone();
+        let fullPath = path + EDGE_COOKIES_SUFFIX;
+        folder.appendRelativePath(fullPath);
+        if (folder.exists() && folder.isReadable() && folder.isDirectory()) {
+          folders.push(folder);
+        }
+      }
+    }
+    return this.__cookiesFolders = folders.length ? folders : null;
+  },
+
+  migrate(aCallback) {
+    CtypesHelpers.initialize();
+
+    let cookiesGenerator = (function genCookie() {
+      let success = false;
+      let folders = this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE ?
+                      this.__cookiesFolders : [this.__cookiesFolder];
+      for (let folder of folders) {
+        let entries = folder.directoryEntries;
+        while (entries.hasMoreElements()) {
+          let entry = entries.getNext().QueryInterface(Ci.nsIFile);
+          // Skip eventual bogus entries.
+          if (!entry.isFile() || !/\.txt$/.test(entry.leafName))
+            continue;
+
+          this._readCookieFile(entry, function(aSuccess) {
+            // Importing even a single cookie file is considered a success.
+            if (aSuccess)
+              success = true;
+            try {
+              cookiesGenerator.next();
+            } catch (ex) {}
+          });
+
+          yield undefined;
+        }
+      }
+
+      CtypesHelpers.finalize();
+
+      aCallback(success);
+    }).apply(this);
+    cookiesGenerator.next();
+  },
+
+  _readCookieFile(aFile, aCallback) {
+    let fileReader = Cc["@mozilla.org/files/filereader;1"].
+                     createInstance(Ci.nsIDOMFileReader);
+    let onLoadEnd = () => {
+      fileReader.removeEventListener("loadend", onLoadEnd, false);
+
+      if (fileReader.readyState != fileReader.DONE) {
+        Cu.reportError("Could not read cookie contents: " + fileReader.error);
+        aCallback(false);
+        return;
+      }
+
+      let success = true;
+      try {
+        this._parseCookieBuffer(fileReader.result);
+      } catch (ex) {
+        Components.utils.reportError("Unable to migrate cookie: " + ex);
+        success = false;
+      } finally {
+        aCallback(success);
+      }
+    };
+    fileReader.addEventListener("loadend", onLoadEnd, false);
+    fileReader.readAsText(new File(aFile));
+  },
+
+  /**
+   * Parses a cookie file buffer and returns an array of the contained cookies.
+   *
+   * The cookie file format is a newline-separated-values with a "*" used as
+   * delimeter between multiple records.
+   * Each cookie has the following fields:
+   *  - name
+   *  - value
+   *  - host/path
+   *  - flags
+   *  - Expiration time most significant integer
+   *  - Expiration time least significant integer
+   *  - Creation time most significant integer
+   *  - Creation time least significant integer
+   *  - Record delimiter "*"
+   *
+   * @note All the times are in FILETIME format.
+   */
+  _parseCookieBuffer(aTextBuffer) {
+    // Note the last record is an empty string.
+    let records = [r for each (r in aTextBuffer.split("*\n")) if (r)];
+    for (let record of records) {
+      let [name, value, hostpath, flags,
+           expireTimeLo, expireTimeHi] = record.split("\n");
+
+      // IE stores deleted cookies with a zero-length value, skip them.
+      if (value.length == 0)
+        continue;
+
+      let hostLen = hostpath.indexOf("/");
+      let host = hostpath.substr(0, hostLen);
+      let path = hostpath.substr(hostLen);
+
+      // For a non-null domain, assume it's what Mozilla considers
+      // a domain cookie.  See bug 222343.
+      if (host.length > 0) {
+        // Fist delete any possible extant matching host cookie.
+        Services.cookies.remove(host, name, path, false);
+        // Now make it a domain cookie.
+        if (host[0] != "." && !hostIsIPAddress(host))
+          host = "." + host;
+      }
+
+      let expireTime = CtypesHelpers.fileTimeToSecondsSinceEpoch(Number(expireTimeHi),
+                                                                 Number(expireTimeLo));
+      Services.cookies.add(host,
+                           path,
+                           name,
+                           value,
+                           Number(flags) & 0x1, // secure
+                           false, // httpOnly
+                           false, // session
+                           expireTime);
+    }
+  }
+};
+
+
+let MSMigrationUtils = {
+  MIGRATION_TYPE_IE: 1,
+  MIGRATION_TYPE_EDGE: 2,
+  getBookmarksMigrator(migrationType = this.MIGRATION_TYPE_IE) {
+    return new Bookmarks(migrationType);
+  },
+  getCookiesMigrator(migrationType = this.MIGRATION_TYPE_IE) {
+    return new Cookies(migrationType);
+  },
+};
--- a/browser/components/migration/moz.build
+++ b/browser/components/migration/moz.build
@@ -5,22 +5,24 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
 XPIDL_SOURCES += [
     'nsIBrowserProfileMigrator.idl',
+    'nsIEdgeReadingListExtractor.idl',
 ]
 
 XPIDL_MODULE = 'migration'
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     SOURCES += [
+        'nsEdgeReadingListExtractor.cpp',
         'nsIEHistoryEnumerator.cpp',
     ]
 
 EXTRA_COMPONENTS += [
     'FirefoxProfileMigrator.js',
     'ProfileMigrator.js',
 ]
 
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/nsEdgeReadingListExtractor.cpp
@@ -0,0 +1,204 @@
+/* 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/. */
+
+#include "nsEdgeReadingListExtractor.h"
+
+#include <windows.h>
+
+#include "nsCOMPtr.h"
+//#include "nsIConsoleService.h"
+#include "nsIMutableArray.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsNetUtil.h"
+#include "nsWindowsMigrationUtils.h"
+
+#define NS_HANDLE_JET_ERROR(err) { \
+  if (err < JET_errSuccess) {	\
+    rv = ConvertJETError(err); \
+    goto CloseDB; \
+  } \
+}
+
+#define MAX_URL_LENGTH 4168
+#define MAX_TITLE_LENGTH 1024
+
+NS_IMPL_ISUPPORTS(nsEdgeReadingListExtractor, nsIEdgeReadingListExtractor)
+
+NS_IMETHODIMP
+nsEdgeReadingListExtractor::Extract(const nsAString& aDBPath, nsIArray** aItems)
+{
+  nsresult rv = NS_OK;
+  *aItems = nullptr;
+
+  JET_ERR err;
+  JET_INSTANCE instance;
+  JET_SESID sesid;
+  JET_DBID dbid;
+  JET_TABLEID tableid;
+  JET_COLUMNDEF urlColumnInfo = { 0 };
+  JET_COLUMNDEF titleColumnInfo = { 0 };
+  JET_COLUMNDEF addedDateColumnInfo = { 0 };
+
+  // Need to ensure this happens before we skip ahead to CloseDB,
+  // otherwise the compiler complains.
+  nsCOMPtr<nsIMutableArray> items = do_CreateInstance(NS_ARRAY_CONTRACTID);
+
+  // JET does not throw exceptions, and so error handling and ensuring we close
+  // the DB is a bit finnicky. Keep track of how far we got so we guarantee closing
+  // the right things
+  bool instanceCreated, sessionCreated, dbOpened, tableOpened;
+
+  char16_t* dbPath = ToNewUnicode(aDBPath);
+
+  // Check for the right page size and initialize with that
+  unsigned long pageSize;
+  err = JetGetDatabaseFileInfoW(dbPath, &pageSize, sizeof(pageSize), JET_DbInfoPageSize);
+  NS_HANDLE_JET_ERROR(err)
+  err = JetSetSystemParameter(&instance, NULL, JET_paramDatabasePageSize, pageSize, NULL);
+  NS_HANDLE_JET_ERROR(err)
+
+  // Turn off recovery, because otherwise we will create log files in either the cwd or
+  // overwrite Edge's own logfiles, which is useless at best and at worst might mess with
+  // Edge actually using the DB
+  err = JetSetSystemParameter(&instance, NULL, JET_paramRecovery, NULL, "Off");
+  NS_HANDLE_JET_ERROR(err)
+
+  // Start our session:
+  err = JetCreateInstance(&instance, "edge_readinglist_migration");
+  NS_HANDLE_JET_ERROR(err)
+  instanceCreated = true;
+
+  err = JetInit(&instance);
+  NS_HANDLE_JET_ERROR(err)
+  err = JetBeginSession(instance, &sesid, 0, 0);
+  NS_HANDLE_JET_ERROR(err)
+  sessionCreated = true;
+
+  // Actually open the DB, and make sure to do so readonly:
+  err = JetAttachDatabaseW(sesid, dbPath, JET_bitDbReadOnly);
+  NS_HANDLE_JET_ERROR(err)
+  dbOpened = true;
+  err = JetOpenDatabaseW(sesid, dbPath, NULL, &dbid, JET_bitDbReadOnly);
+  NS_HANDLE_JET_ERROR(err)
+
+  // Open the readinglist table and get information on the columns we are interested in:
+  err = JetOpenTable(sesid, dbid, "ReadingList", NULL, 0, JET_bitTableReadOnly, &tableid);
+  NS_HANDLE_JET_ERROR(err)
+  tableOpened = true;
+  err = JetGetColumnInfo(sesid, dbid, "ReadingList", "URL", &urlColumnInfo,
+                         sizeof(urlColumnInfo), JET_ColInfo);
+  NS_HANDLE_JET_ERROR(err)
+  if (urlColumnInfo.cbMax > MAX_URL_LENGTH) {
+    /*nsCOMPtr<nsIConsoleService> consoleService = do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+    if (consoleService) {
+      consoleService->LogStringMessage(NS_LITERAL_STRING("Edge migration: URL column size increased").get());
+    }*/
+  }
+  err = JetGetColumnInfo(sesid, dbid, "ReadingList", "Title", &titleColumnInfo,
+                         sizeof(titleColumnInfo), JET_ColInfo);
+  NS_HANDLE_JET_ERROR(err)
+  if (titleColumnInfo.cbMax > MAX_TITLE_LENGTH) {
+    /*nsCOMPtr<nsIConsoleService> consoleService = do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+    if (consoleService) {
+      consoleService->LogStringMessage(NS_LITERAL_STRING("Edge migration: Title column size increased").get());
+    }*/
+  }
+  err = JetGetColumnInfo(sesid, dbid, "ReadingList", "AddedDate", &addedDateColumnInfo,
+                         sizeof(addedDateColumnInfo), JET_ColInfo);
+  NS_HANDLE_JET_ERROR(err)
+
+  // verify the column types are what we expect:
+  if (urlColumnInfo.coltyp != JET_coltypLongText ||
+      titleColumnInfo.coltyp != JET_coltypLongText ||
+      addedDateColumnInfo.coltyp != JET_coltypLongLong) {
+    rv = NS_ERROR_NOT_IMPLEMENTED;
+    goto CloseDB;
+  }
+
+  JET_COLUMNID urlColumnId, titleColumnId, addedDateColumnId;
+  urlColumnId = urlColumnInfo.columnid;
+  titleColumnId = titleColumnInfo.columnid;
+  addedDateColumnId = addedDateColumnInfo.columnid;
+
+  // If we got here, we've found our table and column information
+
+  err = JetMove(sesid, tableid, JET_MoveFirst, 0);
+  // It's possible there are 0 items in this table, in which case we want to
+  // not fail:
+  if (err == JET_errNoCurrentRecord) {
+    items.forget(aItems);
+    goto CloseDB;
+  }
+  // Check for any other errors
+  NS_HANDLE_JET_ERROR(err)
+
+  FILETIME addedDate;
+  wchar_t urlBuffer[MAX_URL_LENGTH] = { 0 };
+  wchar_t titleBuffer[MAX_TITLE_LENGTH] = { 0 };
+  do {
+    err = JetRetrieveColumn(sesid, tableid, urlColumnId, &urlBuffer,
+                            sizeof(urlBuffer), NULL, 0, NULL);
+    NS_HANDLE_JET_ERROR(err)
+    err = JetRetrieveColumn(sesid, tableid, titleColumnId, &titleBuffer,
+                            sizeof(titleBuffer), NULL, 0, NULL);
+    NS_HANDLE_JET_ERROR(err)
+    err = JetRetrieveColumn(sesid, tableid, addedDateColumnId, &addedDate,
+                            sizeof(addedDate), NULL, 0, NULL);
+    NS_HANDLE_JET_ERROR(err)
+    nsCOMPtr<nsIWritablePropertyBag2> pbag = do_CreateInstance("@mozilla.org/hash-property-bag;1");
+    printf("Got reading list item: %S (%S)\n", titleBuffer, urlBuffer);
+    bool dateIsValid;
+    PRTime prAddedDate = WinMigrationFileTimeToPRTime(&addedDate, &dateIsValid);
+    nsDependentString url(urlBuffer);
+    nsDependentString title(titleBuffer);
+    pbag->SetPropertyAsAString(NS_LITERAL_STRING("uri"), url);
+    pbag->SetPropertyAsAString(NS_LITERAL_STRING("title"), title);
+    if (dateIsValid) {
+      pbag->SetPropertyAsInt64(NS_LITERAL_STRING("time"), prAddedDate);
+    }
+    items->AppendElement(pbag, false);
+    memset(urlBuffer, 0, sizeof(urlBuffer));
+    memset(titleBuffer, 0, sizeof(titleBuffer));
+  } while (JET_errSuccess == JetMove(sesid, tableid, JET_MoveNext, 0));
+
+  items.forget(aItems);
+
+CloseDB:
+  // Terminate ESENT. This performs a clean shutdown.
+  // Ignore errors while closing:
+  if (tableOpened)
+    JetCloseTable(sesid, tableid);
+  if (dbOpened)
+    JetCloseDatabase(sesid, dbid, 0);
+  if (sessionCreated)
+    JetEndSession(sesid, 0);
+  if (instanceCreated)
+    JetTerm(instance);
+
+  return rv;
+}
+
+nsresult
+nsEdgeReadingListExtractor::ConvertJETError(const JET_ERR aError)
+{
+  switch (aError) {
+    case JET_errPageSizeMismatch:
+    case JET_errInvalidName:
+    case JET_errColumnNotFound:
+      // The DB format has changed and we haven't updated this migration code:
+      return NS_ERROR_NOT_IMPLEMENTED;
+    case JET_errDatabaseLocked:
+      return NS_ERROR_FILE_IS_LOCKED;
+    case JET_errPermissionDenied:
+    case JET_errAccessDenied:
+      return NS_ERROR_FILE_ACCESS_DENIED;
+    case JET_errInvalidFilename:
+      return NS_ERROR_FILE_INVALID_PATH;
+    case JET_errFileNotFound:
+      return NS_ERROR_FILE_NOT_FOUND;
+    default:
+      return NS_ERROR_FAILURE;
+  }
+}
+
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/nsEdgeReadingListExtractor.h
@@ -0,0 +1,31 @@
+/* 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/. */
+
+#ifndef edgereadinglistextractor__h__
+#define edgereadinglistextractor__h__
+
+#include "nsIArray.h"
+#include "nsIEdgeReadingListExtractor.h"
+
+// To get access to the long data types, we need to use at least the Vista version of the JET APIs
+#undef JET_VERSION
+#define JET_VERSION 0x0600
+#include <esent.h>
+
+class nsEdgeReadingListExtractor final : public nsIEdgeReadingListExtractor
+{
+public:
+  nsEdgeReadingListExtractor() {}
+
+  NS_DECL_ISUPPORTS
+
+  NS_DECL_NSIEDGEREADINGLISTEXTRACTOR
+
+private:
+  ~nsEdgeReadingListExtractor() {}
+
+  nsresult ConvertJETError(JET_ERR err);
+};
+
+#endif
--- a/browser/components/migration/nsIEHistoryEnumerator.cpp
+++ b/browser/components/migration/nsIEHistoryEnumerator.cpp
@@ -2,47 +2,23 @@
  * 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/. */
 
 #include "nsIEHistoryEnumerator.h"
 
 #include <urlhist.h>
 #include <shlguid.h>
 
-#include "nsStringAPI.h"
-#include "nsNetUtil.h"
-#include "prtime.h"
-#include "nsIVariant.h"
+#include "nsArrayEnumerator.h"
 #include "nsCOMArray.h"
-#include "nsArrayEnumerator.h"
-
-namespace {
-
-  PRTime FileTimeToPRTime(FILETIME* filetime)
-  {
-    SYSTEMTIME st;
-    ::FileTimeToSystemTime(filetime, &st);
-    PRExplodedTime prt;
-    prt.tm_year = st.wYear;
-    // SYSTEMTIME's day-of-month parameter is 1-based,
-    // PRExplodedTime's is 0-based.
-    prt.tm_month = st.wMonth - 1;
-    prt.tm_mday = st.wDay;
-    prt.tm_hour = st.wHour;
-    prt.tm_min = st.wMinute;
-    prt.tm_sec = st.wSecond;
-    prt.tm_usec = st.wMilliseconds * 1000;
-    prt.tm_wday = 0;
-    prt.tm_yday = 0;
-    prt.tm_params.tp_gmt_offset = 0;
-    prt.tm_params.tp_dst_offset = 0;
-    return PR_ImplodeTime(&prt);
-  }
-
-} // namespace
+#include "nsIVariant.h"
+#include "nsNetUtil.h"
+#include "nsStringAPI.h"
+#include "nsWindowsMigrationUtils.h"
+#include "prtime.h"
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIEHistoryEnumerator
 
 NS_IMPL_ISUPPORTS(nsIEHistoryEnumerator, nsISimpleEnumerator)
 
 nsIEHistoryEnumerator::nsIEHistoryEnumerator()
 {
@@ -101,24 +77,27 @@ nsIEHistoryEnumerator::HasMoreElements(b
     if (NS_FAILED(rv)) {
       // Got a corrupt or invalid URI, continue to the next entry.
       return HasMoreElements(_retval);
     }
   }
 
   nsDependentString title(statURL.pwcsTitle);
 
-  PRTime lastVisited = FileTimeToPRTime(&(statURL.ftLastVisited));
+  bool lastVisitTimeIsValid;
+  PRTime lastVisited = WinMigrationFileTimeToPRTime(&(statURL.ftLastVisited), &lastVisitTimeIsValid);
 
   mCachedNextEntry = do_CreateInstance("@mozilla.org/hash-property-bag;1");
   MOZ_ASSERT(mCachedNextEntry, "Should have instanced a new property bag");
   if (mCachedNextEntry) {
     mCachedNextEntry->SetPropertyAsInterface(NS_LITERAL_STRING("uri"), uri);
     mCachedNextEntry->SetPropertyAsAString(NS_LITERAL_STRING("title"), title);
-    mCachedNextEntry->SetPropertyAsInt64(NS_LITERAL_STRING("time"), lastVisited);
+    if (lastVisitTimeIsValid) {
+      mCachedNextEntry->SetPropertyAsInt64(NS_LITERAL_STRING("time"), lastVisited);
+    }
 
     *_retval = true;
   }
 
   if (statURL.pwcsTitle)
     ::CoTaskMemFree(statURL.pwcsTitle);
 
   return NS_OK;
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/nsIEdgeReadingListExtractor.idl
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIArray;
+
+[scriptable, uuid(bfdef4aa-dcd1-4d31-b5d9-188fe8d98623)]
+interface nsIEdgeReadingListExtractor : nsISupports
+{
+  /**
+   * Import data from the database indicated by the databasePath
+   * May fail if the path is invalid, unreadable, the database is corrupt,
+   * or the data in the database is not in the format we expect.
+   *
+   * @param  databasePath the absolute path to the database we'd like to import
+   * @return an enumerator of nsIPropertyBag2 items that each have a URL, title, and
+   *         creation dates.
+   */
+  nsIArray extract(in DOMString databasePath);
+};
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/nsWindowsMigrationUtils.h
@@ -0,0 +1,36 @@
+/* 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/. */
+
+#ifndef windowsmigrationutils__h__
+#define windowsmigrationutils__h__
+
+#include "prtime.h"
+
+static
+PRTime WinMigrationFileTimeToPRTime(FILETIME* filetime, bool* isValid)
+{
+  SYSTEMTIME st;
+  *isValid = ::FileTimeToSystemTime(filetime, &st);
+  if (!*isValid) {
+    return 0;
+  }
+  PRExplodedTime prt;
+  prt.tm_year = st.wYear;
+  // SYSTEMTIME's day-of-month parameter is 1-based,
+  // PRExplodedTime's is 0-based.
+  prt.tm_month = st.wMonth - 1;
+  prt.tm_mday = st.wDay;
+  prt.tm_hour = st.wHour;
+  prt.tm_min = st.wMinute;
+  prt.tm_sec = st.wSecond;
+  prt.tm_usec = st.wMilliseconds * 1000;
+  prt.tm_wday = 0;
+  prt.tm_yday = 0;
+  prt.tm_params.tp_gmt_offset = 0;
+  prt.tm_params.tp_dst_offset = 0;
+  return PR_ImplodeTime(&prt);
+}
+
+#endif
+
--- a/browser/locales/en-US/chrome/browser/migration/migration.properties
+++ b/browser/locales/en-US/chrome/browser/migration/migration.properties
@@ -12,16 +12,17 @@ sourceNameCanary=Google Chrome Canary
 sourceNameChrome=Google Chrome
 sourceNameChromium=Chromium
 sourceNameFirefox=Mozilla Firefox
 sourceName360se=360 Secure Browser
 
 importedBookmarksFolder=From %S
 
 importedSafariReadingList=Reading List (From Safari)
+importedEdgeReadingList=Reading List (From Edge)
 
 # Import Sources
 # Note: When adding an import source for profile reset, add the string name to
 # resetProfile.js if it should be listed in the reset dialog.
 1_ie=Internet Options
 1_edge=Settings
 1_safari=Preferences
 1_chrome=Preferences