Safari Bookmark/Favicon import.

Review URL: http://codereview.chromium.org/159750

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@22556 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 1bb9f40b..1294816 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -2742,6 +2742,9 @@
       <message name="IDS_BOOKMARK_GROUP_FROM_FIREFOX" desc="The group name of bookmarks from Firefox">
         Imported From Firefox
       </message>
+      <message name="IDS_BOOKMARK_GROUP_FROM_SAFARI" desc="The group name of bookmarks from Safari">
+        Imported From Safari
+      </message>
       <message name="IDS_BOOKMARK_GROUP_FROM_GOOGLE_TOOLBAR" desc="The group name of bookmarks from Google Toolbar">
         Imported From Google Toolbar
       </message>
diff --git a/chrome/browser/first_run_mac.mm b/chrome/browser/first_run_mac.mm
index d839bf4..7242f94f 100644
--- a/chrome/browser/first_run_mac.mm
+++ b/chrome/browser/first_run_mac.mm
@@ -91,13 +91,15 @@
   }
 
   // Import bookmarks.
-  const ProfileInfo& source_profile = importer_host->GetSourceProfileInfoAt(
-      [dialog.get() browserImportSelectedIndex]);
-  int items = SEARCH_ENGINES + HISTORY + FAVORITES + HOME_PAGE + PASSWORDS;
-  // TODO(port): Call StartImportingWithUI here instead and launch
-  // a new process that does the actual import.
-  importer_host->StartImportSettings(source_profile, profile, items,
-                                     new ProfileWriter(profile), true);
+  if ([dialog.get() importBookmarks]) {
+    const ProfileInfo& source_profile = importer_host->GetSourceProfileInfoAt(
+        [dialog.get() browserImportSelectedIndex]);
+    int items = SEARCH_ENGINES + HISTORY + FAVORITES + HOME_PAGE + PASSWORDS;
+    // TODO(port): Call StartImportingWithUI here instead and launch
+    // a new process that does the actual import.
+    importer_host->StartImportSettings(source_profile, profile, items,
+                                       new ProfileWriter(profile), true);
+  }
 
 #endif  // defined(GOOGLE_CHROME_BUILD)
   return true;
diff --git a/chrome/browser/importer/firefox3_importer.cc b/chrome/browser/importer/firefox3_importer.cc
index ffdf4076..8ee342c 100644
--- a/chrome/browser/importer/firefox3_importer.cc
+++ b/chrome/browser/importer/firefox3_importer.cc
@@ -24,20 +24,6 @@
 using base::Time;
 using webkit_glue::PasswordForm;
 
-// Wraps the function sqlite3_close() in a class that is
-// used in scoped_ptr_malloc.
-
-namespace {
-
-class DBClose {
- public:
-  inline void operator()(sqlite3* x) const {
-    sqlite3_close(x);
-  }
-};
-
-}  // namespace
-
 void Firefox3Importer::StartImport(ProfileInfo profile_info,
                                    uint16 items, ProfileWriter* writer,
                                    MessageLoop* delagate_loop,
@@ -84,7 +70,7 @@
   sqlite3* sqlite;
   if (sqlite3_open(WideToUTF8(file).c_str(), &sqlite) != SQLITE_OK)
     return;
-  scoped_ptr_malloc<sqlite3, DBClose> db(sqlite);
+  sqlite_utils::scoped_sqlite_db_ptr db(sqlite);
 
   SQLStatement s;
   // |visit_type| represent the transition type of URLs (typed, click,
@@ -133,7 +119,7 @@
   sqlite3* sqlite;
   if (sqlite3_open(WideToUTF8(file).c_str(), &sqlite) != SQLITE_OK)
     return;
-  scoped_ptr_malloc<sqlite3, DBClose> db(sqlite);
+  sqlite_utils::scoped_sqlite_db_ptr db(sqlite);
 
   // Get the bookmark folders that we are interested in.
   int toolbar_folder_id = -1;
@@ -329,7 +315,7 @@
   sqlite3* sqlite;
   if (sqlite3_open(WideToUTF8(file).c_str(), &sqlite) != SQLITE_OK)
     return;
-  scoped_ptr_malloc<sqlite3, DBClose> db(sqlite);
+  sqlite_utils::scoped_sqlite_db_ptr db(sqlite);
 
   SQLStatement s;
   const char* stmt = "SELECT engineid FROM engine_data "
diff --git a/chrome/browser/importer/safari_importer.h b/chrome/browser/importer/safari_importer.h
index 28b939f3..75f2a65 100644
--- a/chrome/browser/importer/safari_importer.h
+++ b/chrome/browser/importer/safari_importer.h
@@ -7,6 +7,12 @@
 
 #include "chrome/browser/importer/importer.h"
 
+#include <map>
+#include <set>
+#include <vector>
+
+#include "chrome/common/sqlite_utils.h"
+
 #if __OBJC__
 @class NSDictionary;
 @class NSString;
@@ -31,14 +37,35 @@
                            ImporterHost* host);
 
  private:
+  FRIEND_TEST(SafariImporterTest, BookmarkImport);
+  FRIEND_TEST(SafariImporterTest, FavIconImport);
   FRIEND_TEST(SafariImporterTest, HistoryImport);
 
+  // Multiple URLs can share the same FavIcon, this is a map
+  // of URLs -> IconIDs that we load as a temporary step before
+  // actually loading the icons.
+  typedef std::map<int64, std::set<GURL> > FaviconMap;
+
   void ImportBookmarks();
-  void ImportSearchEngines();
   void ImportPasswords();
   void ImportHistory();
   void ImportHomepage();
 
+  // Parse Safari's stored bookmarks.
+  void ParseBookmarks(std::vector<ProfileWriter::BookmarkEntry>* bookmarks);
+
+  // Function to recursively read Bookmarks out of Safari plist.
+  // |bookmark_folder| The dictionary containing a folder to parse.
+  // |parent_path_elements| Path elements up to this point.
+  // |is_in_toolbar| Is this folder in the toolbar.
+  // |out_bookmarks| BookMark element array to write into.
+  void RecursiveReadBookmarksFolder(
+      NSDictionary* bookmark_folder,
+      const std::vector<std::wstring>& parent_path_elements,
+      bool is_in_toolbar,
+      std::vector<ProfileWriter::BookmarkEntry>* out_bookmarks);
+
+
   // Converts history time stored by Safari as a double serialized as a string,
   // to seconds-since-UNIX-Ephoch-format used by Chrome.
   double HistoryTimeToEpochTime(NSString* history_time);
@@ -46,13 +73,16 @@
   // Parses Safari's history and loads it into the input array.
   void ParseHistoryItems(std::vector<history::URLRow>* history_items);
 
-  // Given the URL of a page and a favicon data URL, adds an appropriate record
-  // to the given favicon usage vector. Will do nothing if the favicon is not
-  // valid.
-  static void DataURLToFaviconUsage(
-      const GURL& link_url,
-      const GURL& favicon_data,
-      std::vector<history::ImportedFavIconUsage>* favicons);
+  // Loads the favicon Database file, returns NULL on failure.
+  sqlite3* OpenFavIconDB();
+
+  // Loads the urls associated with the favicons into favicon_map;
+  void ImportFavIconURLs(sqlite3* db, FaviconMap* favicon_map);
+
+  // Loads and reencodes the individual favicons.
+  void LoadFaviconData(sqlite3* db,
+                       const FaviconMap& favicon_map,
+                       std::vector<history::ImportedFavIconUsage>* favicons);
 
   ProfileWriter* writer_;
   FilePath library_dir_;
diff --git a/chrome/browser/importer/safari_importer.mm b/chrome/browser/importer/safari_importer.mm
index 78e11111..0057aef 100644
--- a/chrome/browser/importer/safari_importer.mm
+++ b/chrome/browser/importer/safari_importer.mm
@@ -6,13 +6,16 @@
 
 #include "chrome/browser/importer/safari_importer.h"
 
+#include <map>
 #include <vector>
 
+#include "app/l10n_util.h"
 #include "base/message_loop.h"
 #include "base/scoped_nsobject.h"
 #include "base/string16.h"
 #include "base/sys_string_conversions.h"
 #include "base/time.h"
+#include "chrome/common/sqlite_utils.h"
 #include "chrome/common/url_constants.h"
 #include "googleurl/src/gurl.h"
 #include "grit/generated_resources.h"
@@ -57,11 +60,6 @@
     ImportBookmarks();
     NotifyItemEnded(FAVORITES);
   }
-  if ((items & SEARCH_ENGINES) && !cancelled()) {
-    NotifyItemStarted(SEARCH_ENGINES);
-    ImportSearchEngines();
-    NotifyItemEnded(SEARCH_ENGINES);
-  }
   if ((items & PASSWORDS) && !cancelled()) {
     NotifyItemStarted(PASSWORDS);
     ImportPasswords();
@@ -76,11 +74,199 @@
 }
 
 void SafariImporter::ImportBookmarks() {
-  NOTIMPLEMENTED();
+  std::vector<ProfileWriter::BookmarkEntry> bookmarks;
+  ParseBookmarks(&bookmarks);
+
+  // Write bookmarks into profile.
+  if (!bookmarks.empty() && !cancelled()) {
+    main_loop_->PostTask(FROM_HERE, NewRunnableMethod(writer_,
+        &ProfileWriter::AddBookmarkEntry, bookmarks,
+        l10n_util::GetString(IDS_BOOKMARK_GROUP_FROM_SAFARI),
+        import_to_bookmark_bar() ? ProfileWriter::IMPORT_TO_BOOKMARK_BAR : 0));
+  }
+
+  // Import favicons.
+  sqlite_utils::scoped_sqlite_db_ptr db(OpenFavIconDB());
+  FaviconMap favicon_map;
+  ImportFavIconURLs(db.get(), &favicon_map);
+  // Write favicons into profile.
+  if (!favicon_map.empty() && !cancelled()) {
+    std::vector<history::ImportedFavIconUsage> favicons;
+    LoadFaviconData(db.get(), favicon_map, &favicons);
+    main_loop_->PostTask(FROM_HERE, NewRunnableMethod(writer_,
+        &ProfileWriter::AddFavicons, favicons));
+  }
 }
 
-void SafariImporter::ImportSearchEngines() {
-  NOTIMPLEMENTED();
+sqlite3* SafariImporter::OpenFavIconDB() {
+  // Construct ~/Library/Safari/WebIcons.db path
+  NSString* library_dir = [NSString
+      stringWithUTF8String:library_dir_.value().c_str()];
+  NSString* safari_dir = [library_dir
+      stringByAppendingPathComponent:@"Safari"];
+  NSString* favicons_db_path = [safari_dir
+    stringByAppendingPathComponent:@"WebpageIcons.db"];
+
+  sqlite3* favicons_db;
+  const char* safariicons_dbname = [favicons_db_path fileSystemRepresentation];
+  if (sqlite3_open(safariicons_dbname, &favicons_db) != SQLITE_OK)
+    return NULL;
+
+  return favicons_db;
+}
+
+void SafariImporter::ImportFavIconURLs(sqlite3* db, FaviconMap* favicon_map) {
+  SQLStatement s;
+  const char* stmt = "SELECT iconID, url FROM PageURL;";
+  if (s.prepare(db, stmt) != SQLITE_OK)
+    return;
+
+  while (s.step() == SQLITE_ROW && !cancelled()) {
+    int64 icon_id = s.column_int(0);
+    GURL url = GURL(s.column_string(1));
+    (*favicon_map)[icon_id].insert(url);
+  }
+}
+
+void SafariImporter::LoadFaviconData(sqlite3* db,
+                                     const FaviconMap& favicon_map,
+                        std::vector<history::ImportedFavIconUsage>* favicons) {
+  SQLStatement s;
+  const char* stmt = "SELECT i.url, d.data "
+                     "FROM IconInfo i JOIN IconData d "
+                     "ON i.iconID = d.iconID "
+                     "WHERE i.iconID = ?;";
+  if (s.prepare(db, stmt) != SQLITE_OK)
+    return;
+
+  for (FaviconMap::const_iterator i = favicon_map.begin();
+       i != favicon_map.end(); ++i) {
+    s.bind_int64(0, i->first);
+    if (s.step() == SQLITE_ROW) {
+      history::ImportedFavIconUsage usage;
+
+      usage.favicon_url = GURL(s.column_string(0));
+      if (!usage.favicon_url.is_valid())
+        continue;  // Don't bother importing favicons with invalid URLs.
+
+      std::vector<unsigned char> data;
+      if (!s.column_blob_as_vector(1, &data) || data.empty())
+        continue;  // Data definitely invalid.
+
+      if (!ReencodeFavicon(&data[0], data.size(), &usage.png_data))
+        continue;  // Unable to decode.
+
+      usage.urls = i->second;
+      favicons->push_back(usage);
+    }
+    s.reset();
+  }
+}
+
+void SafariImporter::RecursiveReadBookmarksFolder(
+    NSDictionary* bookmark_folder,
+    const std::vector<std::wstring>& parent_path_elements,
+    bool is_in_toolbar,
+    std::vector<ProfileWriter::BookmarkEntry>* out_bookmarks) {
+  DCHECK(bookmark_folder);
+
+  NSString* type = [bookmark_folder objectForKey:@"WebBookmarkType"];
+  NSString* title = [bookmark_folder objectForKey:@"Title"];
+
+  // Are we the dictionary that contains all other bookmarks?
+  // We need to know this so we don't add it to the path.
+  bool is_top_level_bookmarks_container = [bookmark_folder
+      objectForKey:@"WebBookmarkFileVersion"] != nil;
+
+  // We're expecting a list of bookmarks here, if that isn't what we got, fail.
+  if (![type isEqualToString:@"WebBookmarkTypeList"] || !title) {
+    DCHECK(false) << "Type =("
+    << (type ? base::SysNSStringToUTF8(type) : "Null Type")
+    << ") Title=(" << (title ? base::SysNSStringToUTF8(title) : "Null title")
+    << ")";
+    return;
+  }
+
+  std::vector<std::wstring> path_elements(parent_path_elements);
+  // Is this the toolbar folder?
+  if ([title isEqualToString:@"BookmarksBar"]) {
+    // Be defensive, the toolbar items shouldn't have a prepended path.
+    path_elements.clear();
+    is_in_toolbar = true;
+  } else if ([title isEqualToString:@"BookmarksMenu"]) {
+    // top level container for normal bookmarks.
+    path_elements.clear();
+  } else if (!is_top_level_bookmarks_container) {
+    if (title)
+      path_elements.push_back(base::SysNSStringToWide(title));
+  }
+
+  NSArray* elements = [bookmark_folder objectForKey:@"Children"];
+  // TODO(jeremy) Does Chrome support importing empty folders?
+  if (!elements)
+    return;
+
+  // Iterate over individual bookmarks.
+  for (NSDictionary* bookmark in elements) {
+    NSString* type = [bookmark objectForKey:@"WebBookmarkType"];
+    if (!type)
+      continue;
+
+    // If this is a folder, recurse.
+    if ([type isEqualToString:@"WebBookmarkTypeList"]) {
+      RecursiveReadBookmarksFolder(bookmark,
+                                   path_elements,
+                                   is_in_toolbar,
+                                   out_bookmarks);
+    }
+
+    // If we didn't see a bookmark folder, then we're expecting a bookmark
+    // item, if that's not what we got then ignore it.
+    if (![type isEqualToString:@"WebBookmarkTypeLeaf"])
+      continue;
+
+    NSString* url = [bookmark objectForKey:@"URLString"];
+    NSString* title = [[bookmark objectForKey:@"URIDictionary"]
+        objectForKey:@"title"];
+
+    if (!url || !title)
+      continue;
+
+    // Output Bookmark.
+    ProfileWriter::BookmarkEntry entry;
+    // Safari doesn't specify a creation time for the bookmark.
+    entry.creation_time = base::Time::Now();
+    entry.title = base::SysNSStringToWide(title);
+    entry.url = GURL(base::SysNSStringToUTF8(url));
+    entry.path = path_elements;
+    entry.in_toolbar = is_in_toolbar;
+
+    out_bookmarks->push_back(entry);
+  }
+}
+
+void SafariImporter::ParseBookmarks(
+    std::vector<ProfileWriter::BookmarkEntry>* bookmarks) {
+  DCHECK(bookmarks);
+
+  // Construct ~/Library/Safari/Bookmarks.plist path
+  NSString* library_dir = [NSString
+      stringWithUTF8String:library_dir_.value().c_str()];
+  NSString* safari_dir = [library_dir
+      stringByAppendingPathComponent:@"Safari"];
+  NSString* bookmarks_plist = [safari_dir
+    stringByAppendingPathComponent:@"Bookmarks.plist"];
+
+  // Load the plist file.
+  NSDictionary* bookmarks_dict = [NSDictionary
+      dictionaryWithContentsOfFile:bookmarks_plist];
+  if (!bookmarks_dict)
+    return;
+
+  // Recursively read in bookmarks.
+  std::vector<std::wstring> parent_path_elements;
+  RecursiveReadBookmarksFolder(bookmarks_dict, parent_path_elements, false,
+                               bookmarks);
 }
 
 void SafariImporter::ImportPasswords() {
@@ -126,13 +312,15 @@
   // Load the plist file.
   NSDictionary* history_dict = [NSDictionary
       dictionaryWithContentsOfFile:history_plist];
+  if (!history_dict)
+    return;
 
-  NSArray* safari_history_items = [history_dict valueForKey:@"WebHistoryDates"];
+  NSArray* safari_history_items = [history_dict objectForKey:@"WebHistoryDates"];
 
   for (NSDictionary* history_item in safari_history_items) {
     using base::SysNSStringToUTF8;
     using base::SysNSStringToWide;
-    NSString* url_ns = [history_item valueForKey:@""];
+    NSString* url_ns = [history_item objectForKey:@""];
     if (!url_ns)
       continue;
 
@@ -142,7 +330,7 @@
       continue;
 
     history::URLRow row(url);
-    NSString* title_ns = [history_item valueForKey:@"title"];
+    NSString* title_ns = [history_item objectForKey:@"title"];
 
     // Sometimes items don't have a title, in which case we just substitue
     // the url.
@@ -150,7 +338,7 @@
       title_ns = url_ns;
 
     row.set_title(SysNSStringToWide(title_ns));
-    int visit_count = [[history_item valueForKey:@"visitCount"]
+    int visit_count = [[history_item objectForKey:@"visitCount"]
                           intValue];
     row.set_visit_count(visit_count);
     // Include imported URLs in autocompletion - don't hide them.
@@ -158,7 +346,7 @@
     // Item was never typed before in the omnibox.
     row.set_typed_count(0);
 
-    NSString* last_visit_str = [history_item valueForKey:@"lastVisitedDate"];
+    NSString* last_visit_str = [history_item objectForKey:@"lastVisitedDate"];
     // The last visit time should always be in the history item, but if not
     /// just continue without this item.
     DCHECK(last_visit_str);
@@ -188,36 +376,3 @@
         &ProfileWriter::AddHomepage, homepage));
   }
 }
-
-// TODO(jeremy): This is temporary, just copied from the FF import code in case
-// we need it, clean this up when writing favicon import code.
-// static
-void SafariImporter::DataURLToFaviconUsage(
-    const GURL& link_url,
-    const GURL& favicon_data,
-    std::vector<history::ImportedFavIconUsage>* favicons) {
-  if (!link_url.is_valid() || !favicon_data.is_valid() ||
-      !favicon_data.SchemeIs(chrome::kDataScheme))
-    return;
-
-  // Parse the data URL.
-  std::string mime_type, char_set, data;
-  if (!net::DataURL::Parse(favicon_data, &mime_type, &char_set, &data) ||
-      data.empty())
-    return;
-
-  history::ImportedFavIconUsage usage;
-  if (!ReencodeFavicon(reinterpret_cast<const unsigned char*>(&data[0]),
-                       data.size(), &usage.png_data))
-    return;  // Unable to decode.
-
-  // We need to make up a URL for the favicon. We use a version of the page's
-  // URL so that we can be sure it will not collide.
-  usage.favicon_url = GURL(std::string("made-up-favicon:") + link_url.spec());
-
-  // We only have one URL per favicon for Firefox 2 bookmarks.
-  usage.urls.insert(link_url);
-
-  favicons->push_back(usage);
-}
-
diff --git a/chrome/browser/importer/safari_importer_unittest.mm b/chrome/browser/importer/safari_importer_unittest.mm
index af0ae9e..78b4f65 100644
--- a/chrome/browser/importer/safari_importer_unittest.mm
+++ b/chrome/browser/importer/safari_importer_unittest.mm
@@ -4,6 +4,7 @@
 
 #include "chrome/browser/importer/safari_importer.h"
 
+#include "base/basictypes.h"
 #include "base/file_path.h"
 #include "base/file_util.h"
 #include "base/path_service.h"
@@ -19,28 +20,32 @@
     std::wstring test_dir_wstring;
     PathService::Get(chrome::DIR_TEST_DATA, &test_dir_wstring);
     FilePath test_dir = FilePath::FromWStringHack(test_dir_wstring);
-    
+
     // Our simulated ~/Library directory
     test_dir = test_dir.Append("safari_import");
     return test_dir;
 }
 
-class SafariImporterTest : public PlatformTest {};
+class SafariImporterTest : public PlatformTest {
+ public:
+  SafariImporter* GetSafariImporter() {
+    FilePath test_library_dir = GetTestSafariLibraryPath();
+    CHECK(file_util::PathExists(test_library_dir))  <<
+        "Missing test data directory";
+
+    return new SafariImporter(test_library_dir);
+  }
+};
 
 TEST_F(SafariImporterTest, HistoryImport) {
-  FilePath test_library_dir = GetTestSafariLibraryPath();
-  ASSERT_TRUE(file_util::PathExists(test_library_dir))  <<
-      "Missing test data directory";
+  scoped_refptr<SafariImporter> importer(GetSafariImporter());
 
-  scoped_refptr<SafariImporter> importer(
-      new SafariImporter(test_library_dir));
-  
   std::vector<history::URLRow> history_items;
   importer->ParseHistoryItems(&history_items);
-  
+
   // Should be 2 history items.
   ASSERT_EQ(history_items.size(), 2U);
-  
+
   history::URLRow& it1 = history_items[0];
   EXPECT_EQ(it1.url(), GURL("http://www.firsthistoryitem.com/"));
   EXPECT_EQ(it1.title(), L"First History Item Title");
@@ -49,7 +54,7 @@
   EXPECT_EQ(it1.typed_count(), 0);
   EXPECT_EQ(it1.last_visit().ToDoubleT(),
       importer->HistoryTimeToEpochTime(@"270598264.4"));
-  
+
   history::URLRow& it2 = history_items[1];
   std::string second_item_title("http://www.secondhistoryitem.com/");
   EXPECT_EQ(it2.url(), GURL(second_item_title));
@@ -61,3 +66,80 @@
   EXPECT_EQ(it2.last_visit().ToDoubleT(),
       importer->HistoryTimeToEpochTime(@"270598231.4"));
 }
+
+TEST_F(SafariImporterTest, BookmarkImport) {
+  // Expected results
+  const struct {
+    bool in_toolbar;
+    GURL url;
+    // If the path array for an element is entry set this to true.
+    bool path_is_empty;
+    // We ony support one level of nesting in paths, this makes testing a little
+    // easier.
+    std::wstring path;
+    std::wstring title;
+  } kImportedBookmarksData[] = {
+    {true, GURL("http://www.apple.com/"), true, L"", L"Apple"},
+    {true, GURL("http://www.yahoo.com/"), true, L"", L"Yahoo!"},
+    {true, GURL("http://www.cnn.com/"), false, L"News", L"CNN"},
+    {true, GURL("http://www.nytimes.com/"), false, L"News",
+        L"The New York Times"},
+    {false, GURL("http://www.reddit.com/"), true, L"",
+        L"reddit.com: what's new online!"},
+  };
+
+  scoped_refptr<SafariImporter> importer(GetSafariImporter());
+  std::vector<ProfileWriter::BookmarkEntry> bookmarks;
+  importer->ParseBookmarks(&bookmarks);
+  size_t num_bookmarks = bookmarks.size();
+  EXPECT_EQ(num_bookmarks, ARRAYSIZE_UNSAFE(kImportedBookmarksData));
+
+  for (size_t i = 0; i < num_bookmarks; ++i) {
+    ProfileWriter::BookmarkEntry& entry = bookmarks[i];
+    EXPECT_EQ(entry.in_toolbar, kImportedBookmarksData[i].in_toolbar);
+    EXPECT_EQ(entry.url, kImportedBookmarksData[i].url);
+    if (kImportedBookmarksData[i].path_is_empty) {
+      EXPECT_EQ(entry.path.size(), 0U);
+    } else {
+      EXPECT_EQ(entry.path.size(), 1U);
+      EXPECT_EQ(entry.path[0], kImportedBookmarksData[i].path);
+      EXPECT_EQ(entry.title, kImportedBookmarksData[i].title);
+    }
+  }
+
+}
+
+TEST_F(SafariImporterTest, FavIconImport) {
+  scoped_refptr<SafariImporter> importer(GetSafariImporter());
+  sqlite_utils::scoped_sqlite_db_ptr db(importer->OpenFavIconDB());
+  ASSERT_TRUE(db.get() != NULL);
+
+  SafariImporter::FaviconMap favicon_map;
+  importer->ImportFavIconURLs(db.get(), &favicon_map);
+
+  std::vector<history::ImportedFavIconUsage> favicons;
+  importer->LoadFaviconData(db.get(), favicon_map, &favicons);
+
+  size_t num_favicons = favicons.size();
+  ASSERT_EQ(num_favicons, 2U);
+
+  history::ImportedFavIconUsage &fav0 = favicons[0];
+  EXPECT_EQ("http://s.ytimg.com/yt/favicon-vfl86270.ico",
+            fav0.favicon_url.spec());
+  EXPECT_GT(fav0.png_data.size(), 0U);
+  EXPECT_EQ(fav0.urls.size(), 1U);
+  EXPECT_TRUE(fav0.urls.find(GURL("http://www.youtube.com/"))
+      != fav0.urls.end());
+
+  history::ImportedFavIconUsage &fav1 = favicons[1];
+  EXPECT_EQ("http://www.opensearch.org/favicon.ico",
+            fav1.favicon_url.spec());
+  EXPECT_GT(fav1.png_data.size(), 0U);
+  EXPECT_EQ(fav1.urls.size(), 2U);
+  EXPECT_TRUE(fav1.urls.find(GURL("http://www.opensearch.org/Home"))
+      != fav1.urls.end());
+
+  EXPECT_TRUE(fav1.urls.find(
+      GURL("http://www.opensearch.org/Special:Search?search=lalala&go=Search"))
+          != fav1.urls.end());
+}
diff --git a/chrome/common/sqlite_utils.h b/chrome/common/sqlite_utils.h
index bacbef0..33915d1 100644
--- a/chrome/common/sqlite_utils.h
+++ b/chrome/common/sqlite_utils.h
@@ -9,6 +9,7 @@
 #include <vector>
 
 #include "base/basictypes.h"
+#include "base/scoped_ptr.h"
 #include "base/string16.h"
 #include "base/string_util.h"
 
@@ -197,6 +198,25 @@
   DISALLOW_COPY_AND_ASSIGN(scoped_sqlite3_stmt_ptr);
 };
 
+//------------------------------------------------------------------------------
+// A scoped sqlite database that closes when it goes out of scope.
+//------------------------------------------------------------------------------
+
+// TODO: Use this namespace for the functions below (see TODO further down by
+// estade).
+namespace sqlite_utils {
+
+class DBClose {
+ public:
+  inline void operator()(sqlite3* x) const {
+    sqlite3_close(x);
+  }
+};
+
+typedef scoped_ptr_malloc<sqlite3, DBClose> scoped_sqlite_db_ptr;
+
+}  // namespace sqlite_utils
+
 
 //------------------------------------------------------------------------------
 // A scoped sqlite statement with convenient C++ wrappers for sqlite3 APIs.
diff --git a/chrome/test/data/safari_import/Safari/Bookmarks.plist b/chrome/test/data/safari_import/Safari/Bookmarks.plist
new file mode 100644
index 0000000..df95780
--- /dev/null
+++ b/chrome/test/data/safari_import/Safari/Bookmarks.plist
Binary files differ
diff --git a/chrome/test/data/safari_import/Safari/WebpageIcons.db b/chrome/test/data/safari_import/Safari/WebpageIcons.db
new file mode 100644
index 0000000..03f15deed
--- /dev/null
+++ b/chrome/test/data/safari_import/Safari/WebpageIcons.db
Binary files differ