Chromium Code Reviews| 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..c4ffdffe3b8ee462714f76f5ff7b7404a57e05da |
| --- /dev/null |
| +++ b/chrome/utility/importer/edge_importer_win.cc |
| @@ -0,0 +1,555 @@ |
| +// 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 <tuple> |
| +#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" |
| +#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[kErrorMessagesize] = {}; |
| + 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_; } |
| + |
| + // This function returns true if the passed error parameter is equal |
| + // to JET_errSuccess |
| + bool SetLastError(JET_ERR error) { |
| + last_error_ = error; |
| + return error == JET_errSuccess; |
| + } |
| + |
| + private: |
| + 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.
|
| + |
| + 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_); |
| + } |
|
Ilya Sherman
2015/12/01 07:31:18
nit: You can now drop the curly braces.
forshaw
2015/12/01 11:12:13
Acknowledged.
|
| + } |
| + |
| + const base::string16& table_name() { return table_name_; } |
| + |
| + bool Reset() { |
| + return SetLastError(JetMove(session_id_, table_id_, JET_MoveFirst, 0)); |
| + } |
| + |
| + bool Next() { |
| + return SetLastError(JetMove(session_id_, table_id_, JET_MoveNext, 0)); |
| + } |
| + |
| + template <typename T> |
| + bool RetrieveColumn(const base::string16& column_name, T* value) { |
| + 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); |
| + SetLastError(err); |
| + if (err != JET_errSuccess && err != JET_wrnColumnNull) { |
| + return false; |
| + } |
| + |
| + if (err == JET_errSuccess) { |
| + column_data.resize(actual_size); |
| + if (!ValidateAndConvertValue(column_base.coltyp, column_data, value)) |
| + return false; |
| + } else { |
| + *value = T(); |
| + } |
| + |
| + return true; |
| + } |
| + |
| + 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(T))) { |
| + memcpy(value, &column_data[0], sizeof(T)); |
| + return true; |
| + } |
| + SetLastError(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; |
| + } |
| + SetLastError(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)) { |
| + base::string16& value_ref = *value; |
| + size_t char_length = column_data.size() / sizeof(base::char16); |
| + 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.
|
| + memcpy(&value_ref[0], &column_data[0], column_data.size()); |
| + // Remove any trailing NUL characters. |
| + while (char_length > 0) { |
| + if (value_ref[char_length - 1]) |
| + break; |
| + char_length--; |
| + } |
| + value_ref.resize(char_length); |
| + return true; |
| + } |
| + SetLastError(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 (!SetLastError(JetGetTableColumnInfo( |
| + session_id_, table_id_, column_name.c_str(), &column_base, |
| + sizeof(column_base), JET_ColInfoBase))) { |
| + // 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.
|
| + 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_); |
| + } |
|
Ilya Sherman
2015/12/01 07:31:18
nit: You can remove the curly braces here.
forshaw
2015/12/01 11:12:13
Acknowledged.
|
| + } |
| + |
| + bool OpenDatabase(const base::string16& database_file) { |
| + if (IsOpen()) { |
| + SetLastError(JET_errOneDatabasePerSession); |
| + return false; |
| + } |
| + if (!SetLastError(JetSetSystemParameter(nullptr, JET_sesidNil, |
| + JET_paramDatabasePageSize, |
| + kEdgeDatabasePageSize, nullptr))) |
| + return false; |
| + if (!SetLastError(JetCreateInstance(&instance_id_, L"EdgeDataImporter"))) |
| + return false; |
| + if (!SetLastError(JetSetSystemParameter(&instance_id_, JET_sesidNil, |
| + JET_paramRecovery, 0, L"Off"))) |
| + return false; |
| + if (!SetLastError(JetInit(&instance_id_))) |
| + return false; |
| + if (!SetLastError( |
| + JetBeginSession(instance_id_, &session_id_, nullptr, nullptr))) |
| + return false; |
| + if (!SetLastError(JetAttachDatabase2(session_id_, database_file.c_str(), 0, |
| + JET_bitDbReadOnly))) |
| + return false; |
| + if (!SetLastError(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()) { |
| + SetLastError(JET_errDatabaseNotFound); |
| + return nullptr; |
| + } |
| + |
| + if (!SetLastError(JetOpenTable(session_id_, db_id_, table_name.c_str(), |
| + nullptr, 0, JET_bitTableReadOnly, |
| + &table_id))) |
| + return nullptr; |
| + |
| + return make_scoped_ptr( |
| + new EdgeDatabaseTableEnumerator(table_name, session_id_, table_id)); |
| + } |
| + |
| + private: |
| + // This is the page size of the Edge data. It's unlikely to change. |
| + static const JET_API_PTR kEdgeDatabasePageSize = 8192; |
| + |
| + 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<const EdgeFavoriteEntry*> children; |
| + |
| + ImportedBookmarkEntry ToBookmarkEntry( |
| + bool in_toolbar, |
| + const std::vector<base::string16>& path) const { |
| + 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 { |
| + return std::tie(lhs->order_number, lhs->title) < |
| + std::tie(rhs->order_number, rhs->title); |
| + } |
| +}; |
| + |
| +// 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(const base::FilePath& profile_path) { |
| + base::FilePath data_path = |
| + profile_path.empty() ? importer::GetEdgeDataFilePath() : profile_path; |
| + 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(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.
|
| + bool is_toolbar, |
| + std::vector<ImportedBookmarkEntry>* bookmarks, |
| + favicon_base::FaviconUsageDataList* favicons, |
| + std::vector<base::string16>* path) { |
| + for (const 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, true, bookmarks, favicons, path); |
| + path->pop_back(); |
| + } else { |
| + path->push_back(entry->title); |
| + BuildBookmarkEntries(entry, is_toolbar, bookmarks, favicons, 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() { |
| + std::vector<ImportedBookmarkEntry> 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 |
| +// 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( |
| + std::vector<ImportedBookmarkEntry>* 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())) { |
| + 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.
|
| + return; |
| + } |
| + |
| + scoped_ptr<EdgeDatabaseTableEnumerator> enumerator = |
| + database.OpenTableEnumerator(L"Favorites"); |
| + if (!enumerator) { |
| + DVLOG(ERROR) << "Error opening database table " |
| + << database.GetErrorMessage(); |
| + return; |
| + } |
| + |
| + std::map<GUID, EdgeFavoriteEntry, GuidComparator> database_entries; |
| + base::FilePath favicon_base = |
| + source_path_.empty() ? importer::GetEdgeDataFilePath() : source_path_; |
| + favicon_base = favicon_base.Append(L"DataStore"); |
| + |
| + if (!enumerator->Reset()) |
| + 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.
|
| + do { |
| + 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 data_updated; |
| + if (!enumerator->RetrieveColumn(L"DateUpdated", &data_updated)) |
| + continue; |
| + entry.date_updated = base::Time::FromFileTime(data_updated); |
| + database_entries[entry.item_id] = entry; |
| + } while (enumerator->Next() && !cancelled()); |
| + |
| + // Build simple tree. |
| + EdgeFavoriteEntry root_entry; |
| + for (auto& entry : database_entries) { |
| + auto found_parent = database_entries.find(entry.second.parent_id); |
| + if (found_parent == database_entries.end() || |
| + !found_parent->second.is_folder) { |
| + root_entry.children.push_back(&entry.second); |
| + } else { |
| + found_parent->second.children.push_back(&entry.second); |
| + } |
| + } |
| + // With tree built sort the children of each node including the root. |
| + std::sort(root_entry.children.begin(), root_entry.children.end(), |
| + EdgeFavoriteEntryComparator()); |
| + for (auto& entry : database_entries) { |
| + std::sort(entry.second.children.begin(), entry.second.children.end(), |
| + EdgeFavoriteEntryComparator()); |
| + } |
| + std::vector<base::string16> path; |
| + BuildBookmarkEntries(&root_entry, false, bookmarks, favicons, &path); |
| +} |