chromium/chrome/browser/component_updater/cros_component_installer_chromeos.cc

// Copyright 2017 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/component_updater/cros_component_installer_chromeos.h"

#include <map>
#include <utility>

#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/login/demo_mode/demo_mode_dimensions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/component_updater/component_installer_errors.h"
#include "chrome/browser/component_updater/metadata_table_chromeos.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/dbus/dbus_thread_manager.h"
#include "chromeos/ash/components/dbus/image_loader/image_loader_client.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/component_updater/component_updater_paths.h"
#include "components/component_updater/component_updater_switches.h"
#include "components/crx_file/id_util.h"
#include "components/prefs/pref_service.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "crypto/sha2.h"

namespace component_updater {

// Switch that can be used for opting in to receive DCHECK-enabled binaries. If
// we need to expose this through chrome://flags on other platforms this can
// move to a shared place (but still share the prefer-dcheck name).
const char kPreferDcheckSwitch[] = "prefer-dcheck";
const char kPreferDcheckOptIn[] = "opt-in";
const char kPreferDcheckOptOut[] = "opt-out";

// Root path where all components are stored.
constexpr char kComponentsRootPath[] = "cros-components";

namespace {

// All downloadable Chrome OS components.
const ComponentConfig kConfigs[] = {
    {"cros-termina", ComponentConfig::PolicyType::kEnvVersion, "980.1",
     "e9d960f84f628e1f42d05de4046bb5b3154b6f1f65c08412c6af57a29aecaffb"},
    {"rtanalytics-full", ComponentConfig::PolicyType::kEnvVersion, "106.0",
     "c93c3e1013c52100a20038b405ac854d69fa889f6dc4fa6f188267051e05e444"},
    {"demo-mode-resources", ComponentConfig::PolicyType::kEnvVersion, "1.0",
     "93c093ebac788581389015e9c59c5af111d2fa5174d206eb795042e6376cbd10"},
    {"demo-mode-app", ComponentConfig::PolicyType::kDemoApp, nullptr,
     "b6c5ce9f03b0ce830eb5f9f92ed3016cfdb7a2327330f0187adbe9a00ddfd34d"},
    // NOTE: If you change the lacros component names, you must also update
    // chrome/browser/ash/crosapi/browser_loader.cc.
    {"lacros-dogfood-canary", ComponentConfig::PolicyType::kLacros, nullptr,
     "7a85ffb4b316a3b89135a3f43660ef3049950a61a2f8df4237e1ec213852b848"},
    {"lacros-dogfood-dev", ComponentConfig::PolicyType::kLacros, nullptr,
     "b3e1ef1780c0acd2d3fa44b4d73c657a0f1ed3ad83fd8c964a18a3502ccf5f4f"},
    {"lacros-dogfood-beta", ComponentConfig::PolicyType::kLacros, nullptr,
     "7d5c1428f7f67b56f95123851adec1da105980c56b5c126352040f3b65d3e43b"},
    {"lacros-dogfood-stable", ComponentConfig::PolicyType::kLacros, nullptr,
     "47f910805afac79e2d4d9117c42d5291a32ac60a4ea1a42e537fd86082c3ba48"},
    {"growth-campaigns", ComponentConfig::PolicyType::kGrowthCampaigns, nullptr,
     "36448796af5fb67380ec0180a8379ddd26fce20d3da6a231e0a60dfe2360407e"},
};

const char* g_ash_version_for_test = nullptr;

// Returns the major version of the current binary, which is the ash/OS binary.
// For example, for ash 89.0.1234.1 returns 89.
uint32_t GetAshMajorVersion() {
  base::Version ash_version = g_ash_version_for_test
                                  ? base::Version(g_ash_version_for_test)
                                  : version_info::GetVersion();
  return ash_version.components()[0];
}

const ComponentConfig* FindConfig(const std::string& name) {
  const ComponentConfig* config =
      base::ranges::find(kConfigs, name, &ComponentConfig::name);
  if (config == std::end(kConfigs)) {
    return nullptr;
  }
  return config;
}

void LogCustomUninstall(std::optional<bool> result) {}

void FinishCustomUninstallOnUIThread(const std::string& name) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  ash::ImageLoaderClient::Get()->UnmountComponent(
      name, base::BindOnce(&LogCustomUninstall));
}

std::string GenerateId(const std::string& sha2hashstr) {
  // kIdSize is the count of a pair of hex in the sha2hash array.
  // In string representation of sha2hash, size is doubled since each hex is
  // represented by a single char.
  return crx_file::id_util::GenerateIdFromHex(
      sha2hashstr.substr(0, crx_file::id_util::kIdSize * 2));
}

// Returns all installed components.
std::vector<ComponentConfig> GetInstalled() {
  std::vector<ComponentConfig> configs;
  base::FilePath root;
  if (!base::PathService::Get(DIR_COMPONENT_USER, &root)) {
    return configs;
  }

  root = root.Append(kComponentsRootPath);
  for (const ComponentConfig& config : kConfigs) {
    base::FilePath component_path = root.Append(config.name);
    if (base::PathExists(component_path)) {
      configs.push_back(config);
    }
  }
  return configs;
}

const bool kDefaultLacrosAllowUpdates = true;

// Report Error code.
void ReportError(ComponentManagerAsh::Error error) {
  UMA_HISTOGRAM_ENUMERATION("ComponentUpdater.InstallResult", error);
}

}  // namespace

CrOSComponentInstallerPolicy::CrOSComponentInstallerPolicy(
    const ComponentConfig& config,
    CrOSComponentInstaller* cros_component_installer)
    : cros_component_installer_(cros_component_installer), name_(config.name) {
  if (strlen(config.sha2hash) != crypto::kSHA256Length * 2) {
    return;
  }

  bool converted = base::HexStringToBytes(config.sha2hash, &sha2_hash_);
  DCHECK(converted);
  DCHECK_EQ(crypto::kSHA256Length, sha2_hash_.size());
}

CrOSComponentInstallerPolicy::~CrOSComponentInstallerPolicy() = default;

bool CrOSComponentInstallerPolicy::SupportsGroupPolicyEnabledComponentUpdates()
    const {
  return true;
}

bool CrOSComponentInstallerPolicy::RequiresNetworkEncryption() const {
  return true;
}

bool CrOSComponentInstallerPolicy::AllowUpdates() const {
  return true;
}

update_client::CrxInstaller::Result
CrOSComponentInstallerPolicy::OnCustomInstall(
    const base::Value::Dict& manifest,
    const base::FilePath& install_dir) {
  cros_component_installer_->EmitInstalledSignal(GetName());

  return update_client::CrxInstaller::Result(update_client::InstallError::NONE);
}

void CrOSComponentInstallerPolicy::OnCustomUninstall() {
  cros_component_installer_->UnregisterCompatiblePath(name_);

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&FinishCustomUninstallOnUIThread, name_));
}

bool CrOSComponentInstallerPolicy::VerifyInstallation(
    const base::Value::Dict& manifest,
    const base::FilePath& install_dir) const {
  return true;
}

base::FilePath CrOSComponentInstallerPolicy::GetRelativeInstallDir() const {
  base::FilePath path = base::FilePath(kComponentsRootPath);
  return path.Append(name_);
}

void CrOSComponentInstallerPolicy::GetHash(std::vector<uint8_t>* hash) const {
  *hash = sha2_hash_;
}

std::string CrOSComponentInstallerPolicy::GetName() const {
  return name_;
}

EnvVersionInstallerPolicy::EnvVersionInstallerPolicy(
    const ComponentConfig& config,
    CrOSComponentInstaller* cros_component_installer)
    : CrOSComponentInstallerPolicy(config, cros_component_installer),
      env_version_(config.env_version) {
  DCHECK(!env_version_.empty());
}

EnvVersionInstallerPolicy::~EnvVersionInstallerPolicy() = default;

void EnvVersionInstallerPolicy::ComponentReady(const base::Version& version,
                                               const base::FilePath& path,
                                               base::Value::Dict manifest) {
  std::string* min_env_version = manifest.FindString("min_env_version");
  if (!min_env_version) {
    return;
  }

  if (!IsCompatible(env_version_, *min_env_version)) {
    return;
  }

  cros_component_installer_->RegisterCompatiblePath(
      GetName(), CompatibleComponentInfo(path, version));
}

update_client::InstallerAttributes
EnvVersionInstallerPolicy::GetInstallerAttributes() const {
  update_client::InstallerAttributes attrs;
  attrs["_env_version"] = env_version_;
  return attrs;
}

// static
bool EnvVersionInstallerPolicy::IsCompatible(
    const std::string& env_version_str,
    const std::string& min_env_version_str) {
  base::Version env_version(env_version_str);
  base::Version min_env_version(min_env_version_str);
  return env_version.IsValid() && min_env_version.IsValid() &&
         env_version.components()[0] == min_env_version.components()[0] &&
         env_version >= min_env_version;
}

LacrosInstallerPolicy::LacrosInstallerPolicy(
    const ComponentConfig& config,
    CrOSComponentInstaller* cros_component_installer)
    : CrOSComponentInstallerPolicy(config, cros_component_installer) {}

LacrosInstallerPolicy::~LacrosInstallerPolicy() = default;

void LacrosInstallerPolicy::ComponentReady(const base::Version& version,
                                           const base::FilePath& path,
                                           base::Value::Dict manifest) {
  // Each version of Lacros guarantees it will be compatible through the same
  // major ash/OS version and -2. For example, Lacros 89 will work with ash/OS
  // 89, 88, and 87. But it may not work with ash/OS 86 or 90.
  //
  // As you see we (client side) only enforces the Lacros/Ash same version
  // check here, while the code does not check the -2 version skew requirement.
  // This is because go/lacros-version-skew-guide mentions the restriction on
  // lacros being too new is enforced on the Omaha server side - and the too
  // old check is enforced client side. Supposedly this makes it easy for us to
  // start supporting newer lacros versions by just updating the Omaha server
  // code.
  uint32_t lacros_major_version = version.components()[0];
  if (lacros_major_version < GetAshMajorVersion()) {
    // Current lacros install is not compatible.
    return;
  }
  cros_component_installer_->RegisterCompatiblePath(
      GetName(), CompatibleComponentInfo(path, version));

  // Clear the load cache for the newly installed component version to avoid
  // loading stale components on successive loads, causing a version update
  // restart loop (see crbug.com/1322678).
  cros_component_installer_->RemoveLoadCacheEntry(GetName());
}

update_client::InstallerAttributes
LacrosInstallerPolicy::GetInstallerAttributes() const {
  update_client::InstallerAttributes attributes;
  auto* const cmdline = base::CommandLine::ForCurrentProcess();
  if (cmdline->HasSwitch(kPreferDcheckSwitch)) {
    attributes[kPreferDcheckSwitch] =
        cmdline->GetSwitchValueASCII(kPreferDcheckSwitch);
  }
  return attributes;
}

// Lacros is supposed to be updated even if component updates are turned off.
bool LacrosInstallerPolicy::SupportsGroupPolicyEnabledComponentUpdates() const {
  return false;
}

bool LacrosInstallerPolicy::AllowUpdates() const {
  bool allow_updates = kDefaultLacrosAllowUpdates;

  ash::CrosSettings* settings = ash::CrosSettings::Get();
  if (!settings) {
    return allow_updates;
  }

  const base::Value* os_updates_disabled =
      settings->GetPref(ash::kUpdateDisabled);
  if (os_updates_disabled == nullptr || !os_updates_disabled->is_bool()) {
    return allow_updates;
  }

  // We disable Lacros updates when ChromeOS system updates are disabled.
  allow_updates = !os_updates_disabled->GetBool();

  return allow_updates;
}

// static
void LacrosInstallerPolicy::SetAshVersionForTest(const char* version) {
  g_ash_version_for_test = version;
}

DemoAppInstallerPolicy::DemoAppInstallerPolicy(
    const ComponentConfig& config,
    CrOSComponentInstaller* cros_component_installer)
    : CrOSComponentInstallerPolicy(config, cros_component_installer) {}

DemoAppInstallerPolicy::~DemoAppInstallerPolicy() = default;

void DemoAppInstallerPolicy::ComponentReady(const base::Version& version,
                                            const base::FilePath& path,
                                            base::Value::Dict manifest) {
  cros_component_installer_->RegisterCompatiblePath(
      GetName(), CompatibleComponentInfo(path, version));
}

update_client::InstallerAttributes
DemoAppInstallerPolicy::GetInstallerAttributes() const {
  update_client::InstallerAttributes demo_app_installer_attributes;
  demo_app_installer_attributes["retailer_id"] = ash::demo_mode::RetailerName();
  demo_app_installer_attributes["store_id"] = ash::demo_mode::StoreNumber();
  demo_app_installer_attributes["demo_country"] = ash::demo_mode::Country();
  demo_app_installer_attributes["is_cloud_gaming_device"] =
      ash::demo_mode::IsCloudGamingDevice() ? "true" : "false";
  demo_app_installer_attributes["is_feature_aware_device"] =
      ash::demo_mode::IsFeatureAwareDevice() ? "true" : "false";

  auto* const cmdline = base::CommandLine::ForCurrentProcess();
  if (cmdline->HasSwitch(switches::kDemoModeTestTag)) {
    demo_app_installer_attributes["tag"] =
        cmdline->GetSwitchValueASCII(switches::kDemoModeTestTag);
  }
  return demo_app_installer_attributes;
}

GrowthCampaignsInstallerPolicy::GrowthCampaignsInstallerPolicy(
    const ComponentConfig& config,
    CrOSComponentInstaller* cros_component_installer)
    : CrOSComponentInstallerPolicy(config, cros_component_installer) {}

GrowthCampaignsInstallerPolicy::~GrowthCampaignsInstallerPolicy() = default;

void GrowthCampaignsInstallerPolicy::ComponentReady(
    const base::Version& version,
    const base::FilePath& path,
    base::Value::Dict manifest) {
  cros_component_installer_->RegisterCompatiblePath(
      GetName(), CompatibleComponentInfo(path, version));
}

update_client::InstallerAttributes
GrowthCampaignsInstallerPolicy::GetInstallerAttributes() const {
  update_client::InstallerAttributes attributes;
  auto* const cmdline = base::CommandLine::ForCurrentProcess();
  if (cmdline->HasSwitch(switches::kCampaignsTestTag)) {
    attributes["tag"] =
        cmdline->GetSwitchValueASCII(switches::kCampaignsTestTag);
  }
  return attributes;
}

CrOSComponentInstaller::CrOSComponentInstaller(
    std::unique_ptr<MetadataTable> metadata_table,
    ComponentUpdateService* component_updater)
    : metadata_table_(std::move(metadata_table)),
      component_updater_(component_updater) {}

CrOSComponentInstaller::~CrOSComponentInstaller() = default;

void CrOSComponentInstaller::SetDelegate(Delegate* delegate) {
  delegate_ = delegate;
}

void CrOSComponentInstaller::Load(const std::string& name,
                                  MountPolicy mount_policy,
                                  UpdatePolicy update_policy,
                                  LoadCallback load_callback) {
  if (!IsCompatible(name) || update_policy == UpdatePolicy::kForce) {
    // A compatible component is not installed, or forced update is requested.
    // Start registration and installation/update process.
    Install(name, update_policy, mount_policy, std::move(load_callback));
  } else if (mount_policy == MountPolicy::kMount) {
    // A compatible component is installed, load it.
    LoadInternal(name, std::move(load_callback));
  } else {
    // A compatible component is installed, do not load it.
    constexpr Error error = Error::NONE;
    ReportError(error);
    std::move(load_callback).Run(error, base::FilePath());
  }
}

bool CrOSComponentInstaller::Unload(const std::string& name) {
  DispatchFailedLoads(std::move(load_cache_[name].callbacks));
  load_cache_.erase(name);

  const ComponentConfig* config = FindConfig(name);
  if (!config) {
    // Component |name| does not exist.
    return false;
  }
  const std::string id = GenerateId(config->sha2hash);
  metadata_table_->DeleteComponentForCurrentUser(name);
  return metadata_table_->HasComponentForAnyUser(name) ||
         component_updater_->UnregisterComponent(id);
}

void CrOSComponentInstaller::GetVersion(
    const std::string& name,
    base::OnceCallback<void(const base::Version&)> version_callback) const {
  if (!IsCompatible(name)) {
    // `name` does not match to any component.
    std::move(version_callback).Run(base::Version());
    return;
  }

  auto component_iter = compatible_components_.find(name);

  // Path compatible to `name` must exist.
  CHECK(component_iter != compatible_components_.end() &&
        !(component_iter->second.path.empty()));
  if (component_iter->second.version.has_value()) {
    content::GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(std::move(version_callback),
                                  component_iter->second.version.value()));
  } else {
    ash::ImageLoaderClient::Get()->RequestComponentVersion(
        name, base::BindOnce(&CrOSComponentInstaller::FinishGetVersion,
                             weak_factory_.GetWeakPtr(),
                             std::move(version_callback)));
  }
}

void CrOSComponentInstaller::RegisterInstalled() {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()}, base::BindOnce(GetInstalled),
      base::BindOnce(&CrOSComponentInstaller::RegisterN,
                     weak_factory_.GetWeakPtr()));
}

void CrOSComponentInstaller::RegisterCompatiblePath(
    const std::string& name,
    CompatibleComponentInfo info) {
  compatible_components_[name] = std::move(info);
}

void CrOSComponentInstaller::UnregisterCompatiblePath(const std::string& name) {
  DispatchFailedLoads(std::move(load_cache_[name].callbacks));
  load_cache_.erase(name);
  compatible_components_.erase(name);
}

base::FilePath CrOSComponentInstaller::GetCompatiblePath(
    const std::string& name) const {
  const auto it = compatible_components_.find(name);
  return it == compatible_components_.end() ? base::FilePath()
                                            : it->second.path;
}

void CrOSComponentInstaller::EmitInstalledSignal(const std::string& component) {
  if (delegate_) {
    delegate_->EmitInstalledSignal(component);
  }
}

CrOSComponentInstaller::LoadInfo::LoadInfo() = default;
CrOSComponentInstaller::LoadInfo::~LoadInfo() = default;
std::map<std::string, CrOSComponentInstaller::LoadInfo>&
CrOSComponentInstaller::GetLoadCacheForTesting() {
  return load_cache_;
}

void CrOSComponentInstaller::RemoveLoadCacheEntry(
    const std::string& component_name) {
  load_cache_.erase(component_name);
}

bool CrOSComponentInstaller::IsRegisteredMayBlock(const std::string& name) {
  base::FilePath root;
  if (!base::PathService::Get(DIR_COMPONENT_USER, &root)) {
    return false;
  }

  return base::PathExists(root.Append(kComponentsRootPath).Append(name));
}

void CrOSComponentInstaller::Register(const ComponentConfig& config,
                                      base::OnceClosure register_callback) {
  std::unique_ptr<CrOSComponentInstallerPolicy> policy;
  switch (config.policy_type) {
    case ComponentConfig::PolicyType::kEnvVersion:
      policy = std::make_unique<EnvVersionInstallerPolicy>(config, this);
      break;
    case ComponentConfig::PolicyType::kLacros:
      policy = std::make_unique<LacrosInstallerPolicy>(config, this);
      break;
    case ComponentConfig::PolicyType::kDemoApp:
      policy = std::make_unique<DemoAppInstallerPolicy>(config, this);
      break;
    case ComponentConfig::PolicyType::kGrowthCampaigns:
      policy = std::make_unique<GrowthCampaignsInstallerPolicy>(config, this);
      break;
  }
  auto installer = base::MakeRefCounted<ComponentInstaller>(std::move(policy));
  installer->Register(component_updater_, std::move(register_callback));
}

void CrOSComponentInstaller::Install(const std::string& name,
                                     UpdatePolicy update_policy,
                                     MountPolicy mount_policy,
                                     LoadCallback load_callback) {
  const ComponentConfig* config = FindConfig(name);
  if (!config) {
    constexpr Error error = Error::UNKNOWN_COMPONENT;
    ReportError(error);
    std::move(load_callback).Run(error, base::FilePath());
    return;
  }

  Register(
      *config,
      base::BindOnce(
          &CrOSComponentInstaller::StartInstall, weak_factory_.GetWeakPtr(),
          name, GenerateId(config->sha2hash), update_policy,
          base::BindOnce(&CrOSComponentInstaller::FinishInstall,
                         weak_factory_.GetWeakPtr(), name, mount_policy,
                         update_policy, std::move(load_callback))));
}

void CrOSComponentInstaller::StartInstall(
    const std::string& name,
    const std::string& id,
    UpdatePolicy update_policy,
    update_client::Callback install_callback) {
  // Check whether an installed component was found during registration, and
  // determine whether OnDemandUpdater should be started accordingly.
  const bool is_compatible = IsCompatible(name);
  if (update_policy == UpdatePolicy::kSkip ||
      (is_compatible && update_policy != UpdatePolicy::kForce)) {
    std::move(install_callback).Run(update_client::Error::NONE);
    return;
  }

  const component_updater::OnDemandUpdater::Priority priority =
      is_compatible ? component_updater::OnDemandUpdater::Priority::BACKGROUND
                    : component_updater::OnDemandUpdater::Priority::FOREGROUND;
  component_updater_->GetOnDemandUpdater().OnDemandUpdate(
      id, priority, std::move(install_callback));
}

void CrOSComponentInstaller::FinishInstall(const std::string& name,
                                           MountPolicy mount_policy,
                                           UpdatePolicy update_policy,
                                           LoadCallback load_callback,
                                           update_client::Error error) {
  if (error != update_client::Error::NONE) {
    Error err = Error::INSTALL_FAILURE;
    if (error == update_client::Error::UPDATE_IN_PROGRESS) {
      err = Error::UPDATE_IN_PROGRESS;
    }
    ReportError(err);
    std::move(load_callback).Run(err, base::FilePath());
  } else if (!IsCompatible(name)) {
    const Error err = update_policy == UpdatePolicy::kSkip
                          ? Error::NOT_FOUND
                          : Error::COMPATIBILITY_CHECK_FAILED;
    ReportError(err);
    std::move(load_callback).Run(err, base::FilePath());
  } else if (mount_policy == MountPolicy::kMount) {
    LoadInternal(name, std::move(load_callback));
  } else {
    constexpr Error err = Error::NONE;
    ReportError(err);
    std::move(load_callback).Run(err, base::FilePath());
  }
}

void CrOSComponentInstaller::LoadInternal(const std::string& name,
                                          LoadCallback load_callback) {
  // Use the cached value if it exists.
  auto it = load_cache_.find(name);
  if (it != load_cache_.end()) {
    // If the request is ongoing, queue up a callback.
    if (!it->second.success.has_value()) {
      it->second.callbacks.push_back(std::move(load_callback));
      return;
    }
    // Otherwise immediately dispatch.
    DispatchLoadCallback(std::move(load_callback), it->second.path,
                         it->second.success.value());
    return;
  }

  // Update the cache to indicate the request is being queued.
  load_cache_[name].success = std::nullopt;

  const base::FilePath path = GetCompatiblePath(name);
  DCHECK(!path.empty());
  ash::ImageLoaderClient::Get()->LoadComponentAtPath(
      name, path,
      base::BindOnce(&CrOSComponentInstaller::FinishLoad,
                     weak_factory_.GetWeakPtr(), std::move(load_callback),
                     name));
}

void CrOSComponentInstaller::FinishLoad(LoadCallback load_callback,
                                        const std::string& name,
                                        std::optional<base::FilePath> result) {
  // ImageLoader returns an empty path if mount failed.
  bool success = result.has_value() && !result.value().empty();
  base::FilePath path;
  if (success) {
    path = result.value();
  }

  DispatchLoadCallback(std::move(load_callback), path, success);

  // Update the cache.
  auto it = load_cache_.find(name);
  if (it != load_cache_.end()) {
    it->second.success = success;
    it->second.path = path;

    // Dispatch queued up callbacks.
    for (LoadCallback& queued_callback : it->second.callbacks) {
      DispatchLoadCallback(std::move(queued_callback), path, success);
    }
    it->second.callbacks.clear();
  }
}

void CrOSComponentInstaller::FinishGetVersion(
    base::OnceCallback<void(const base::Version&)> version_callback,
    std::optional<std::string> result) const {
  std::move(version_callback).Run(base::Version(result.value_or("")));
}

void CrOSComponentInstaller::RegisterN(
    const std::vector<ComponentConfig>& configs) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  for (const auto& config : configs) {
    Register(config, base::OnceClosure());
  }
}

bool CrOSComponentInstaller::IsCompatible(const std::string& name) const {
  return compatible_components_.count(name) > 0;
}

void CrOSComponentInstaller::DispatchLoadCallback(LoadCallback callback,
                                                  base::FilePath path,
                                                  bool success) {
  Error error = success ? Error::NONE : Error::MOUNT_FAILURE;
  ReportError(error);
  std::move(callback).Run(error, std::move(path));
}

void CrOSComponentInstaller::DispatchFailedLoads(
    std::vector<LoadCallback> callbacks) {
  for (LoadCallback& callback : callbacks) {
    DispatchLoadCallback(std::move(callback), base::FilePath(),
                         /*success=*/false);
  }
}

}  // namespace component_updater