Fix Windows delta updates (#31937)

Chromium 142 broke delta updates on Windows. This commit fixes them by
reverting upstream commits 13ca199c and a4d120da with a few patches,
vendored code and chromium_src overrides. The changes should affect the
browser Windows installer only, and not the Omaha update client.

Co-authored-by: Max Karolinskiy <max@brave.com>
This commit is contained in:
Michael Herrmann
2025-12-04 19:16:50 +01:00
committed by GitHub
co-authored by Max Karolinskiy
parent d34a60dc3c
commit 49faa97bb6
227 changed files with 3532 additions and 89 deletions
+38
View File
@@ -0,0 +1,38 @@
# Copyright (c) 2025 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
import("//testing/test.gni")
assert(is_win)
source_set("unit_tests") {
sources = [
"archive_patch_helper_unittest.cc",
"brave_setup_util_unittest.cc",
]
deps = [
"//base",
"//base/test:test_support",
"//chrome/common:constants",
"//chrome/installer/setup:lib",
"//chrome/installer/util:constants",
"//chrome/installer/util:metainstaller_utils",
"//chrome/installer/util:with_no_strings",
"//testing/gtest",
]
testonly = true
}
# As of this writing, the sole purpose of this GN target is to call
# chrome::RegisterPathProvider() for archive_patch_helper_unittest.cc.
source_set("run_all_unittests") {
sources = [ "run_all_unittests.cc" ]
deps = [
"//base",
"//base/test:test_support",
"//chrome/common:constants",
]
testonly = true
}
+10
View File
@@ -0,0 +1,10 @@
include_rules = [
"+brave/third_party/bspatch",
"+chrome/common/chrome_paths.h",
"+chrome/installer/setup",
"+chrome/installer/util/lzma_util.h",
"+chrome/installer/util/installation_state.h",
"+chrome/installer/util/util_constants.h",
"+components/zucchini/zucchini.h",
"+components/zucchini/zucchini_integration.h",
]
+125
View File
@@ -0,0 +1,125 @@
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
// This file contains code that used to be upstream and had to be restored in
// Brave to support delta updates on Windows until we are on Omaha 4. See:
// github.com/brave/brave-core/pull/31937
#include "brave/installer/setup/archive_patch_helper.h"
#include <stdint.h>
#include <optional>
#include "base/files/file_util.h"
#include "base/logging.h"
#include "brave/third_party/bspatch/mbspatch.h"
#include "chrome/installer/util/lzma_util.h"
#include "components/zucchini/zucchini.h"
#include "components/zucchini/zucchini_integration.h"
namespace installer {
ArchivePatchHelper::ArchivePatchHelper(const base::FilePath& working_directory,
const base::FilePath& compressed_archive,
const base::FilePath& patch_source,
const base::FilePath& target,
UnPackConsumer consumer)
: working_directory_(working_directory),
compressed_archive_(compressed_archive),
patch_source_(patch_source),
target_(target),
consumer_(consumer) {}
ArchivePatchHelper::~ArchivePatchHelper() = default;
// static
bool ArchivePatchHelper::UncompressAndPatch(
const base::FilePath& working_directory,
const base::FilePath& compressed_archive,
const base::FilePath& patch_source,
const base::FilePath& target,
UnPackConsumer consumer) {
ArchivePatchHelper instance(working_directory, compressed_archive,
patch_source, target, consumer);
return (instance.Uncompress(nullptr) && instance.ApplyAndDeletePatch());
}
bool ArchivePatchHelper::Uncompress(base::FilePath* last_uncompressed_file) {
// The target shouldn't already exist.
DCHECK(!base::PathExists(target_));
// UnPackArchive takes care of logging.
base::FilePath output_file;
UnPackStatus unpack_status =
UnPackArchive(compressed_archive_, working_directory_, &output_file);
RecordUnPackMetrics(unpack_status, consumer_);
if (unpack_status != UNPACK_NO_ERROR) {
return false;
}
last_uncompressed_file_ = output_file;
if (last_uncompressed_file) {
*last_uncompressed_file = last_uncompressed_file_;
}
return true;
}
bool ArchivePatchHelper::ApplyAndDeletePatch() {
const bool succeeded = ZucchiniEnsemblePatch() || BinaryPatch();
if (!last_uncompressed_file_.empty()) {
base::DeleteFile(last_uncompressed_file_);
}
return succeeded;
}
bool ArchivePatchHelper::ZucchiniEnsemblePatch() {
if (last_uncompressed_file_.empty()) {
LOG(ERROR) << "No patch file found in compressed archive.";
return false;
}
zucchini::status::Code result =
zucchini::Apply(patch_source_, last_uncompressed_file_, target_);
if (result == zucchini::status::kStatusSuccess) {
return true;
}
LOG(ERROR) << "Failed to apply patch " << last_uncompressed_file_.value()
<< " to file " << patch_source_.value() << " and generating file "
<< target_.value()
<< " using Zucchini. err=" << static_cast<uint32_t>(result);
// Ensure a partial output is not left behind.
base::DeleteFile(target_);
return false;
}
bool ArchivePatchHelper::BinaryPatch() {
if (last_uncompressed_file_.empty()) {
LOG(ERROR) << "No patch file found in compressed archive.";
return false;
}
int result = ApplyBinaryPatch(patch_source_.value().c_str(),
last_uncompressed_file_.value().c_str(),
target_.value().c_str());
if (result == OK) {
return true;
}
LOG(ERROR) << "Failed to apply patch " << last_uncompressed_file_.value()
<< " to file " << patch_source_.value() << " and generating file "
<< target_.value() << " using bsdiff. err=" << result;
// Ensure a partial output is not left behind.
base::DeleteFile(target_);
return false;
}
} // namespace installer
+112
View File
@@ -0,0 +1,112 @@
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
// This file contains code that used to be upstream and had to be restored in
// Brave to support delta updates on Windows until we are on Omaha 4. See:
// github.com/brave/brave-core/pull/31937
#ifndef BRAVE_INSTALLER_SETUP_ARCHIVE_PATCH_HELPER_H_
#define BRAVE_INSTALLER_SETUP_ARCHIVE_PATCH_HELPER_H_
#include "base/files/file_path.h"
#include "chrome/installer/setup/setup_util.h"
#include "chrome/installer/util/lzma_util.h"
namespace installer {
// A helper class that facilitates uncompressing and patching the chrome archive
// and installer.
//
// Chrome's installer is deployed along with a compressed archive containing
// either 1) an uncompressd archive of the product binaries or 2) a patch file
// to be applied to the uncompressed archive of the version being updated. To
// obtain the uncompressed archive, the contents of the compressed archive are
// uncompressed and extracted. Installation proceeds directly if the
// uncompressed archive is found after this step. Otherwise, the patch is
// applied to the previous version's uncompressed archive using either
// Zucchini's patching or bspatch.
//
// Chrome's installer itself may also be deployed as a patch against the
// previous version's saved installer binary. The same process is followed to
// obtain the new installer. The compressed archive unconditionally contains a
// patch file in this case.
class ArchivePatchHelper {
public:
// Constructs an instance that can uncompress |compressed_archive| into
// |working_directory| and optionally apply the extracted patch file to
// |patch_source|, writing the result to |target|.
ArchivePatchHelper(const base::FilePath& working_directory,
const base::FilePath& compressed_archive,
const base::FilePath& patch_source,
const base::FilePath& target,
UnPackConsumer consumer);
ArchivePatchHelper(const ArchivePatchHelper&) = delete;
ArchivePatchHelper& operator=(const ArchivePatchHelper&) = delete;
~ArchivePatchHelper();
// Uncompresses |compressed_archive| in |working_directory| then applies the
// extracted patch file to |patch_source|, writing the result to |target|.
// Ensemble patching via Zucchini is attempted first (if it is enabled). If
// that fails bspatch is attempted. Returns false if uncompression or all
// patching steps fail.
static bool UncompressAndPatch(const base::FilePath& working_directory,
const base::FilePath& compressed_archive,
const base::FilePath& patch_source,
const base::FilePath& target,
UnPackConsumer consumer);
// Uncompresses compressed_archive() into the working directory. On success,
// last_uncompressed_file (if not nullptr) is populated with the path to the
// last file extracted from the archive.
bool Uncompress(base::FilePath* last_uncompressed_file);
// Performs ensemble patching on the uncompressed version of
// |compressed_archive| in |working_directory| as specified in the constructor
// using files from |patch_source|. Ensemble patching via Zucchini is
// attempted first (if it is enabled). Zucchini falls back to bspatch if
// unsuccessful. The uncompressed patch file is unconditionally deleted at the
// end.
bool ApplyAndDeletePatch();
// Attempts to use Zucchini to apply last_uncompressed_file() to
// patch_source() to generate target(). Returns false if patching fails.
bool ZucchiniEnsemblePatch();
// Attempts to use bspatch to apply last_uncompressed_file() to patch_source()
// to generate target(). Returns false if patching fails.
bool BinaryPatch();
const base::FilePath& compressed_archive() const {
return compressed_archive_;
}
void set_patch_source(const base::FilePath& patch_source) {
patch_source_ = patch_source;
}
const base::FilePath& patch_source() const { return patch_source_; }
const base::FilePath& target() const { return target_; }
// Returns the path of the last file extracted by Uncompress().
const base::FilePath& last_uncompressed_file() const {
return last_uncompressed_file_;
}
void set_last_uncompressed_file(
const base::FilePath& last_uncompressed_file) {
last_uncompressed_file_ = last_uncompressed_file;
}
private:
base::FilePath working_directory_;
base::FilePath compressed_archive_;
base::FilePath patch_source_;
base::FilePath target_;
base::FilePath last_uncompressed_file_;
UnPackConsumer consumer_;
};
} // namespace installer
#endif // BRAVE_INSTALLER_SETUP_ARCHIVE_PATCH_HELPER_H_
@@ -0,0 +1,83 @@
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
// This file contains code that used to be upstream and had to be restored in
// Brave to support delta updates on Windows until we are on Omaha 4. See:
// github.com/brave/brave-core/pull/31937
#include "brave/installer/setup/archive_patch_helper.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "chrome/common/chrome_paths.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
class ArchivePatchHelperTest : public testing::Test {
protected:
void SetUp() override {
// This requires chrome::RegisterPathProvider() to have been called.
ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &data_dir_));
data_dir_ = data_dir_.AppendASCII("installer");
ASSERT_TRUE(base::PathExists(data_dir_));
// Create a temp directory for testing.
ASSERT_TRUE(test_dir_.CreateUniqueTempDir());
}
void TearDown() override {
data_dir_.clear();
// Clean up test directory manually so we can fail if it leaks.
ASSERT_TRUE(test_dir_.Delete());
}
// The path to input data used in tests.
base::FilePath data_dir_;
// The temporary directory used to contain the test operations.
base::ScopedTempDir test_dir_;
};
} // namespace
TEST_F(ArchivePatchHelperTest, ZucchiniPatching) {
base::FilePath src = data_dir_.AppendASCII("archive1.7z");
base::FilePath patch = data_dir_.AppendASCII("zucchini_archive.diff");
base::FilePath dest = test_dir_.GetPath().AppendASCII("archive2.7z");
installer::ArchivePatchHelper archive_helper(
test_dir_.GetPath(), base::FilePath(), src, dest,
installer::UnPackConsumer::SETUP_EXE_PATCH);
archive_helper.set_last_uncompressed_file(patch);
EXPECT_TRUE(archive_helper.ZucchiniEnsemblePatch());
base::FilePath base = data_dir_.AppendASCII("archive2.7z");
EXPECT_TRUE(base::ContentsEqual(dest, base));
}
TEST_F(ArchivePatchHelperTest, InvalidDiff_MisalignedCblen) {
base::FilePath src = data_dir_.AppendASCII("bin.old");
base::FilePath patch = data_dir_.AppendASCII("misaligned_cblen.diff");
base::FilePath dest = test_dir_.GetPath().AppendASCII("bin.new");
installer::ArchivePatchHelper archive_helper(
test_dir_.GetPath(), base::FilePath(), src, dest,
installer::UnPackConsumer::SETUP_EXE_PATCH);
archive_helper.set_last_uncompressed_file(patch);
// Should fail, but not crash.
EXPECT_FALSE(archive_helper.BinaryPatch());
}
TEST_F(ArchivePatchHelperTest, InvalidDiff_NegativeSeek) {
base::FilePath src = data_dir_.AppendASCII("bin.old");
base::FilePath patch = data_dir_.AppendASCII("negative_seek.diff");
base::FilePath dest = test_dir_.GetPath().AppendASCII("bin.new");
installer::ArchivePatchHelper archive_helper(
test_dir_.GetPath(), base::FilePath(), src, dest,
installer::UnPackConsumer::SETUP_EXE_PATCH);
archive_helper.set_last_uncompressed_file(patch);
// Should fail, but not crash.
EXPECT_FALSE(archive_helper.BinaryPatch());
}
+56
View File
@@ -0,0 +1,56 @@
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/installer/setup/brave_setup_util.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/version.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"
namespace installer {
// This function implementation used to be upstream and had to be restored in
// Brave to support delta updates on Windows until we are on Omaha 4. See:
// github.com/brave/brave-core/pull/31937
base::FilePath FindArchiveToPatch(const InstallationState& original_state,
const InstallerState& installer_state,
const base::Version& desired_version) {
if (desired_version.IsValid()) {
base::FilePath archive(
installer_state.GetInstallerDirectory(desired_version)
.Append(kChromeArchive));
return base::PathExists(archive) ? archive : base::FilePath();
}
// Check based on the version number advertised to Google Update, since that
// is the value used to select a specific differential update. If an archive
// can't be found using that, fallback to using the newest version present.
base::FilePath patch_source;
const ProductState* product =
original_state.GetProductState(installer_state.system_install());
if (product) {
patch_source = installer_state.GetInstallerDirectory(product->version())
.Append(installer::kChromeArchive);
if (base::PathExists(patch_source)) {
return patch_source;
}
}
std::unique_ptr<base::Version> version(
installer::GetMaxVersionFromArchiveDir(installer_state.target_path()));
if (version) {
patch_source = installer_state.GetInstallerDirectory(*version).Append(
installer::kChromeArchive);
if (base::PathExists(patch_source)) {
return patch_source;
}
}
return base::FilePath();
}
} // namespace installer
+32
View File
@@ -0,0 +1,32 @@
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_INSTALLER_SETUP_BRAVE_SETUP_UTIL_H_
#define BRAVE_INSTALLER_SETUP_BRAVE_SETUP_UTIL_H_
namespace base {
class FilePath;
class Version;
} // namespace base
namespace installer {
class InstallationState;
class InstallerState;
// Returns the uncompressed archive of the installed version that serves as the
// source for patching. If |desired_version| is valid, only the path to that
// version will be returned, or empty if it doesn't exist.
//
// This function used to be upstream and had to be restored in Brave to support
// delta updates on Windows until we are on Omaha 4. See:
// github.com/brave/brave-core/pull/31937
base::FilePath FindArchiveToPatch(const InstallationState& original_state,
const InstallerState& installer_state,
const base::Version& desired_version);
} // namespace installer
#endif // BRAVE_INSTALLER_SETUP_BRAVE_SETUP_UTIL_H_
@@ -0,0 +1,181 @@
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/installer/setup/brave_setup_util.h"
#include <memory>
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/test_reg_util_win.h"
#include "base/version.h"
#include "chrome/installer/setup/installer_state.h"
#include "chrome/installer/setup/setup_constants.h"
#include "chrome/installer/util/installation_state.h"
#include "chrome/installer/util/util_constants.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
// A test fixture that configures an InstallationState and an InstallerState
// with a product being updated.
//
// FindArchiveToPatchTest and its test cases used to be upstream and had to be
// restored in Brave to support delta updates on Windows until we are on
// Omaha 4. See github.com/brave/brave-core/pull/31937.
class FindArchiveToPatchTest : public testing::Test {
public:
FindArchiveToPatchTest(const FindArchiveToPatchTest&) = delete;
FindArchiveToPatchTest& operator=(const FindArchiveToPatchTest&) = delete;
protected:
class FakeInstallationState : public installer::InstallationState {};
class FakeProductState : public installer::ProductState {
public:
static FakeProductState* FromProductState(const ProductState* product) {
return static_cast<FakeProductState*>(const_cast<ProductState*>(product));
}
void set_version(const base::Version& version) {
if (version.IsValid()) {
version_ = std::make_unique<base::Version>(version);
} else {
version_.reset();
}
}
void set_uninstall_command(const base::CommandLine& uninstall_command) {
uninstall_command_ = uninstall_command;
}
};
FindArchiveToPatchTest() = default;
void SetUp() override {
ASSERT_TRUE(test_dir_.CreateUniqueTempDir());
ASSERT_NO_FATAL_FAILURE(
registry_override_manager_.OverrideRegistry(HKEY_CURRENT_USER));
ASSERT_NO_FATAL_FAILURE(
registry_override_manager_.OverrideRegistry(HKEY_LOCAL_MACHINE));
product_version_ = base::Version("30.0.1559.0");
max_version_ = base::Version("47.0.1559.0");
// Install the product according to the version.
original_state_ = std::make_unique<FakeInstallationState>();
InstallProduct();
// Prepare to update the product in the temp dir.
installer_state_ = std::make_unique<installer::InstallerState>(
installer::InstallerState::USER_LEVEL);
installer_state_->set_target_path_for_testing(test_dir_.GetPath());
// Create archives in the two version dirs.
ASSERT_TRUE(
base::CreateDirectory(GetProductVersionArchivePath().DirName()));
ASSERT_TRUE(base::WriteFile(GetProductVersionArchivePath(), "a"));
ASSERT_TRUE(base::CreateDirectory(GetMaxVersionArchivePath().DirName()));
ASSERT_TRUE(base::WriteFile(GetMaxVersionArchivePath(), "b"));
}
void TearDown() override { original_state_.reset(); }
base::FilePath GetArchivePath(const base::Version& version) const {
return test_dir_.GetPath()
.AppendASCII(version.GetString())
.Append(installer::kInstallerDir)
.Append(installer::kChromeArchive);
}
base::FilePath GetMaxVersionArchivePath() const {
return GetArchivePath(max_version_);
}
base::FilePath GetProductVersionArchivePath() const {
return GetArchivePath(product_version_);
}
void InstallProduct() {
FakeProductState* product = FakeProductState::FromProductState(
original_state_->GetNonVersionedProductState(false));
product->set_version(product_version_);
base::CommandLine uninstall_command(
test_dir_.GetPath()
.AppendASCII(product_version_.GetString())
.Append(installer::kInstallerDir)
.Append(installer::kSetupExe));
uninstall_command.AppendSwitch(installer::switches::kUninstall);
product->set_uninstall_command(uninstall_command);
}
void UninstallProduct() {
FakeProductState::FromProductState(
original_state_->GetNonVersionedProductState(false))
->set_version(base::Version());
}
base::ScopedTempDir test_dir_;
base::Version product_version_;
base::Version max_version_;
std::unique_ptr<FakeInstallationState> original_state_;
std::unique_ptr<installer::InstallerState> installer_state_;
private:
registry_util::RegistryOverrideManager registry_override_manager_;
};
} // namespace
// Test that the path to the advertised product version is found.
TEST_F(FindArchiveToPatchTest, ProductVersionFound) {
base::FilePath patch_source(installer::FindArchiveToPatch(
*original_state_, *installer_state_, base::Version()));
EXPECT_EQ(GetProductVersionArchivePath().value(), patch_source.value());
}
// Test that the path to the max version is found if the advertised version is
// missing.
TEST_F(FindArchiveToPatchTest, MaxVersionFound) {
// The patch file is absent.
ASSERT_TRUE(base::DeleteFile(GetProductVersionArchivePath()));
base::FilePath patch_source(installer::FindArchiveToPatch(
*original_state_, *installer_state_, base::Version()));
EXPECT_EQ(GetMaxVersionArchivePath().value(), patch_source.value());
// The product doesn't appear to be installed, so the max version is found.
UninstallProduct();
patch_source = installer::FindArchiveToPatch(
*original_state_, *installer_state_, base::Version());
EXPECT_EQ(GetMaxVersionArchivePath().value(), patch_source.value());
}
// Test that an empty path is returned if no version is found.
TEST_F(FindArchiveToPatchTest, NoVersionFound) {
// The product doesn't appear to be installed and no archives are present.
UninstallProduct();
ASSERT_TRUE(base::DeleteFile(GetProductVersionArchivePath()));
ASSERT_TRUE(base::DeleteFile(GetMaxVersionArchivePath()));
base::FilePath patch_source(installer::FindArchiveToPatch(
*original_state_, *installer_state_, base::Version()));
EXPECT_EQ(base::FilePath::StringType(), patch_source.value());
}
TEST_F(FindArchiveToPatchTest, DesiredVersionFound) {
base::FilePath patch_source1(installer::FindArchiveToPatch(
*original_state_, *installer_state_, product_version_));
EXPECT_EQ(GetProductVersionArchivePath().value(), patch_source1.value());
base::FilePath patch_source2(installer::FindArchiveToPatch(
*original_state_, *installer_state_, max_version_));
EXPECT_EQ(GetMaxVersionArchivePath().value(), patch_source2.value());
}
TEST_F(FindArchiveToPatchTest, DesiredVersionNotFound) {
base::FilePath patch_source(installer::FindArchiveToPatch(
*original_state_, *installer_state_, base::Version("1.2.3.4")));
EXPECT_EQ(base::FilePath().value(), patch_source.value());
}
+20
View File
@@ -0,0 +1,20 @@
/* Copyright (c) 2025 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "base/functional/bind.h"
#include "base/test/launcher/unit_test_launcher.h"
#include "base/test/test_suite.h"
#include "chrome/common/chrome_paths.h"
int main(int argc, char* argv[]) {
base::TestSuite test_suite(argc, argv);
// For archive_patch_helper_unittest.cc.
chrome::RegisterPathProvider();
return base::LaunchUnitTests(
argc, argv,
base::BindOnce(&base::TestSuite::Run, base::Unretained(&test_suite)));
}