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