chromium/chrome/installer/setup/unpack_archive.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This file defines the unpack function needed to unpack compressed and
// uncompressed archives in setup.exe.

#include "chrome/installer/setup/unpack_archive.h"

#include <memory>

#include "base/check.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "base/version.h"
#include "chrome/installer/setup/archive_patch_helper.h"
#include "chrome/installer/setup/installer_state.h"
#include "chrome/installer/setup/setup_constants.h"
#include "chrome/installer/setup/setup_util.h"
#include "chrome/installer/util/installation_state.h"
#include "chrome/installer/util/installer_util_strings.h"
#include "chrome/installer/util/util_constants.h"

namespace installer {

namespace {

// Returns nullptr if no compressed archive is available for processing,
// otherwise returns a patch helper configured to uncompress and patch.
std::unique_ptr<ArchivePatchHelper> CreateChromeArchiveHelper(
    const base::FilePath& setup_exe,
    const base::FilePath& install_archive,
    const InstallerState& installer_state,
    const base::FilePath& working_directory,
    UnPackConsumer consumer) {
  // A compressed archive is ordinarily given on the command line by the mini
  // installer. If one was not given, look for chrome.packed.7z next to the
  // running program.
  base::FilePath compressed_archive =
      install_archive.empty()
          ? setup_exe.DirName().Append(kChromeCompressedArchive)
          : install_archive;

  // Fail if no compressed archive is found.
  if (!base::PathExists(compressed_archive)) {
    LOG_IF(ERROR, !install_archive.empty())
        << switches::kInstallArchive << "=" << compressed_archive.value()
        << " not found.";
    return nullptr;
  }

  // chrome.7z is either extracted directly from the compressed archive into the
  // working dir or is the target of patching in the working dir.
  base::FilePath target(working_directory.Append(kChromeArchive));
  DCHECK(!base::PathExists(target));

  // Specify an empty path for the patch source since it isn't yet known that
  // one is needed. It will be supplied in UncompressAndPatchChromeArchive if it
  // is.
  return std::make_unique<ArchivePatchHelper>(
      working_directory, compressed_archive, base::FilePath(), target,
      consumer);
}

}  // namespace

// Workhorse for producing an uncompressed archive (chrome.7z) given a
// chrome.packed.7z containing either a patch file based on the version of
// chrome being updated or the full uncompressed archive. Returns true on
// success, in which case |archive_type| is populated based on what was found.
// Returns false on failure, in which case |install_status| contains the error
// code and the result is written to the registry (via WriteInstallerResult).
base::expected<void, InstallStatus> UncompressAndPatchChromeArchive(
    const InstallationState& original_state,
    const InstallerState& installer_state,
    ArchivePatchHelper* archive_helper,
    ArchiveType* archive_type,
    const base::Version& previous_version) {
  installer_state.SetStage(UNCOMPRESSING);

  // UMA tells us the following about the time required for uncompression as of
  // M75:
  // --- Foreground (<10%) ---
  //   Full archive: 7.5s (50%ile) / 52s (99%ile)
  //   Archive patch: <2s (50%ile) / 10-20s (99%ile)
  // --- Background (>90%) ---
  //   Full archive: 22s (50%ile) / >3m (99%ile)
  //   Archive patch: ~2s (50%ile) / 1.5m - >3m (99%ile)
  //
  // The top unpack failure result with 28 days aggregation (>=0.01%)
  // Setup.Install.LzmaUnPackResult_CompressedChromeArchive
  // 13.50% DISK_FULL
  // 0.67% ERROR_NO_SYSTEM_RESOURCES
  // 0.12% ERROR_IO_DEVICE
  // 0.05% INVALID_HANDLE
  // 0.01% INVALID_LEVEL
  // 0.01% FILE_NOT_FOUND
  // 0.01% LOCK_VIOLATION
  // 0.01% ACCESS_DENIED
  //
  // Setup.Install.LzmaUnPackResult_ChromeArchivePatch
  // 0.09% DISK_FULL
  // 0.01% FILE_NOT_FOUND
  //
  // More information can also be found with metrics:
  // Setup.Install.LzmaUnPackNTSTATUS_CompressedChromeArchive
  // Setup.Install.LzmaUnPackNTSTATUS_ChromeArchivePatch
  if (!archive_helper->Uncompress(nullptr)) {
    installer_state.WriteInstallerResult(
        UNCOMPRESSION_FAILED, IDS_INSTALL_UNCOMPRESSION_FAILED_BASE, nullptr);
    return base::unexpected(UNCOMPRESSION_FAILED);
  }

  // Short-circuit if uncompression produced the uncompressed archive rather
  // than a patch file.
  if (base::PathExists(archive_helper->target())) {
    *archive_type = FULL_ARCHIVE_TYPE;
    return base::ok();
  }

  // Find the installed version's archive to serve as the source for patching.
  base::FilePath patch_source(
      FindArchiveToPatch(original_state, installer_state, previous_version));
  if (patch_source.empty()) {
    LOG(ERROR) << "Failed to find archive to patch.";
    installer_state.WriteInstallerResult(DIFF_PATCH_SOURCE_MISSING,
                                         IDS_INSTALL_UNCOMPRESSION_FAILED_BASE,
                                         nullptr);
    return base::unexpected(DIFF_PATCH_SOURCE_MISSING);
  }
  archive_helper->set_patch_source(patch_source);

  // UMA tells us the following about the time required for patching as of M75:
  // --- Foreground ---
  //   12s (50%ile) / 3-6m (99%ile)
  // --- Background ---
  //   1m (50%ile) / >60m (99%ile)
  installer_state.SetStage(PATCHING);
  if (!archive_helper->ApplyAndDeletePatch()) {
    installer_state.WriteInstallerResult(APPLY_DIFF_PATCH_FAILED,
                                         IDS_INSTALL_UNCOMPRESSION_FAILED_BASE,
                                         nullptr);
    return base::unexpected(APPLY_DIFF_PATCH_FAILED);
  }

  *archive_type = INCREMENTAL_ARCHIVE_TYPE;
  return base::ok();
}

base::expected<void, InstallStatus> UnpackAndMaybePatchChromeArchive(
    const base::FilePath& unpack_path,
    InstallationState& original_state,
    const base::FilePath& setup_exe,
    const base::CommandLine& cmd_line,
    const InstallerState& installer_state,
    ArchiveType* archive_type,
    base::FilePath& uncompressed_archive) {
  *archive_type = UNKNOWN_ARCHIVE_TYPE;
  base::FilePath install_archive =
      cmd_line.GetSwitchValuePath(switches::kInstallArchive);
  // If this is an uncompressed installation then pass the uncompressed
  // chrome.7z directly, so the chrome.packed.7z unpacking step will be
  // bypassed.
  uncompressed_archive =
      cmd_line.GetSwitchValuePath(switches::kUncompressedArchive);
  if (!install_archive.empty() || uncompressed_archive.empty()) {
    if (!uncompressed_archive.empty()) {
      LOG(ERROR)
          << "A compressed archive and an uncompressed archive were both "
             "provided. This is unsupported. Please provide one archive.";
      return base::unexpected(UNSUPPORTED_OPTION);
    }
    base::Version previous_version;
    if (cmd_line.HasSwitch(switches::kPreviousVersion)) {
      previous_version = base::Version(
          cmd_line.GetSwitchValueASCII(switches::kPreviousVersion));
    }

    std::unique_ptr<ArchivePatchHelper> archive_helper(
        CreateChromeArchiveHelper(
            setup_exe, install_archive, installer_state, unpack_path,
            (previous_version.IsValid()
                 ? UnPackConsumer::CHROME_ARCHIVE_PATCH
                 : UnPackConsumer::COMPRESSED_CHROME_ARCHIVE)));
    if (archive_helper) {
      VLOG(1) << "Installing Chrome from compressed archive "
              << archive_helper->compressed_archive().value();
      RETURN_IF_ERROR(UncompressAndPatchChromeArchive(
          original_state, installer_state, archive_helper.get(), archive_type,
          previous_version));
      uncompressed_archive = archive_helper->target();
      DCHECK(!uncompressed_archive.empty());
    }
  }

  // Check for an uncompressed archive alongside the current executable if one
  // was not given or generated.
  if (uncompressed_archive.empty()) {
    uncompressed_archive = setup_exe.DirName().Append(kChromeArchive);
  }

  if (*archive_type == UNKNOWN_ARCHIVE_TYPE) {
    // An archive was not uncompressed or patched above.
    if (uncompressed_archive.empty() ||
        !base::PathExists(uncompressed_archive)) {
      LOG(ERROR) << "Cannot install Chrome without an uncompressed archive.";
      installer_state.WriteInstallerResult(
          INVALID_ARCHIVE, IDS_INSTALL_INVALID_ARCHIVE_BASE, nullptr);
      return base::unexpected(INVALID_ARCHIVE);
    }
    *archive_type = FULL_ARCHIVE_TYPE;
  }

  // Unpack the uncompressed archive.
  // UMA tells us the following about the time required to unpack as of M75:
  // --- Foreground ---
  //   <2.7s (50%ile) / 45s (99%ile)
  // --- Background ---
  //   ~14s (50%ile) / >3m (99%ile)
  //
  // The top unpack failure result with 28 days aggregation (>=0.01%)
  // Setup.Install.LzmaUnPackResult_UncompressedChromeArchive
  // 0.66% DISK_FULL
  // 0.04% ACCESS_DENIED
  // 0.01% INVALID_HANDLE
  // 0.01% ERROR_NO_SYSTEM_RESOURCES
  // 0.01% PATH_NOT_FOUND
  // 0.01% ERROR_IO_DEVICE
  //
  // More information can also be found with metric:
  // Setup.Install.LzmaUnPackNTSTATUS_UncompressedChromeArchive
  installer_state.SetStage(UNPACKING);
  UnPackStatus unpack_status = UnPackArchive(uncompressed_archive, unpack_path,
                                             /*output_file=*/nullptr);
  RecordUnPackMetrics(unpack_status,
                      UnPackConsumer::UNCOMPRESSED_CHROME_ARCHIVE);
  if (unpack_status != UNPACK_NO_ERROR) {
    installer_state.WriteInstallerResult(
        UNPACKING_FAILED, IDS_INSTALL_UNCOMPRESSION_FAILED_BASE, nullptr);
    return base::unexpected(UNPACKING_FAILED);
  }
  return base::ok();
}

}  // namespace installer