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

Side by Side 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 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 unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/utility/importer/edge_importer_win.h"
6
7 #define JET_UNICODE
8 #include <esent.h>
9 #undef JET_UNICODE
10 #include <Shlobj.h>
11
12 #include <algorithm>
13 #include <map>
14 #include <string>
15 #include <vector>
16
17 #include "base/files/file_enumerator.h"
18 #include "base/files/file_path.h"
19 #include "base/files/file_util.h"
20 #include "base/memory/ref_counted.h"
21 #include "base/memory/scoped_ptr.h"
22 #include "base/strings/string16.h"
23 #include "base/strings/string_util.h"
24 #include "base/time/time.h"
25 #include "base/win/windows_version.h"
26
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.
27 #include "chrome/common/importer/edge_importer_utils_win.h"
28 #include "chrome/common/importer/imported_bookmark_entry.h"
29 #include "chrome/common/importer/importer_bridge.h"
30 #include "chrome/grit/generated_resources.h"
31 #include "chrome/utility/importer/favicon_reencode.h"
32 #include "ui/base/l10n/l10n_util.h"
33 #include "url/gurl.h"
34
35 namespace {
36
37 // Toolbar favorites are placed under this special folder name.
38 const base::char16 kFavoritesBarTitle[] = L"_Favorites_Bar_";
39 const base::char16 kSpartanDatabaseFile[] = L"spartan.edb";
40
41 class EdgeErrorObject {
42 public:
43 EdgeErrorObject() : last_error_(JET_errSuccess) {}
44
45 base::string16 GetErrorMessage() const {
46 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
47 JET_API_PTR err = last_error_;
48 JET_ERR result = JetGetSystemParameter(
49 JET_instanceNil, JET_sesidNil, JET_paramErrorToString, &err,
50 error_message, sizeof(error_message));
51 if (result != JET_errSuccess)
52 return L"";
53
54 return error_message;
55 }
56
57 protected:
58 JET_ERR last_error() const { return last_error_; }
59
60 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.
61 last_error_ = error;
62 if (error == JET_errSuccess)
63 return true;
64 return false;
Ilya Sherman 2015/11/26 02:04:44 nit: "return error == JET_errSuccess;"
forshaw 2015/11/30 12:57:59 Acknowledged.
65 }
66
67 private:
68 JET_ERR last_error_;
69
70 DISALLOW_COPY_AND_ASSIGN(EdgeErrorObject);
71 };
72
73 class EdgeDatabaseTableEnumerator : public EdgeErrorObject {
74 public:
75 EdgeDatabaseTableEnumerator(const base::string16& table_name,
76 JET_SESID session_id,
77 JET_TABLEID table_id)
78 : table_id_(table_id), table_name_(table_name), session_id_(session_id) {}
79
80 ~EdgeDatabaseTableEnumerator() {
81 if (table_id_ != JET_tableidNil) {
82 JetCloseTable(session_id_, table_id_);
83 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
84 }
85 }
86
87 const base::string16& table_name() { return table_name_; }
88
89 bool Reset() {
90 return set_last_error(JetMove(session_id_, table_id_, JET_MoveFirst, 0));
91 }
92
93 bool Next() {
94 return set_last_error(JetMove(session_id_, table_id_, JET_MoveNext, 0));
95 }
96
97 template <typename T>
98 bool RetrieveColumn(const base::string16& column_name,
99 T& value,
100 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.
101 const JET_COLUMNBASE& column_base = GetColumnByName(column_name);
102 if (column_base.cbMax == 0)
103 return false;
104 std::vector<uint8_t> column_data(column_base.cbMax);
105 unsigned long actual_size = 0;
106 JET_ERR err = JetRetrieveColumn(
107 session_id_, table_id_, column_base.columnid, &column_data[0],
108 column_data.size(), &actual_size, 0, nullptr);
109 if (err != JET_errSuccess && err != JET_wrnColumnNull) {
110 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
111 return false;
112 }
113
114 if (err == JET_errSuccess) {
115 column_data.resize(actual_size);
116 if (!ValidateAndConvertValue(column_base.coltyp, column_data, value))
117 return false;
118 } else {
119 value = T();
120 }
121
122 is_null = err == JET_wrnColumnNull;
123 return true;
124 }
125
126 template <typename T>
127 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
128 bool is_null;
129 return RetrieveColumn(column_name, value, is_null);
130 }
131
132 private:
133 template <typename T>
134 bool ValidateAndConvertValueGeneric(const JET_COLTYP match_column_type,
135 const JET_COLTYP column_type,
136 const std::vector<uint8_t>& column_data,
137 T& value) {
138 if ((column_type == match_column_type) &&
139 (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
140 memcpy(&value, &column_data[0], sizeof(value));
141 return true;
142 }
143 set_last_error(JET_errInvalidColumnType);
144 return false;
145 }
146
147 bool ValidateAndConvertValue(const JET_COLTYP column_type,
148 const std::vector<uint8_t>& column_data,
149 bool& value) {
150 if ((column_type == JET_coltypBit) && (column_data.size() >= 1)) {
151 value = (column_data[0] & 1) == 1;
152 return true;
153 }
154 set_last_error(JET_errInvalidColumnType);
155 return false;
156 }
157
158 bool ValidateAndConvertValue(const JET_COLTYP column_type,
159 const std::vector<uint8_t>& column_data,
160 base::string16& value) {
161 if ((column_type == JET_coltypLongText) &&
162 ((column_data.size() % sizeof(base::char16)) == 0)) {
163 const base::char16* ptr =
164 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
165 size_t length = column_data.size() / sizeof(base::char16);
166 // Remove any trailing NUL characters.
167 while (length > 0) {
168 if (ptr[length - 1])
169 break;
170 length--;
171 }
172 value = base::string16(ptr, length);
173 return true;
174 }
175 set_last_error(JET_errInvalidColumnType);
176 return false;
177 }
178
179 bool ValidateAndConvertValue(const JET_COLTYP column_type,
180 const std::vector<uint8_t>& column_data,
181 GUID& value) {
182 return ValidateAndConvertValueGeneric(JET_coltypGUID, column_type,
183 column_data, value);
184 }
185
186 bool ValidateAndConvertValue(const JET_COLTYP column_type,
187 const std::vector<uint8_t>& column_data,
188 int32_t& value) {
189 return ValidateAndConvertValueGeneric(JET_coltypLong, column_type,
190 column_data, value);
191 }
192
193 bool ValidateAndConvertValue(const JET_COLTYP column_type,
194 const std::vector<uint8_t>& column_data,
195 int64_t& value) {
196 return ValidateAndConvertValueGeneric(JET_coltypLongLong, column_type,
197 column_data, value);
198 }
199
200 bool ValidateAndConvertValue(const JET_COLTYP column_type,
201 const std::vector<uint8_t>& column_data,
202 FILETIME& value) {
203 return ValidateAndConvertValueGeneric(JET_coltypLongLong, column_type,
204 column_data, value);
205 }
206
207 bool ValidateAndConvertValue(const JET_COLTYP column_type,
208 const std::vector<uint8_t>& column_data,
209 uint32_t& value) {
210 return ValidateAndConvertValueGeneric(JET_coltypUnsignedLong, column_type,
211 column_data, value);
212 }
213
214 const JET_COLUMNBASE& GetColumnByName(const base::string16& column_name) {
215 auto found_col = columns_by_name_.find(column_name);
216 if (found_col == columns_by_name_.end()) {
217 JET_COLUMNBASE column_base = {};
218 column_base.cbStruct = sizeof(JET_COLUMNBASE);
219 if (!set_last_error(JetGetTableColumnInfo(
220 session_id_, table_id_, column_name.c_str(), &column_base,
221 sizeof(column_base), JET_ColInfoBase))) {
222 // 0 means we'll fail to extract the column data.
223 column_base.cbMax = 0;
224 }
225 columns_by_name_[column_name] = column_base;
226 found_col = columns_by_name_.find(column_name);
227 }
228 return found_col->second;
229 }
230
231 std::map<const base::string16, JET_COLUMNBASE> columns_by_name_;
232 JET_TABLEID table_id_;
233 base::string16 table_name_;
234 JET_SESID session_id_;
235
236 DISALLOW_COPY_AND_ASSIGN(EdgeDatabaseTableEnumerator);
237 };
238
239 class EdgeDatabaseSession : public EdgeErrorObject {
240 public:
241 EdgeDatabaseSession()
242 : db_id_(JET_dbidNil),
243 instance_id_(JET_instanceNil),
244 session_id_(JET_sesidNil) {}
245
246 ~EdgeDatabaseSession() {
247 // We don't need to collect other ID handles, terminating instance
248 // is enough to shut the entire session down.
249 if (instance_id_ != JET_instanceNil) {
250 JetTerm(instance_id_);
251 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.
252 }
253 }
254
255 bool OpenDatabase(const base::string16& database_file) {
256 if (IsOpen()) {
257 set_last_error(JET_errOneDatabasePerSession);
258 return false;
259 }
260 if (!set_last_error(JetSetSystemParameter(
261 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
262 return false;
263 if (!set_last_error(JetCreateInstance(&instance_id_, L"EdgeDataImporter")))
264 return false;
265 if (!set_last_error(JetSetSystemParameter(&instance_id_, JET_sesidNil,
266 JET_paramRecovery, 0, L"Off")))
267 return false;
268 if (!set_last_error(JetInit(&instance_id_)))
269 return false;
270 if (!set_last_error(
271 JetBeginSession(instance_id_, &session_id_, nullptr, nullptr)))
272 return false;
273 if (!set_last_error(JetAttachDatabase2(session_id_, database_file.c_str(),
274 0, JET_bitDbReadOnly)))
275 return false;
276 if (!set_last_error(JetOpenDatabase(session_id_, database_file.c_str(),
277 nullptr, &db_id_, JET_bitDbReadOnly)))
278 return false;
279 return true;
280 }
281
282 scoped_ptr<EdgeDatabaseTableEnumerator> OpenTableEnumerator(
283 const base::string16& table_name) {
284 JET_TABLEID table_id;
285
286 if (!IsOpen()) {
287 set_last_error(JET_errDatabaseNotFound);
288 return nullptr;
289 }
290
291 if (!set_last_error(JetOpenTable(session_id_, db_id_, table_name.c_str(),
292 nullptr, 0, JET_bitTableReadOnly,
293 &table_id)))
294 return nullptr;
295
296 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.
297 new EdgeDatabaseTableEnumerator(table_name, session_id_, table_id));
298 }
299
300 private:
301 bool IsOpen() { return instance_id_ != JET_instanceNil; }
302
303 JET_DBID db_id_;
304 JET_INSTANCE instance_id_;
305 JET_SESID session_id_;
306
307 DISALLOW_COPY_AND_ASSIGN(EdgeDatabaseSession);
308 };
309
310 struct EdgeFavoriteEntry {
311 EdgeFavoriteEntry()
312 : is_folder(false),
313 order_number(0),
314 item_id(GUID_NULL),
315 parent_id(GUID_NULL) {}
316
317 base::string16 title;
318 GURL url;
319 base::FilePath favicon_file;
320 bool is_folder;
321 int64_t order_number;
322 base::Time date_updated;
323 GUID item_id;
324 GUID parent_id;
325
326 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
327
328 ImportedBookmarkEntry ToBookmarkEntry(
329 bool in_toolbar,
330 const std::vector<base::string16>& path) {
331 ImportedBookmarkEntry entry;
332 entry.in_toolbar = in_toolbar;
333 entry.is_folder = is_folder;
334 entry.url = url;
335 entry.path = path;
336 entry.title = title;
337 entry.creation_time = date_updated;
338 return entry;
339 }
340 };
341
342 struct EdgeFavoriteEntryComparator {
343 bool operator()(const EdgeFavoriteEntry* lhs,
344 const EdgeFavoriteEntry* rhs) const {
345 if (lhs->order_number == rhs->order_number)
346 return lhs->title < rhs->title;
347 else
348 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.
349 }
350 };
351
352 // The name of the database file is spartan.edb, however it isn't clear how
353 // the intermediate path between the DataStore and the database is generated.
354 // Therefore we just do a simple recursive search until we find a matching name.
355 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.
356 base::FilePath data_path =
357 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
358 if (data_path.empty())
359 return base::FilePath();
360
361 base::FileEnumerator enumerator(data_path.Append(L"DataStore\\Data"), true,
362 base::FileEnumerator::FILES);
363 base::FilePath path = enumerator.Next();
364 while (!path.empty()) {
365 if (base::EqualsCaseInsensitiveASCII(path.BaseName().value(),
366 kSpartanDatabaseFile))
367 return path;
368 path = enumerator.Next();
369 }
370 return base::FilePath();
371 }
372
373 struct GuidComparator {
374 bool operator()(const GUID& a, const GUID& b) const {
375 return memcmp(&a, &b, sizeof(a)) < 0;
376 }
377 };
378
379 bool ReadFaviconData(const base::FilePath& file,
380 std::vector<unsigned char>* data) {
381 std::string image_data;
382 if (!base::ReadFileToString(file, &image_data))
383 return false;
384
385 const unsigned char* ptr =
386 reinterpret_cast<const unsigned char*>(image_data.c_str());
387 return importer::ReencodeFavicon(ptr, image_data.size(), data);
388 }
389
390 void BuildBookmarkEntries(EdgeFavoriteEntry* current_entry,
391 std::vector<ImportedBookmarkEntry>* bookmarks,
392 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.
393 bool is_toolbar,
394 std::vector<base::string16>& path) {
395 // Sort the children first by order number and then by title.
396 std::sort(current_entry->children.begin(), current_entry->children.end(),
397 EdgeFavoriteEntryComparator());
398 for (EdgeFavoriteEntry* entry : current_entry->children) {
399 if (entry->is_folder) {
400 // If the favorites bar then load all children as toolbar items.
401 if (base::EqualsCaseInsensitiveASCII(entry->title, kFavoritesBarTitle)) {
402 // Replace name with Links similar to IE.
403 path.push_back(L"Links");
404 BuildBookmarkEntries(entry, bookmarks, favicons, true, path);
405 path.pop_back();
406 } else {
407 path.push_back(entry->title);
408 BuildBookmarkEntries(entry, bookmarks, favicons, is_toolbar, path);
409 path.pop_back();
410 }
411 } else {
412 bookmarks->push_back(entry->ToBookmarkEntry(is_toolbar, path));
413 favicon_base::FaviconUsageData favicon;
414 if (entry->url.is_valid() && !entry->favicon_file.empty() &&
415 ReadFaviconData(entry->favicon_file, &favicon.png_data)) {
416 // As the database doesn't provide us a favicon URL we'll fake one.
417 GURL::Replacements path_replace;
418 path_replace.SetPathStr("/favicon.ico");
419 favicon.favicon_url =
420 entry->url.GetWithEmptyPath().ReplaceComponents(path_replace);
421 favicon.urls.insert(entry->url);
422 favicons->push_back(favicon);
423 }
424 }
425 }
426 }
427
428 } // namespace
429
430 EdgeImporter::EdgeImporter() {}
431
432 void EdgeImporter::StartImport(const importer::SourceProfile& source_profile,
433 uint16 items,
434 ImporterBridge* bridge) {
435 bridge_ = bridge;
436 bridge_->NotifyStarted();
437 source_path_ = source_profile.source_path;
438
439 if ((items & importer::FAVORITES) && !cancelled()) {
440 bridge_->NotifyItemStarted(importer::FAVORITES);
441 ImportFavorites();
442 bridge_->NotifyItemEnded(importer::FAVORITES);
443 }
444 bridge_->NotifyEnded();
445 }
446
447 EdgeImporter::~EdgeImporter() {}
448
449 void EdgeImporter::ImportFavorites() {
450 BookmarkVector bookmarks;
451 favicon_base::FaviconUsageDataList favicons;
452 ParseFavoritesDatabase(&bookmarks, &favicons);
453
454 if (!bookmarks.empty() && !cancelled()) {
455 const base::string16& first_folder_name =
456 l10n_util::GetStringUTF16(IDS_BOOKMARK_GROUP_FROM_EDGE);
457 bridge_->AddBookmarks(bookmarks, first_folder_name);
458 }
459 if (!favicons.empty() && !cancelled())
460 bridge_->SetFavicons(favicons);
461 }
462
463 // 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.
464 // database within the Edge local storage. The import uses the ESE library to
465 // open and read the data file. The data is stored in a Favorites table with
466 // the following schema.
467 // Column Name Column Type
468 // ------------------------------------------
469 // DateUpdated LongLong - FILETIME
470 // FaviconFile LongText - Relative path
471 // HashedUrl ULong
472 // IsDeleted Bit
473 // IsFolder Bit
474 // ItemId Guid
475 // OrderNumber LongLong
476 // ParentId Guid
477 // RoamDisabled Bit
478 // RowId Long
479 // Title LongText
480 // URL LongText
481 void EdgeImporter::ParseFavoritesDatabase(
482 BookmarkVector* bookmarks,
483 favicon_base::FaviconUsageDataList* favicons) {
484 base::FilePath database_path = FindSpartanDatabase(source_path_);
485 if (database_path.empty())
486 return;
487
488 EdgeDatabaseSession database;
489 if (!database.OpenDatabase(database_path.value())) {
490 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.
491 return;
492 }
493
494 scoped_ptr<EdgeDatabaseTableEnumerator> enumerator =
495 database.OpenTableEnumerator(L"Favorites");
496 if (!enumerator) {
497 DLOG(ERROR) << "Error opening database table "
498 << database.GetErrorMessage();
499 return;
500 }
501
502 std::map<GUID, EdgeFavoriteEntry, GuidComparator> database_entries;
503 base::FilePath favicon_base =
504 source_path_.empty()
505 ? importer::GetEdgeDataFilePath().Append(L"DataStore")
506 : 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.
507
508 enumerator->Reset();
509 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
510 EdgeFavoriteEntry entry;
511 bool is_deleted = false;
512 if (!enumerator->RetrieveColumn(L"IsDeleted", is_deleted))
513 continue;
514 if (is_deleted)
515 continue;
516 if (!enumerator->RetrieveColumn(L"IsFolder", entry.is_folder))
517 continue;
518 base::string16 url;
519 if (!enumerator->RetrieveColumn(L"URL", url))
520 continue;
521 entry.url = GURL(url);
522 if (!entry.is_folder && !entry.url.is_valid())
523 continue;
524 if (!enumerator->RetrieveColumn(L"Title", entry.title))
525 continue;
526 base::string16 favicon_file;
527 if (!enumerator->RetrieveColumn(L"FaviconFile", favicon_file))
528 continue;
529 if (!favicon_file.empty())
530 entry.favicon_file = favicon_base.Append(favicon_file);
531 if (!enumerator->RetrieveColumn(L"ParentId", entry.parent_id))
532 continue;
533 if (!enumerator->RetrieveColumn(L"ItemId", entry.item_id))
534 continue;
535 if (!enumerator->RetrieveColumn(L"OrderNumber", entry.order_number))
536 continue;
537 FILETIME ft;
538 if (!enumerator->RetrieveColumn(L"DateUpdated", ft))
539 continue;
540 entry.date_updated = base::Time::FromFileTime(ft);
541 database_entries[entry.item_id] = entry;
542 } while (enumerator->Next() && !cancelled());
543
544 // Build simple tree.
545 EdgeFavoriteEntry root_entry;
546 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.
547 auto found_parent = database_entries.find(e.second.parent_id);
548 if (found_parent == database_entries.end() ||
549 !found_parent->second.is_folder) {
550 root_entry.children.push_back(&e.second);
551 } else {
552 found_parent->second.children.push_back(&e.second);
553 }
554 }
555 std::vector<base::string16> path;
556 BuildBookmarkEntries(&root_entry, bookmarks, favicons, false, path);
557 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698