chromium/chrome/browser/ash/crosapi/browser_data_migrator_util.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/crosapi/browser_data_migrator_util.h"

#include <unistd.h>

#include <algorithm>
#include <string_view>

#include "base/containers/contains.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util.h"
#include "base/system/sys_info.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_keeplist_chromeos.h"
#include "chrome/common/chrome_constants.h"
#include "chromeos/ash/components/standalone_browser/migration_progress_tracker.h"
#include "components/sync/base/data_type.h"
#include "components/sync/base/storage_type.h"
#include "components/sync/model/blocking_data_type_store_impl.h"
#include "third_party/leveldatabase/src/include/leveldb/write_batch.h"

namespace ash::browser_data_migrator_util {
namespace {

struct PathNamePair {
  const char* key;
  const char* value;
};

struct PathNameComparator {
  constexpr bool operator()(const PathNamePair& p1,
                            const PathNamePair& p2) const {
    return std::string_view(p1.key) < std::string_view(p2.key);
  }
};

// Key value pairs of path names in profile data directory and their
// corresponding UMA item names.
constexpr PathNamePair kPathNamePairs[] = {
    {"AccountManagerTokens.bin", "AccountManagerTokensBin"},
    {"Accounts", "Accounts"},
    {"Affiliation Database", "AffiliationDatabase"},
    {"AutofillStrikeDatabase", "AutofillStrikeDatabase"},
    {"Bookmarks", "Bookmarks"},
    {"BudgetDatabase", "BudgetDatabase"},
    {"Cache", "Cache"},
    {"Code Cache", "CodeCache"},
    {"Cookies", "Cookies"},
    {"DNR Extension Rules", "DNRExtensionRules"},
    {"Download Service", "DownloadService"},
    {"Downloads", "Downloads"},
    {"Extension Cookies", "ExtensionCookies"},
    {"Extension Rules", "ExtensionRules"},
    {"Extension State", "ExtensionState"},
    {"Extensions", "Extensions"},
    {"Favicons", "Favicons"},
    {"Feature Engagement Tracker", "FeatureEngagementTracker"},
    {"File System", "FileSystem"},
    {"FullRestoreData", "FullRestoreData"},
    {"GCM Store", "GCMStore"},
    {"GCache", "GCache"},
    {"GPUCache", "GPUCache"},
    {"History", "History"},
    {"IndexedDB", "IndexedDB"},
    {"LOCK", "LOCK"},
    {"LOG", "LOG"},
    {"LOG.old", "LOGOld"},
    {"Local App Settings", "LocalAppSettings"},
    {"Local Extension Settings", "LocalExtensionSettings"},
    {"Local Storage", "LocalStorage"},
    {"Login Data", "LoginData"},
    {"Login Data For Account", "LoginDataForAccount"},
    {"Managed Extension Settings", "ManagedExtensionSettings"},
    {"MyFiles", "MyFiles"},
    {"NearbySharePublicCertificateDatabase",
     "NearbySharePublicCertificateDatabase"},
    {"Network Action Predictor", "NetworkActionPredictor"},
    {"Network Persistent State", "NetworkPersistentState"},
    {"PPDCache", "PPDCache"},
    {"Platform Notifications", "PlatformNotifications"},
    {"Policy", "Policy"},
    {"Preferences", "Preferences"},
    {"PreferredApps", "PreferredApps"},
    {"PrintJobDatabase", "PrintJobDatabase"},
    {"QuotaManager", "QuotaManager"},
    {"README", "README"},
    {"RLZ Data", "RLZData"},
    {"Reporting and NEL", "ReportingAndNEL"},
    {"Service Worker", "ServiceWorker"},
    {"Session Storage", "SessionStorage"},
    {"Sessions", "Sessions"},
    {"Shortcuts", "Shortcuts"},
    {"Site Characteristics Database", "SiteCharacteristicsDatabase"},
    {"Storage", "Storage"},
    {"Sync App Settings", "SyncAppSettings"},
    {"Sync Data", "SyncData"},
    {"Sync Extension Settings", "SyncExtensionSettings"},
    {"Top Sites", "TopSites"},
    {"Translate Ranker Model", "TranslateRankerModel"},
    {"TransportSecurity", "TransportSecurity"},
    {"Trusted Vault", "TrustedVault"},
    {"Visited Links", "VisitedLinks"},
    {"Web Applications", "WebApplications"},
    {"Web Data", "WebData"},
    {"WebRTC Logs", "WebRTCLogs"},
    {"app_ranker.pb", "AppRankerPb"},
    {"arc.apps", "ArcApps"},
    {"autobrightness", "Autobrightness"},
    {"blob_storage", "BlobStorage"},
    {"browser_data_migrator", "BrowserDataMigrator"},
    {"crostini.icons", "CrostiniIcons"},
    {"data_reduction_proxy_leveldb", "DataReductionProxyLeveldb"},
    {"databases", "Databases"},
    {"extension_install_log", "ExtensionInstallLog"},
    {"google-assistant-library", "GoogleAssistantLibrary"},
    {"heavy_ad_intervention_opt_out.db", "HeavyAdInterventionOptOutDb"},
    {"lacros", "Lacros"},
    {"login-times", "LoginTimes"},
    {"logout-times", "LogoutTimes"},
    {"optimization_guide_hint_cache_store", "OptimizationGuideHintCacheStore"},
    {"optimization_guide_model_and_features_store",
     "OptimizationGuideModelAndFeaturesStore"},
    {"previews_opt_out.db", "PreviewsOptOutDb"},
    {"shared_proto_db", "SharedProtoDb"},
    {"smartcharging", "Smartcharging"},
    {"structured_metrics", "StructuredMetrics"},
    {"webrtc_event_logs", "WebrtcEventLogs"},
    {"zero_state_group_ranker.pb", "ZeroStateGroupRankerPb"},
    {"zero_state_local_files.pb", "ZeroStateLocalFilesPb"}};

static_assert(base::ranges::is_sorted(kPathNamePairs, PathNameComparator()),
              "kPathNamePairs needs to be sorted by the keys of its elements "
              "so that binary_search can be used on it.");

std::optional<uint64_t> g_extra_bytes_required_to_be_freed_for_testing;

// Key prefixes in LocalStorage's LevelDB.
constexpr char kMetaPrefix[] = "META:chrome-extension://";
constexpr char kKeyPrefix[] = "_chrome-extension://";

// IndexedDB extension suffixes.
constexpr char kIndexedDBBlobExtension[] = ".indexeddb.blob";
constexpr char kIndexedDBLevelDBExtension[] = ".indexeddb.leveldb";

bool ShouldRemoveExtensionByType(const std::string_view extension_id,
                                 ChromeType chrome_type) {
  switch (chrome_type) {
    case ChromeType::kAsh:
      return !base::Contains(kExtensionsAshOnly, extension_id) &&
             !base::Contains(
                 extensions::GetExtensionsAndAppsRunInOSAndStandaloneBrowser(),
                 extension_id);

    case ChromeType::kLacros:
      return base::Contains(kExtensionsAshOnly, extension_id);
  }
}

void UpdatePreferencesDictByType(base::Value::Dict& dict,
                                 ChromeType chrome_type) {
  std::vector<std::string> keys_to_remove;

  // Collect keys that don't belong in `chrome_type`.
  for (const auto entry : dict) {
    const std::string_view extension_id = entry.first;
    if (ShouldRemoveExtensionByType(extension_id, chrome_type))
      keys_to_remove.emplace_back(extension_id);
  }

  // Delete those keys.
  for (const std::string& k : keys_to_remove) {
    dict.Remove(k);
  }
}

void UpdatePreferencesListByType(base::Value::List& list,
                                 ChromeType chrome_type) {
  // Erase all elements in the list that don't belong in `chrome_type`.
  list.EraseIf([&](const base::Value& item) {
    if (!item.is_string())
      return false;

    const std::string_view extension_id = item.GetString();
    return ShouldRemoveExtensionByType(extension_id, chrome_type);
  });
}

}  // namespace

CancelFlag::CancelFlag() : cancelled_(false) {}
CancelFlag::~CancelFlag() = default;

TargetItem::TargetItem(base::FilePath path, int64_t size, ItemType item_type)
    : path(std::move(path)),
      size(size),
      is_directory(item_type == ItemType::kDirectory) {}

bool TargetItem::operator==(const TargetItem& rhs) const {
  return this->path == rhs.path && this->size == rhs.size &&
         this->is_directory == rhs.is_directory;
}

TargetItems::TargetItems() = default;
TargetItems::TargetItems(TargetItems&&) = default;
TargetItems::~TargetItems() = default;

// Copies `item` to location pointed by `dest`. Returns true on success and
// false on failure.
bool CopyTargetItem(
    const TargetItem& item,
    const base::FilePath& dest,
    CancelFlag* cancel_flag,
    standalone_browser::MigrationProgressTracker* progress_tracker) {
  if (cancel_flag->IsSet())
    return false;

  if (item.is_directory) {
    if (CopyDirectory(item.path, dest, cancel_flag, progress_tracker))
      return true;
  } else {
    if (base::CopyFile(item.path, dest)) {
      progress_tracker->UpdateProgress(item.size);
      return true;
    }
  }

  PLOG(ERROR) << "Copy failed for " << item.path;
  return false;
}

TargetItems GetTargetItems(const base::FilePath& original_profile_dir,
                           const ItemType type) {
  base::span<const char* const> target_paths;
  switch (type) {
    case ItemType::kLacros:
      target_paths = base::span<const char* const>(kLacrosDataPaths);
      break;
    case ItemType::kRemainInAsh:
      target_paths = base::span<const char* const>(kRemainInAshDataPaths);
      break;
    case ItemType::kDeletable:
      target_paths = base::span<const char* const>(kDeletablePaths);
      break;
    case ItemType::kNeedCopyForMove:
      target_paths = base::span<const char* const>(kNeedCopyForMoveDataPaths);
      break;
    case ItemType::kNeedCopyForCopy:
      target_paths = base::span<const char* const>(kNeedCopyForCopyDataPaths);
      break;
    default:
      NOTREACHED_IN_MIGRATION();
  }

  TargetItems target_items;
  base::FileEnumerator enumerator(original_profile_dir, false /* recursive */,
                                  base::FileEnumerator::FILES |
                                      base::FileEnumerator::DIRECTORIES |
                                      base::FileEnumerator::SHOW_SYM_LINKS);
  for (base::FilePath entry = enumerator.Next(); !entry.empty();
       entry = enumerator.Next()) {
    const base::FileEnumerator::FileInfo& info = enumerator.GetInfo();
    int64_t size;
    TargetItem::ItemType item_type;
    if (S_ISREG(info.stat().st_mode)) {
      size = info.GetSize();
      item_type = TargetItem::ItemType::kFile;
    } else if (S_ISDIR(info.stat().st_mode)) {
      size =
          browser_data_migrator_util::ComputeDirectorySizeWithoutLinks(entry);
      item_type = TargetItem::ItemType::kDirectory;
    } else {
      // Skip if `entry` is not a file or directory such as a symlink.
      continue;
    }

    if (base::Contains(target_paths, entry.BaseName().value())) {
      target_items.total_size += size;
      target_items.items.emplace_back(TargetItem{entry, size, item_type});
    }
  }

  return target_items;
}

uint64_t ExtraBytesRequiredToBeFreed(
    const int64_t total_copy_size,
    const base::FilePath& original_profile_dir) {
  if (g_extra_bytes_required_to_be_freed_for_testing)
    return *g_extra_bytes_required_to_be_freed_for_testing;

  const int64_t free_disk_space =
      base::SysInfo::AmountOfFreeDiskSpace(original_profile_dir);
  const int64_t required_disk_space = total_copy_size + kBuffer;

  if (required_disk_space > free_disk_space) {
    LOG(WARNING) << required_disk_space
                 << " bytes of disk space is required but only "
                 << free_disk_space << " bytes are available.";
    return required_disk_space - free_disk_space;
  }

  return 0;
}

int64_t EstimatedExtraBytesCreated(const base::FilePath& original_profile_dir) {
  const TargetItems need_to_copy_items =
      GetTargetItems(original_profile_dir, ItemType::kNeedCopyForMove);

  int64_t total_size = need_to_copy_items.total_size;

  // Get file size of 'Preferences' and 'Sync Data'.
  const base::FilePath preferences_path =
      original_profile_dir.Append(chrome::kPreferencesFilename);
  if (base::PathExists(preferences_path)) {
    int64_t size;
    if (base::GetFileSize(preferences_path, &size)) {
      // For 'Preferences', add size * 2 because we create copies for Lacros and
      // Ash of the same size.
      total_size += size * 2;
    } else {
      PLOG(ERROR) << "Failed to get file size for " << preferences_path.value();
    }
  }

  // For 'Sync Data', the "copies" we create for Ash and Lacros have no
  // overlap thus the total size of the newly created copies is approximately
  // equal to the size of the original database.
  const base::FilePath sync_data_path =
      original_profile_dir.Append(kSyncDataFilePath);
  if (base::PathExists(sync_data_path)) {
    total_size += base::ComputeDirectorySize(sync_data_path);
  }

  return total_size;
}

ScopedExtraBytesRequiredToBeFreedForTesting::
    ScopedExtraBytesRequiredToBeFreedForTesting(uint64_t required_size) {
  DCHECK(!g_extra_bytes_required_to_be_freed_for_testing.has_value());
  g_extra_bytes_required_to_be_freed_for_testing = required_size;
}

ScopedExtraBytesRequiredToBeFreedForTesting::
    ~ScopedExtraBytesRequiredToBeFreedForTesting() {
  g_extra_bytes_required_to_be_freed_for_testing.reset();
}

bool CopyDirectory(
    const base::FilePath& from_path,
    const base::FilePath& to_path,
    CancelFlag* cancel_flag,
    standalone_browser::MigrationProgressTracker* progress_tracker) {
  if (cancel_flag->IsSet())
    return false;

  if (!base::PathExists(to_path) && !base::CreateDirectory(to_path)) {
    PLOG(ERROR) << "CreateDirectory() failed for " << to_path.value();
    return false;
  }

  base::FileEnumerator enumerator(from_path, false /* recursive */,
                                  base::FileEnumerator::FILES |
                                      base::FileEnumerator::DIRECTORIES |
                                      base::FileEnumerator::SHOW_SYM_LINKS);
  for (base::FilePath entry = enumerator.Next(); !entry.empty();
       entry = enumerator.Next()) {
    if (cancel_flag->IsSet())
      return false;

    const base::FileEnumerator::FileInfo& info = enumerator.GetInfo();

    // Only copy a file or a dir i.e. skip other types like symlink since
    // copying those might introdue a security risk.
    if (S_ISREG(info.stat().st_mode)) {
      if (!base::CopyFile(entry, to_path.Append(entry.BaseName())))
        return false;

      progress_tracker->UpdateProgress(info.GetSize());
    } else if (S_ISDIR(info.stat().st_mode)) {
      if (!CopyDirectory(entry, to_path.Append(entry.BaseName()), cancel_flag,
                         progress_tracker)) {
        return false;
      }
    }
  }

  return true;
}

bool CopyTargetItems(
    const base::FilePath& to_dir,
    const TargetItems& target_items,
    CancelFlag* cancel_flag,
    standalone_browser::MigrationProgressTracker* progress_tracker) {
  for (const auto& item : target_items.items) {
    if (cancel_flag->IsSet())
      return false;

    if (!CopyTargetItem(item, to_dir.Append(item.path.BaseName()), cancel_flag,
                        progress_tracker)) {
      return false;
    }
  }

  return true;
}

int64_t ComputeDirectorySizeWithoutLinks(const base::FilePath& dir_path) {
  base::FileEnumerator enumerator(dir_path, false /* recursive */,
                                  base::FileEnumerator::FILES |
                                      base::FileEnumerator::DIRECTORIES |
                                      base::FileEnumerator::SHOW_SYM_LINKS);
  int64_t size = 0;
  for (base::FilePath entry = enumerator.Next(); !entry.empty();
       entry = enumerator.Next()) {
    const base::FileEnumerator::FileInfo& info = enumerator.GetInfo();

    if (S_ISREG(info.stat().st_mode)) {
      size += info.GetSize();
    } else if (S_ISDIR(info.stat().st_mode)) {
      size += ComputeDirectorySizeWithoutLinks(entry);
    } else {
      // Skip links.
      continue;
    }
  }

  return size;
}

void RecordTotalSize(int64_t size) {
  base::UmaHistogramCustomCounts(kTotalSize, size / 1024 / 1024, 1, 10000, 100);
}

void RecordTargetItemSizes(const std::vector<TargetItem>& items) {
  for (auto& item : items)
    browser_data_migrator_util::RecordUserDataSize(item.path, item.size);
}

void RecordUserDataSize(const base::FilePath& path, int64_t size) {
  std::string uma_name = kUserDataStatsRecorderDataSize;
  uma_name += GetUMAItemName(path);

  // Divide 10GB into 100 buckets. Unit in MB.
  base::UmaHistogramCustomCounts(uma_name, size / 1024 / 1024, 1, 10000, 100);
}

std::string GetUMAItemName(const base::FilePath& path) {
  std::string path_name = path.BaseName().value();

  auto* it = std::lower_bound(
      std::begin(kPathNamePairs), std::end(kPathNamePairs),
      PathNamePair{path_name.c_str(), nullptr}, PathNameComparator());

  if (it != std::end(kPathNamePairs) &&
      std::string_view(it->key) == path_name) {
    return it->value;
  }

  // If `path_name` was not found in kPathNamePairs, return "Unknown" as name.
  return kUnknownUMAName;
}

void DryRunToCollectUMA(const base::FilePath& profile_data_dir) {
  TargetItems lacros_items =
      GetTargetItems(profile_data_dir, ItemType::kLacros);
  TargetItems need_copy_items =
      GetTargetItems(profile_data_dir, ItemType::kNeedCopyForMove);
  TargetItems remain_in_ash_items =
      GetTargetItems(profile_data_dir, ItemType::kRemainInAsh);
  TargetItems deletable_items =
      GetTargetItems(profile_data_dir, ItemType::kDeletable);

  base::UmaHistogramCustomCounts(kDryRunNoCopyDataSize,
                                 deletable_items.total_size / 1024 / 1024, 1,
                                 10000, 100);
  base::UmaHistogramCustomCounts(kDryRunAshDataSize,
                                 remain_in_ash_items.total_size / 1024 / 1024,
                                 1, 10000, 100);
  base::UmaHistogramCustomCounts(kDryRunLacrosDataSize,
                                 lacros_items.total_size / 1024 / 1024, 1,
                                 10000, 100);
  base::UmaHistogramCustomCounts(kDryRunCommonDataSize,
                                 need_copy_items.total_size / 1024 / 1024, 1,
                                 10000, 100);

  const int64_t total_items_size =
      need_copy_items.total_size + lacros_items.total_size +
      remain_in_ash_items.total_size + deletable_items.total_size;
  browser_data_migrator_util::RecordTotalSize(total_items_size);

  RecordTargetItemSizes(deletable_items.items);
  RecordTargetItemSizes(remain_in_ash_items.items);
  RecordTargetItemSizes(lacros_items.items);
  RecordTargetItemSizes(need_copy_items.items);

  const int64_t extra_bytes_created_by_move =
      EstimatedExtraBytesCreated(profile_data_dir);
  const int64_t free_disk_space =
      base::SysInfo::AmountOfFreeDiskSpace(profile_data_dir);
  const int64_t free_disk_space_after_delete =
      free_disk_space + deletable_items.total_size;
  const int64_t free_disk_space_after_migration =
      free_disk_space_after_delete - extra_bytes_created_by_move;

  base::UmaHistogramCustomCounts(kDryRunExtraDiskSpaceOccupiedByMove,
                                 extra_bytes_created_by_move / 1024 / 1024, 1,
                                 10000, 100);
  base::UmaHistogramCustomCounts(kDryRunFreeDiskSpaceAfterDelete,
                                 free_disk_space_after_delete / 1024 / 1024, 1,
                                 10000, 100);
  base::UmaHistogramCustomCounts(kDryRunFreeDiskSpaceAfterMigration,
                                 free_disk_space_after_migration / 1024 / 1024,
                                 -10000, 10000, 200);

  if (free_disk_space_after_migration < (int64_t)kBuffer) {
    base::UmaHistogramCustomCounts(
        kDryRunExtraDiskSpaceOccupiedByMoveLowDiskUser2,
        extra_bytes_created_by_move / 1024 / 1024, 1, 10000, 100);
    base::UmaHistogramCustomCounts(kDryRunFreeDiskSpaceLowDiskUser2,
                                   free_disk_space / 1024 / 1024, 1, 10000,
                                   100);
    base::UmaHistogramCustomCounts(kDryRunFreeDiskSpaceAfterDeleteLowDiskUser2,
                                   free_disk_space_after_delete / 1024 / 1024,
                                   1, 10000, 100);
    base::UmaHistogramCustomCounts(
        kDryRunProfileDirSizeLowDiskUser2,
        ComputeDirectorySizeWithoutLinks(profile_data_dir) / 1024 / 1024, 1,
        10000, 100);
    base::UmaHistogramCustomCounts(
        kDryRunMyFilesDirSizeLowDiskUser2,
        ComputeDirectorySizeWithoutLinks(profile_data_dir.Append("MyFiles")) /
            1024 / 1024,
        1, 10000, 100);
  }
}

leveldb::Status GetExtensionKeys(leveldb::DB* db,
                                 LevelDBType leveldb_type,
                                 ExtensionKeys* result) {
  std::unique_ptr<leveldb::Iterator> it(
      db->NewIterator(leveldb::ReadOptions()));

  // Iterate through all the elements of the leveldb database.
  for (it->SeekToFirst(); it->Valid(); it->Next()) {
    std::string extension_id;
    const std::string key = it->key().ToString();

    switch (leveldb_type) {
      // LocalStorage format.
      // Refer to: components/services/storage/dom_storage/local_storage_impl.cc
      case LevelDBType::kLocalStorage:
        if (base::StartsWith(key, kMetaPrefix)) {
          extension_id = key.substr(std::size(kMetaPrefix) - 1);
        } else if (base::StartsWith(key, kKeyPrefix)) {
          size_t pos = std::size(kKeyPrefix) - 1;
          size_t end = key.find('\x00', pos);
          if (end != std::string::npos)
            extension_id = key.substr(pos, end - pos);
        }
        break;

      // StateStore format (e.g. `Extension State/Rules`).
      // Refer to: extensions/browser/state_store.cc
      case LevelDBType::kStateStore:
        size_t separator = key.find('.');
        if (separator != std::string::npos)
          extension_id = key.substr(0, separator);
        break;
    }

    // Collect keys associated with each extension id.
    if (!extension_id.empty())
      (*result)[extension_id].push_back(key);
  }

  PLOG_IF(ERROR, !it->status().ok())
      << "GetExtensionKeys() failed with status: " << it->status().ToString();

  return it->status();
}

bool IsAshOnlySyncDataType(std::string_view key) {
  for (auto type : kAshOnlySyncDataTypesForLacrosMigration) {
    if ((base::StartsWith(
             key, FormatDataPrefix(type, syncer::StorageType::kUnspecified)) ||
         base::StartsWith(
             key, FormatMetaPrefix(type, syncer::StorageType::kUnspecified)) ||
         key == FormatGlobalMetadataKey(type,
                                        syncer::StorageType::kUnspecified))) {
      return true;
    }
  }
  return false;
}

IndexedDBPaths GetIndexedDBPaths(const base::FilePath& profile_path,
                                 const char* extension_id) {
  const base::FilePath indexed_db_dir = profile_path.Append(kIndexedDBFilePath);
  const base::FilePath base_path = indexed_db_dir.Append(
      "chrome_extension_" + std::string(extension_id) + "_0");

  return {
      base_path.AddExtension(kIndexedDBBlobExtension),
      base_path.AddExtension(kIndexedDBLevelDBExtension),
  };
}

bool MigrateLevelDB(const base::FilePath& original_path,
                    const base::FilePath& target_path,
                    const LevelDBType leveldb_type) {
  // LevelDB options.
  leveldb_env::Options options;
  options.create_if_missing = false;

  // Open the original LevelDB database.
  std::unique_ptr<leveldb::DB> original_db;
  leveldb::Status status =
      leveldb_env::OpenDB(options, original_path.value(), &original_db);
  if (!status.ok()) {
    PLOG(ERROR) << "Failure while opening original leveldb: " << original_path
                << ": " << status.ToString();
    return false;
  }

  // Retrieve all extensions' keys, indexed by extension id.
  ExtensionKeys original_keys;
  status = GetExtensionKeys(original_db.get(), leveldb_type, &original_keys);
  if (!status.ok()) {
    PLOG(ERROR) << "Failure while reading keys from original leveldb: "
                << original_path << ": " << status.ToString();
    return false;
  }

  // Create a new LevelDB database to store entries that will stay in Ash.
  std::unique_ptr<leveldb::DB> target_db;
  options.create_if_missing = true;
  options.error_if_exists = true;
  status = leveldb_env::OpenDB(options, target_path.value(), &target_db);
  if (!status.ok()) {
    PLOG(ERROR) << "Failure while opening new leveldb: " << target_path << ": "
                << status.ToString();
    return false;
  }

  // Prepare new LevelDB database according to schema.
  // Refer to:
  // - components/services/storage/dom_storage/local_storage_impl.cc
  // - extensions/browser/state_store.cc
  leveldb::WriteBatch write_batch;
  if (leveldb_type == LevelDBType::kLocalStorage) {
    write_batch.Put("VERSION", "1");
  }

  // Copy all the key-value pairs that need to be kept in Ash.
  for (const auto& [extension_id, keys] : original_keys) {
    if (base::Contains(kExtensionsAshOnly, extension_id) ||
        base::Contains(
            extensions::GetExtensionsAndAppsRunInOSAndStandaloneBrowser(),
            extension_id)) {
      for (const std::string& key : keys) {
        std::string value;
        status = original_db->Get(leveldb::ReadOptions(), key, &value);
        if (!status.ok()) {
          PLOG(ERROR) << "Failure while reading from original leveldb: "
                      << original_path << ": " << status.ToString();
          return false;
        }
        write_batch.Put(key, value);
      }
    }
  }

  // Write everything in bulk.
  leveldb::WriteOptions write_options;
  write_options.sync = true;
  status = target_db->Write(write_options, &write_batch);
  if (!status.ok()) {
    PLOG(ERROR) << "Failure while writing into new leveldb: " << target_path
                << ": " << status.ToString();
    return false;
  }

  return true;
}

bool MigrateSyncDataLevelDB(const base::FilePath& original_path,
                            const base::FilePath& ash_target_path,
                            const base::FilePath& lacros_target_path) {
  // Open the original LevelDB database.
  std::unique_ptr<leveldb::DB> original_db;
  leveldb_env::Options options;
  options.create_if_missing = false;
  leveldb::Status status =
      leveldb_env::OpenDB(options, original_path.value(), &original_db);
  if (!status.ok()) {
    PLOG(ERROR) << "Failure while opening original leveldb: " << original_path
                << ": " << status.ToString();
    return false;
  }

  // Create a new LevelDB database to store entries that will stay in Ash.
  std::unique_ptr<leveldb::DB> ash_target_db;
  options.create_if_missing = true;
  options.error_if_exists = true;
  status =
      leveldb_env::OpenDB(options, ash_target_path.value(), &ash_target_db);
  if (!status.ok()) {
    PLOG(ERROR) << "Failure while opening new leveldb: " << ash_target_path
                << ": " << status.ToString();
    return false;
  }

  // Create a new LevelDB database to store entries that will migrate to Lacros.
  std::unique_ptr<leveldb::DB> lacros_target_db;
  status = leveldb_env::OpenDB(options, lacros_target_path.value(),
                               &lacros_target_db);
  if (!status.ok()) {
    PLOG(ERROR) << "Failure while opening new leveldb: " << lacros_target_path
                << ": " << status.ToString();
    return false;
  }

  // Split the key-value pairs between Ash and Lacros.
  leveldb::WriteBatch ash_write_batch;
  leveldb::WriteBatch lacros_write_batch;
  // Iterate through all the elements of the leveldb database.
  std::unique_ptr<leveldb::Iterator> it(
      original_db->NewIterator(leveldb::ReadOptions()));
  for (it->SeekToFirst(); it->Valid(); it->Next()) {
    const std::string key = it->key().ToString();
    const std::string value = it->value().ToString();
    if (IsAshOnlySyncDataType(key))
      ash_write_batch.Put(key, value);
    else
      lacros_write_batch.Put(key, value);
  }
  if (!it->status().ok()) {
    PLOG(ERROR) << "Failure while reading from original leveldb: "
                << original_path << ": " << status.ToString();
    return false;
  }

  // Write everything in bulk.
  leveldb::WriteOptions write_options;
  write_options.sync = true;
  status = ash_target_db->Write(write_options, &ash_write_batch);
  if (!status.ok()) {
    PLOG(ERROR) << "Failure while writing into new leveldb: " << ash_target_path
                << ": " << status.ToString();
    return false;
  }
  status = lacros_target_db->Write(write_options, &lacros_write_batch);
  if (!status.ok()) {
    PLOG(ERROR) << "Failure while writing into new leveldb: "
                << lacros_target_path << ": " << status.ToString();
    return false;
  }

  return true;
}

void UpdatePreferencesKeyByType(base::Value::Dict* root_dict,
                                const std::string_view key,
                                ChromeType chrome_type) {
  base::Value* value = root_dict->FindByDottedPath(key);
  if (!value)
    return;

  if (value->is_dict()) {
    UpdatePreferencesDictByType(value->GetDict(), chrome_type);
  } else if (value->is_list()) {
    UpdatePreferencesListByType(value->GetList(), chrome_type);
  }
}

std::optional<PreferencesContents> MigratePreferencesContents(
    const std::string_view original_contents) {
  // Parse the original JSON file from Ash.
  std::optional<base::Value> ash_root =
      base::JSONReader::Read(original_contents);
  if (!ash_root) {
    PLOG(ERROR) << "Failure while parsing Ash's Preferences";
    return std::nullopt;
  }
  base::Value::Dict* ash_root_dict = ash_root->GetIfDict();
  if (!ash_root_dict) {
    PLOG(ERROR) << "Failure while parsing Ash's Preferences root node";
    return std::nullopt;
  }

  // Create a copy for Lacros migration.
  base::Value lacros_root = ash_root->Clone();
  base::Value::Dict* lacros_root_dict = lacros_root.GetIfDict();
  if (!lacros_root_dict) {
    PLOG(ERROR) << "Failure while parsing Lacros's Preferences root node";
    return std::nullopt;
  }

  // Some preferences are to be moved to Lacros, and deleted in Ash.
  for (const char* key : kLacrosOnlyPreferencesKeys) {
    base::Value* result = ash_root_dict->FindByDottedPath(key);
    if (result)
      ash_root_dict->RemoveByDottedPath(key);
  }

  // Some preferences don't need to be copied to Lacros.
  for (const char* key : kAshOnlyPreferencesKeys) {
    base::Value* result = lacros_root_dict->FindByDottedPath(key);
    if (result)
      lacros_root_dict->RemoveByDottedPath(key);
  }

  // Some preferences need to be split between Ash and Lacros.
  for (const char* key : kSplitPreferencesKeys) {
    UpdatePreferencesKeyByType(ash_root_dict, key, ChromeType::kAsh);
    UpdatePreferencesKeyByType(lacros_root_dict, key, ChromeType::kLacros);
  }

  // Sync feature setup should not be triggered after migration and should be
  // assumed completed. In Lacros it is controlled by the preference below, but
  // this preference doesn't exist in Ash, so need to set it explicitly here.
  lacros_root_dict->SetByDottedPath(
      kSyncInitialSyncFeatureSetupCompletePrefName, base::Value(true));

  // Generate the resulting JSON.
  PreferencesContents contents;
  if (!base::JSONWriter::Write(*ash_root, &contents.ash)) {
    PLOG(ERROR) << "Failure while generating Ash's Preferences";
    return std::nullopt;
  }
  if (!base::JSONWriter::Write(lacros_root, &contents.lacros)) {
    PLOG(ERROR) << "Failure while generating Lacros's Preferences";
    return std::nullopt;
  }

  return contents;
}

bool MigratePreferences(const base::FilePath& original_path,
                        const base::FilePath& ash_target_path,
                        const base::FilePath& lacros_target_path) {
  std::string original_contents;
  if (!base::ReadFileToString(original_path, &original_contents)) {
    PLOG(ERROR) << "Failure while opening original Preferences: "
                << original_path.value();
    return false;
  }

  auto contents = MigratePreferencesContents(original_contents);
  if (!contents)
    return false;

  if (!base::WriteFile(ash_target_path, contents->ash)) {
    PLOG(ERROR) << "Failure while writing Ash's Preferences: "
                << ash_target_path.value();
    return false;
  }
  if (!base::WriteFile(lacros_target_path, contents->lacros)) {
    PLOG(ERROR) << "Failure while writing Lacros's Preferences: "
                << lacros_target_path.value();
    return false;
  }

  return true;
}

bool MigrateAshIndexedDB(const base::FilePath& src_profile_dir,
                         const base::FilePath& target_indexed_db_dir,
                         const char* extension_id,
                         bool copy) {
  auto MigratePath = [&](const base::FilePath& from, const base::FilePath& to) {
    return copy ? base::CopyDirectory(from, to, /*recursive=*/true)
                : base::Move(from, to);
  };

  const auto& [blob_path, leveldb_path] =
      browser_data_migrator_util::GetIndexedDBPaths(src_profile_dir,
                                                    extension_id);
  if (base::PathExists(blob_path)) {
    const base::FilePath ash_blob_path =
        target_indexed_db_dir.Append(blob_path.BaseName());
    if (!MigratePath(blob_path, ash_blob_path)) {
      PLOG(ERROR) << "Failed migrating " << blob_path.value() << " to "
                  << ash_blob_path.value();
      return false;
    }
  }
  if (base::PathExists(leveldb_path)) {
    const base::FilePath ash_leveldb_path =
        target_indexed_db_dir.Append(leveldb_path.BaseName());
    if (!MigratePath(leveldb_path, target_indexed_db_dir)) {
      PLOG(ERROR) << "Failed migrating " << leveldb_path.value() << " to "
                  << ash_leveldb_path.value();
      return false;
    }
  }

  return true;
}

}  // namespace ash::browser_data_migrator_util