Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(174)

Unified Diff: chrome/utility/importer/edge_importer_win.cc

Issue 1465853002: Implement support for importing favorites from Edge on Windows 10. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix another CLANG warning Created 5 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/utility/importer/edge_importer_win.cc
diff --git a/chrome/utility/importer/edge_importer_win.cc b/chrome/utility/importer/edge_importer_win.cc
new file mode 100644
index 0000000000000000000000000000000000000000..8497035ff9d1a5c1ccda1ea30ffd4586717cf905
--- /dev/null
+++ b/chrome/utility/importer/edge_importer_win.cc
@@ -0,0 +1,557 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/utility/importer/edge_importer_win.h"
+
+#define JET_UNICODE
+#include <esent.h>
+#undef JET_UNICODE
+#include <Shlobj.h>
+
+#include <algorithm>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/files/file_enumerator.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/strings/string16.h"
+#include "base/strings/string_util.h"
+#include "base/time/time.h"
+#include "base/win/windows_version.h"
+
Ilya Sherman 2015/11/26 02:04:45 nit: This line break is typically omitted in Chrom
forshaw 2015/11/30 12:57:58 Acknowledged.
+#include "chrome/common/importer/edge_importer_utils_win.h"
+#include "chrome/common/importer/imported_bookmark_entry.h"
+#include "chrome/common/importer/importer_bridge.h"
+#include "chrome/grit/generated_resources.h"
+#include "chrome/utility/importer/favicon_reencode.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "url/gurl.h"
+
+namespace {
+
+// Toolbar favorites are placed under this special folder name.
+const base::char16 kFavoritesBarTitle[] = L"_Favorites_Bar_";
+const base::char16 kSpartanDatabaseFile[] = L"spartan.edb";
+
+class EdgeErrorObject {
+ public:
+ EdgeErrorObject() : last_error_(JET_errSuccess) {}
+
+ base::string16 GetErrorMessage() const {
+ WCHAR error_message[1024] = {};
Ilya Sherman 2015/11/26 02:04:45 nit: Why 1024?
forshaw 2015/11/30 12:57:58 Just an arbitrary size, the API provides no way of
+ JET_API_PTR err = last_error_;
+ JET_ERR result = JetGetSystemParameter(
+ JET_instanceNil, JET_sesidNil, JET_paramErrorToString, &err,
+ error_message, sizeof(error_message));
+ if (result != JET_errSuccess)
+ return L"";
+
+ return error_message;
+ }
+
+ protected:
+ JET_ERR last_error() const { return last_error_; }
+
+ bool set_last_error(JET_ERR error) {
Ilya Sherman 2015/11/26 02:04:44 Please document what the return value means. Also
forshaw 2015/11/30 12:57:58 Acknowledged.
+ last_error_ = error;
+ if (error == JET_errSuccess)
+ return true;
+ return false;
Ilya Sherman 2015/11/26 02:04:44 nit: "return error == JET_errSuccess;"
forshaw 2015/11/30 12:57:59 Acknowledged.
+ }
+
+ private:
+ JET_ERR last_error_;
+
+ DISALLOW_COPY_AND_ASSIGN(EdgeErrorObject);
+};
+
+class EdgeDatabaseTableEnumerator : public EdgeErrorObject {
+ public:
+ EdgeDatabaseTableEnumerator(const base::string16& table_name,
+ JET_SESID session_id,
+ JET_TABLEID table_id)
+ : table_id_(table_id), table_name_(table_name), session_id_(session_id) {}
+
+ ~EdgeDatabaseTableEnumerator() {
+ if (table_id_ != JET_tableidNil) {
+ JetCloseTable(session_id_, table_id_);
+ table_id_ = JET_tableidNil;
Ilya Sherman 2015/11/26 02:04:44 Why is this line needed, given that it's in the de
forshaw 2015/11/30 12:57:58 I tend to do this to reduce risk of breakage if it
+ }
+ }
+
+ const base::string16& table_name() { return table_name_; }
+
+ bool Reset() {
+ return set_last_error(JetMove(session_id_, table_id_, JET_MoveFirst, 0));
+ }
+
+ bool Next() {
+ return set_last_error(JetMove(session_id_, table_id_, JET_MoveNext, 0));
+ }
+
+ template <typename T>
+ bool RetrieveColumn(const base::string16& column_name,
+ T& value,
+ bool& is_null) {
Ilya Sherman 2015/11/26 02:04:44 Please pass these by pointer rather than by refere
forshaw 2015/11/30 12:57:58 Acknowledged.
+ const JET_COLUMNBASE& column_base = GetColumnByName(column_name);
+ if (column_base.cbMax == 0)
+ return false;
+ std::vector<uint8_t> column_data(column_base.cbMax);
+ unsigned long actual_size = 0;
+ JET_ERR err = JetRetrieveColumn(
+ session_id_, table_id_, column_base.columnid, &column_data[0],
+ column_data.size(), &actual_size, 0, nullptr);
+ if (err != JET_errSuccess && err != JET_wrnColumnNull) {
+ set_last_error(err);
Ilya Sherman 2015/11/26 02:04:44 Is it important whether or not set_last_error is e
forshaw 2015/11/30 12:57:59 Not really important, just in most cases there's n
+ return false;
+ }
+
+ if (err == JET_errSuccess) {
+ column_data.resize(actual_size);
+ if (!ValidateAndConvertValue(column_base.coltyp, column_data, value))
+ return false;
+ } else {
+ value = T();
+ }
+
+ is_null = err == JET_wrnColumnNull;
+ return true;
+ }
+
+ template <typename T>
+ bool RetrieveColumn(const base::string16& column_name, T& value) {
Ilya Sherman 2015/11/26 02:04:45 Chromium style generally speaking frowns upon defa
forshaw 2015/11/30 12:57:58 Well I'd argue that it isn't providing default par
+ bool is_null;
+ return RetrieveColumn(column_name, value, is_null);
+ }
+
+ private:
+ template <typename T>
+ bool ValidateAndConvertValueGeneric(const JET_COLTYP match_column_type,
+ const JET_COLTYP column_type,
+ const std::vector<uint8_t>& column_data,
+ T& value) {
+ if ((column_type == match_column_type) &&
+ (column_data.size() >= sizeof(value))) {
Ilya Sherman 2015/11/26 02:04:44 Why is this >= rather than simply =? Truncating d
forshaw 2015/11/30 12:57:58 The database engine is one of the ones where the t
+ memcpy(&value, &column_data[0], sizeof(value));
+ return true;
+ }
+ set_last_error(JET_errInvalidColumnType);
+ return false;
+ }
+
+ bool ValidateAndConvertValue(const JET_COLTYP column_type,
+ const std::vector<uint8_t>& column_data,
+ bool& value) {
+ if ((column_type == JET_coltypBit) && (column_data.size() >= 1)) {
+ value = (column_data[0] & 1) == 1;
+ return true;
+ }
+ set_last_error(JET_errInvalidColumnType);
+ return false;
+ }
+
+ bool ValidateAndConvertValue(const JET_COLTYP column_type,
+ const std::vector<uint8_t>& column_data,
+ base::string16& value) {
+ if ((column_type == JET_coltypLongText) &&
+ ((column_data.size() % sizeof(base::char16)) == 0)) {
+ const base::char16* ptr =
+ reinterpret_cast<const base::char16*>(&column_data[0]);
Ilya Sherman 2015/11/26 02:04:44 I may be wrong, I don't think that this is a safe
forshaw 2015/11/30 12:57:59 Reworked
+ size_t length = column_data.size() / sizeof(base::char16);
+ // Remove any trailing NUL characters.
+ while (length > 0) {
+ if (ptr[length - 1])
+ break;
+ length--;
+ }
+ value = base::string16(ptr, length);
+ return true;
+ }
+ set_last_error(JET_errInvalidColumnType);
+ return false;
+ }
+
+ bool ValidateAndConvertValue(const JET_COLTYP column_type,
+ const std::vector<uint8_t>& column_data,
+ GUID& value) {
+ return ValidateAndConvertValueGeneric(JET_coltypGUID, column_type,
+ column_data, value);
+ }
+
+ bool ValidateAndConvertValue(const JET_COLTYP column_type,
+ const std::vector<uint8_t>& column_data,
+ int32_t& value) {
+ return ValidateAndConvertValueGeneric(JET_coltypLong, column_type,
+ column_data, value);
+ }
+
+ bool ValidateAndConvertValue(const JET_COLTYP column_type,
+ const std::vector<uint8_t>& column_data,
+ int64_t& value) {
+ return ValidateAndConvertValueGeneric(JET_coltypLongLong, column_type,
+ column_data, value);
+ }
+
+ bool ValidateAndConvertValue(const JET_COLTYP column_type,
+ const std::vector<uint8_t>& column_data,
+ FILETIME& value) {
+ return ValidateAndConvertValueGeneric(JET_coltypLongLong, column_type,
+ column_data, value);
+ }
+
+ bool ValidateAndConvertValue(const JET_COLTYP column_type,
+ const std::vector<uint8_t>& column_data,
+ uint32_t& value) {
+ return ValidateAndConvertValueGeneric(JET_coltypUnsignedLong, column_type,
+ column_data, value);
+ }
+
+ const JET_COLUMNBASE& GetColumnByName(const base::string16& column_name) {
+ auto found_col = columns_by_name_.find(column_name);
+ if (found_col == columns_by_name_.end()) {
+ JET_COLUMNBASE column_base = {};
+ column_base.cbStruct = sizeof(JET_COLUMNBASE);
+ if (!set_last_error(JetGetTableColumnInfo(
+ session_id_, table_id_, column_name.c_str(), &column_base,
+ sizeof(column_base), JET_ColInfoBase))) {
+ // 0 means we'll fail to extract the column data.
+ column_base.cbMax = 0;
+ }
+ columns_by_name_[column_name] = column_base;
+ found_col = columns_by_name_.find(column_name);
+ }
+ return found_col->second;
+ }
+
+ std::map<const base::string16, JET_COLUMNBASE> columns_by_name_;
+ JET_TABLEID table_id_;
+ base::string16 table_name_;
+ JET_SESID session_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(EdgeDatabaseTableEnumerator);
+};
+
+class EdgeDatabaseSession : public EdgeErrorObject {
+ public:
+ EdgeDatabaseSession()
+ : db_id_(JET_dbidNil),
+ instance_id_(JET_instanceNil),
+ session_id_(JET_sesidNil) {}
+
+ ~EdgeDatabaseSession() {
+ // We don't need to collect other ID handles, terminating instance
+ // is enough to shut the entire session down.
+ if (instance_id_ != JET_instanceNil) {
+ JetTerm(instance_id_);
+ instance_id_ = JET_instanceNil;
Ilya Sherman 2015/11/26 02:04:44 nit: Why is this line needed?
forshaw 2015/11/30 12:57:59 As above but removed.
+ }
+ }
+
+ bool OpenDatabase(const base::string16& database_file) {
+ if (IsOpen()) {
+ set_last_error(JET_errOneDatabasePerSession);
+ return false;
+ }
+ if (!set_last_error(JetSetSystemParameter(
+ nullptr, JET_sesidNil, JET_paramDatabasePageSize, 8192, nullptr)))
Ilya Sherman 2015/11/26 02:04:44 Why 8192? Is that a guaranteed constant, or might
forshaw 2015/11/30 12:57:58 In theory it could change but it's unlikely to do
+ return false;
+ if (!set_last_error(JetCreateInstance(&instance_id_, L"EdgeDataImporter")))
+ return false;
+ if (!set_last_error(JetSetSystemParameter(&instance_id_, JET_sesidNil,
+ JET_paramRecovery, 0, L"Off")))
+ return false;
+ if (!set_last_error(JetInit(&instance_id_)))
+ return false;
+ if (!set_last_error(
+ JetBeginSession(instance_id_, &session_id_, nullptr, nullptr)))
+ return false;
+ if (!set_last_error(JetAttachDatabase2(session_id_, database_file.c_str(),
+ 0, JET_bitDbReadOnly)))
+ return false;
+ if (!set_last_error(JetOpenDatabase(session_id_, database_file.c_str(),
+ nullptr, &db_id_, JET_bitDbReadOnly)))
+ return false;
+ return true;
+ }
+
+ scoped_ptr<EdgeDatabaseTableEnumerator> OpenTableEnumerator(
+ const base::string16& table_name) {
+ JET_TABLEID table_id;
+
+ if (!IsOpen()) {
+ set_last_error(JET_errDatabaseNotFound);
+ return nullptr;
+ }
+
+ if (!set_last_error(JetOpenTable(session_id_, db_id_, table_name.c_str(),
+ nullptr, 0, JET_bitTableReadOnly,
+ &table_id)))
+ return nullptr;
+
+ return scoped_ptr<EdgeDatabaseTableEnumerator>(
Ilya Sherman 2015/11/26 02:04:44 nit: You can use make_scoped_ptr here.
forshaw 2015/11/30 12:57:58 Acknowledged.
+ new EdgeDatabaseTableEnumerator(table_name, session_id_, table_id));
+ }
+
+ private:
+ bool IsOpen() { return instance_id_ != JET_instanceNil; }
+
+ JET_DBID db_id_;
+ JET_INSTANCE instance_id_;
+ JET_SESID session_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(EdgeDatabaseSession);
+};
+
+struct EdgeFavoriteEntry {
+ EdgeFavoriteEntry()
+ : is_folder(false),
+ order_number(0),
+ item_id(GUID_NULL),
+ parent_id(GUID_NULL) {}
+
+ base::string16 title;
+ GURL url;
+ base::FilePath favicon_file;
+ bool is_folder;
+ int64_t order_number;
+ base::Time date_updated;
+ GUID item_id;
+ GUID parent_id;
+
+ std::vector<EdgeFavoriteEntry*> children;
Ilya Sherman 2015/11/26 02:04:44 nit: Can the items be const?
forshaw 2015/11/30 12:57:58 I've moved the sorting of the children out of the
+
+ ImportedBookmarkEntry ToBookmarkEntry(
+ bool in_toolbar,
+ const std::vector<base::string16>& path) {
+ ImportedBookmarkEntry entry;
+ entry.in_toolbar = in_toolbar;
+ entry.is_folder = is_folder;
+ entry.url = url;
+ entry.path = path;
+ entry.title = title;
+ entry.creation_time = date_updated;
+ return entry;
+ }
+};
+
+struct EdgeFavoriteEntryComparator {
+ bool operator()(const EdgeFavoriteEntry* lhs,
+ const EdgeFavoriteEntry* rhs) const {
+ if (lhs->order_number == rhs->order_number)
+ return lhs->title < rhs->title;
+ else
+ return lhs->order_number < rhs->order_number;
Ilya Sherman 2015/11/26 02:04:44 nit: Please implement this using std::tie: https:/
forshaw 2015/11/30 12:57:59 Acknowledged.
+ }
+};
+
+// The name of the database file is spartan.edb, however it isn't clear how
+// the intermediate path between the DataStore and the database is generated.
+// Therefore we just do a simple recursive search until we find a matching name.
+base::FilePath FindSpartanDatabase(base::FilePath profile_path) {
Ilya Sherman 2015/11/26 02:04:44 nit: Pass by const-ref?
forshaw 2015/11/30 12:57:58 Acknowledged.
+ base::FilePath data_path =
+ profile_path.empty() ? importer::GetEdgeDataFilePath() : profile_path;
Ilya Sherman 2015/11/26 02:04:44 Are both of these cases possible? I'd kind of exp
forshaw 2015/11/30 12:57:58 GetEdgeDataFilePath could also return an empty pat
+ if (data_path.empty())
+ return base::FilePath();
+
+ base::FileEnumerator enumerator(data_path.Append(L"DataStore\\Data"), true,
+ base::FileEnumerator::FILES);
+ base::FilePath path = enumerator.Next();
+ while (!path.empty()) {
+ if (base::EqualsCaseInsensitiveASCII(path.BaseName().value(),
+ kSpartanDatabaseFile))
+ return path;
+ path = enumerator.Next();
+ }
+ return base::FilePath();
+}
+
+struct GuidComparator {
+ bool operator()(const GUID& a, const GUID& b) const {
+ return memcmp(&a, &b, sizeof(a)) < 0;
+ }
+};
+
+bool ReadFaviconData(const base::FilePath& file,
+ std::vector<unsigned char>* data) {
+ std::string image_data;
+ if (!base::ReadFileToString(file, &image_data))
+ return false;
+
+ const unsigned char* ptr =
+ reinterpret_cast<const unsigned char*>(image_data.c_str());
+ return importer::ReencodeFavicon(ptr, image_data.size(), data);
+}
+
+void BuildBookmarkEntries(EdgeFavoriteEntry* current_entry,
+ std::vector<ImportedBookmarkEntry>* bookmarks,
+ favicon_base::FaviconUsageDataList* favicons,
Ilya Sherman 2015/11/26 02:04:44 nit: Outparams are usually written after the in-pa
forshaw 2015/11/30 12:57:58 Acknowledged.
+ bool is_toolbar,
+ std::vector<base::string16>& path) {
+ // Sort the children first by order number and then by title.
+ std::sort(current_entry->children.begin(), current_entry->children.end(),
+ EdgeFavoriteEntryComparator());
+ for (EdgeFavoriteEntry* entry : current_entry->children) {
+ if (entry->is_folder) {
+ // If the favorites bar then load all children as toolbar items.
+ if (base::EqualsCaseInsensitiveASCII(entry->title, kFavoritesBarTitle)) {
+ // Replace name with Links similar to IE.
+ path.push_back(L"Links");
+ BuildBookmarkEntries(entry, bookmarks, favicons, true, path);
+ path.pop_back();
+ } else {
+ path.push_back(entry->title);
+ BuildBookmarkEntries(entry, bookmarks, favicons, is_toolbar, path);
+ path.pop_back();
+ }
+ } else {
+ bookmarks->push_back(entry->ToBookmarkEntry(is_toolbar, path));
+ favicon_base::FaviconUsageData favicon;
+ if (entry->url.is_valid() && !entry->favicon_file.empty() &&
+ ReadFaviconData(entry->favicon_file, &favicon.png_data)) {
+ // As the database doesn't provide us a favicon URL we'll fake one.
+ GURL::Replacements path_replace;
+ path_replace.SetPathStr("/favicon.ico");
+ favicon.favicon_url =
+ entry->url.GetWithEmptyPath().ReplaceComponents(path_replace);
+ favicon.urls.insert(entry->url);
+ favicons->push_back(favicon);
+ }
+ }
+ }
+}
+
+} // namespace
+
+EdgeImporter::EdgeImporter() {}
+
+void EdgeImporter::StartImport(const importer::SourceProfile& source_profile,
+ uint16 items,
+ ImporterBridge* bridge) {
+ bridge_ = bridge;
+ bridge_->NotifyStarted();
+ source_path_ = source_profile.source_path;
+
+ if ((items & importer::FAVORITES) && !cancelled()) {
+ bridge_->NotifyItemStarted(importer::FAVORITES);
+ ImportFavorites();
+ bridge_->NotifyItemEnded(importer::FAVORITES);
+ }
+ bridge_->NotifyEnded();
+}
+
+EdgeImporter::~EdgeImporter() {}
+
+void EdgeImporter::ImportFavorites() {
+ BookmarkVector bookmarks;
+ favicon_base::FaviconUsageDataList favicons;
+ ParseFavoritesDatabase(&bookmarks, &favicons);
+
+ if (!bookmarks.empty() && !cancelled()) {
+ const base::string16& first_folder_name =
+ l10n_util::GetStringUTF16(IDS_BOOKMARK_GROUP_FROM_EDGE);
+ bridge_->AddBookmarks(bookmarks, first_folder_name);
+ }
+ if (!favicons.empty() && !cancelled())
+ bridge_->SetFavicons(favicons);
+}
+
+// From Edge 13 (released with Windows 10 TH2) Favorites are stored in a JET
Ilya Sherman 2015/11/26 02:04:44 nit: Please add a comma after the closing paren.
forshaw 2015/11/30 12:57:58 Acknowledged.
+// database within the Edge local storage. The import uses the ESE library to
+// open and read the data file. The data is stored in a Favorites table with
+// the following schema.
+// Column Name Column Type
+// ------------------------------------------
+// DateUpdated LongLong - FILETIME
+// FaviconFile LongText - Relative path
+// HashedUrl ULong
+// IsDeleted Bit
+// IsFolder Bit
+// ItemId Guid
+// OrderNumber LongLong
+// ParentId Guid
+// RoamDisabled Bit
+// RowId Long
+// Title LongText
+// URL LongText
+void EdgeImporter::ParseFavoritesDatabase(
+ BookmarkVector* bookmarks,
+ favicon_base::FaviconUsageDataList* favicons) {
+ base::FilePath database_path = FindSpartanDatabase(source_path_);
+ if (database_path.empty())
+ return;
+
+ EdgeDatabaseSession database;
+ if (!database.OpenDatabase(database_path.value())) {
+ DLOG(ERROR) << "Error opening database " << database.GetErrorMessage();
Ilya Sherman 2015/11/26 02:04:44 Optional nit: You probably want DVLOG or just LOG.
forshaw 2015/11/30 12:57:58 Acknowledged.
+ return;
+ }
+
+ scoped_ptr<EdgeDatabaseTableEnumerator> enumerator =
+ database.OpenTableEnumerator(L"Favorites");
+ if (!enumerator) {
+ DLOG(ERROR) << "Error opening database table "
+ << database.GetErrorMessage();
+ return;
+ }
+
+ std::map<GUID, EdgeFavoriteEntry, GuidComparator> database_entries;
+ base::FilePath favicon_base =
+ source_path_.empty()
+ ? importer::GetEdgeDataFilePath().Append(L"DataStore")
+ : source_path_.Append(L"DataStore");
Ilya Sherman 2015/11/26 02:04:44 nit: I'd move the .Append(L"DataStore") out of the
forshaw 2015/11/30 12:57:59 Acknowledged.
+
+ enumerator->Reset();
+ do {
Ilya Sherman 2015/11/26 02:04:45 Is it possible for the enumerator to be empty?
forshaw 2015/11/30 12:57:58 Yes it could be, this would be caught by the loop
+ EdgeFavoriteEntry entry;
+ bool is_deleted = false;
+ if (!enumerator->RetrieveColumn(L"IsDeleted", is_deleted))
+ continue;
+ if (is_deleted)
+ continue;
+ if (!enumerator->RetrieveColumn(L"IsFolder", entry.is_folder))
+ continue;
+ base::string16 url;
+ if (!enumerator->RetrieveColumn(L"URL", url))
+ continue;
+ entry.url = GURL(url);
+ if (!entry.is_folder && !entry.url.is_valid())
+ continue;
+ if (!enumerator->RetrieveColumn(L"Title", entry.title))
+ continue;
+ base::string16 favicon_file;
+ if (!enumerator->RetrieveColumn(L"FaviconFile", favicon_file))
+ continue;
+ if (!favicon_file.empty())
+ entry.favicon_file = favicon_base.Append(favicon_file);
+ if (!enumerator->RetrieveColumn(L"ParentId", entry.parent_id))
+ continue;
+ if (!enumerator->RetrieveColumn(L"ItemId", entry.item_id))
+ continue;
+ if (!enumerator->RetrieveColumn(L"OrderNumber", entry.order_number))
+ continue;
+ FILETIME ft;
+ if (!enumerator->RetrieveColumn(L"DateUpdated", ft))
+ continue;
+ entry.date_updated = base::Time::FromFileTime(ft);
+ database_entries[entry.item_id] = entry;
+ } while (enumerator->Next() && !cancelled());
+
+ // Build simple tree.
+ EdgeFavoriteEntry root_entry;
+ for (auto& e : database_entries) {
Ilya Sherman 2015/11/26 02:04:44 nit: Can the entry be const?
Ilya Sherman 2015/11/26 02:04:44 nit: s/e/entry
forshaw 2015/11/30 12:57:58 Acknowledged.
forshaw 2015/11/30 12:57:58 In the loop? Don't think so unless I'm missing som
forshaw 2015/11/30 12:57:58 Acknowledged.
forshaw 2015/11/30 12:57:59 Acknowledged.
+ auto found_parent = database_entries.find(e.second.parent_id);
+ if (found_parent == database_entries.end() ||
+ !found_parent->second.is_folder) {
+ root_entry.children.push_back(&e.second);
+ } else {
+ found_parent->second.children.push_back(&e.second);
+ }
+ }
+ std::vector<base::string16> path;
+ BuildBookmarkEntries(&root_entry, bookmarks, favicons, false, path);
+}

Powered by Google App Engine
This is Rietveld 408576698