blob: 1e2b7e788a9f2b3e652057618160abb36bcd4999 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/chrome_zipfile_installer.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/extensions/load_error_reporter.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/base/testing_profile.h"
#include "components/services/unzip/content/unzip_service.h"
#include "components/services/unzip/in_process_unzipper.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_registry_observer.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
#endif
namespace extensions {
namespace {
struct MockExtensionRegistryObserver : public ExtensionRegistryObserver {
void WaitForInstall(bool expect_error) {
extensions::LoadErrorReporter* error_reporter =
extensions::LoadErrorReporter::GetInstance();
error_reporter->ClearErrors();
while (true) {
base::RunLoop run_loop;
// We do not get a notification if installation fails. Make sure to wake
// up and check for errors to get an error better than the test
// timing-out.
// TODO(jcivelli): make LoadErrorReporter::Observer report installation
// failures for packaged extensions so we don't have to poll.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), base::Milliseconds(100));
quit_closure = run_loop.QuitClosure();
run_loop.Run();
const std::vector<std::u16string>* errors = error_reporter->GetErrors();
if (!errors->empty()) {
if (!expect_error) {
FAIL() << "Error(s) happened when unzipping extension: "
<< (*errors)[0];
}
break;
}
if (!last_extension_installed.empty()) {
// Extension install succeeded.
EXPECT_FALSE(expect_error);
break;
}
}
}
void OnExtensionInstalled(content::BrowserContext* browser_context,
const Extension* extension,
bool is_update) override {
last_extension_installed = extension->id();
last_extension_installed_path = extension->path();
std::move(quit_closure).Run();
}
std::string last_extension_installed;
base::FilePath last_extension_installed_path;
base::OnceClosure quit_closure;
};
struct UnzipFileFilterTestCase {
const base::FilePath::CharType* input;
const bool should_unzip;
};
} // namespace
// Assists with testing the non-installation location behavior of the installer.
class ZipFileInstallerTest : public ExtensionServiceTestBase {
public:
void SetUp() override {
ExtensionServiceTestBase::SetUp();
InitializeEmptyExtensionService();
extensions::LoadErrorReporter::Init(/*enable_noisy_errors=*/false);
in_process_utility_thread_helper_ =
std::make_unique<content::InProcessUtilityThreadHelper>();
unzip::SetUnzipperLaunchOverrideForTesting(
base::BindRepeating(&unzip::LaunchInProcessUnzipper));
registry()->AddObserver(&observer_);
}
void TearDown() override {
registry()->RemoveObserver(&observer_);
ExtensionServiceTestBase::TearDown();
// Need to destruct ZipFileInstaller before the message loop since
// it posts a task to it.
zipfile_installer_.reset();
unzip::SetUnzipperLaunchOverrideForTesting(base::NullCallback());
base::RunLoop().RunUntilIdle();
}
protected:
scoped_refptr<ZipFileInstaller> zipfile_installer_;
std::unique_ptr<content::InProcessUtilityThreadHelper>
in_process_utility_thread_helper_;
MockExtensionRegistryObserver observer_;
private:
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
};
// Assists with testing the zip file filtering behavior of ZipFileInstaller.
class ZipFileInstallerFilterTest : public ZipFileInstallerTest {
protected:
void RunZipFileFilterTest(
const std::vector<UnzipFileFilterTestCase>& cases,
base::RepeatingCallback<bool(const base::FilePath&)>& filter) {
for (size_t i = 0; i < cases.size(); ++i) {
base::FilePath input(cases[i].input);
bool observed = filter.Run(input);
EXPECT_EQ(cases[i].should_unzip, observed)
<< "i: " << i << ", input: " << input.value();
}
}
};
TEST_F(ZipFileInstallerFilterTest, NonTheme_FileExtractionFilter) {
const std::vector<UnzipFileFilterTestCase> cases = {
{FILE_PATH_LITERAL("foo"), true},
{FILE_PATH_LITERAL("foo.nexe"), true},
{FILE_PATH_LITERAL("foo.dll"), true},
{FILE_PATH_LITERAL("foo.jpg.exe"), false},
{FILE_PATH_LITERAL("foo.exe"), false},
{FILE_PATH_LITERAL("foo.EXE"), false},
{FILE_PATH_LITERAL("file_without_extension"), true},
};
base::RepeatingCallback<bool(const base::FilePath&)> filter =
base::BindRepeating(&ZipFileInstaller::ShouldExtractFile, false);
RunZipFileFilterTest(cases, filter);
}
TEST_F(ZipFileInstallerFilterTest, Theme_FileExtractionFilter) {
const std::vector<UnzipFileFilterTestCase> cases = {
{FILE_PATH_LITERAL("image.jpg"), true},
{FILE_PATH_LITERAL("IMAGE.JPEG"), true},
{FILE_PATH_LITERAL("test/image.bmp"), true},
{FILE_PATH_LITERAL("test/IMAGE.gif"), true},
{FILE_PATH_LITERAL("test/image.WEBP"), true},
{FILE_PATH_LITERAL("test/dir/file.image.png"), true},
{FILE_PATH_LITERAL("manifest.json"), true},
{FILE_PATH_LITERAL("other.html"), false},
{FILE_PATH_LITERAL("file_without_extension"), true},
};
base::RepeatingCallback<bool(const base::FilePath&)> filter =
base::BindRepeating(&ZipFileInstaller::ShouldExtractFile, true);
RunZipFileFilterTest(cases, filter);
}
TEST_F(ZipFileInstallerFilterTest, ManifestExtractionFilter) {
const std::vector<UnzipFileFilterTestCase> cases = {
{FILE_PATH_LITERAL("manifest.json"), true},
{FILE_PATH_LITERAL("MANIFEST.JSON"), true},
{FILE_PATH_LITERAL("test/manifest.json"), false},
{FILE_PATH_LITERAL("manifest.json/test"), false},
{FILE_PATH_LITERAL("other.file"), false},
};
base::RepeatingCallback<bool(const base::FilePath&)> filter =
base::BindRepeating(&ZipFileInstaller::IsManifestFile);
RunZipFileFilterTest(cases, filter);
}
class ZipFileInstallerLocationTest : public ZipFileInstallerTest,
public testing::WithParamInterface<bool> {
public:
void SetUp() override {
ZipFileInstallerTest::SetUp();
const bool kFeatureEnabled = GetParam();
feature_list_.InitWithFeatureState(
extensions_features::kExtensionsZipFileInstalledInProfileDir,
kFeatureEnabled);
if (kFeatureEnabled) {
expected_extension_install_directory_ =
service()->unpacked_install_directory();
} else {
base::FilePath dir_temp;
ASSERT_TRUE(base::PathService::Get(base::DIR_TEMP, &dir_temp));
expected_extension_install_directory_ = dir_temp;
}
}
// Install the .zip in the test directory with `zip_name` and `expect_error`
// if it should fail. The method installs the .zip differently based on
// whether `extensions_features::kExtensionsZipFileInstalledInProfileDir` is
// enabled. `unzip_dir_root` allows passing a custom installation path when
// that feature is enabled.
void RunInstaller(const std::string& zip_name,
bool expect_error,
base::FilePath unzip_dir_root = base::FilePath());
protected:
base::test::ScopedFeatureList scoped_feature_list_;
base::FilePath expected_extension_install_directory_;
};
void ZipFileInstallerLocationTest::RunInstaller(const std::string& zip_name,
bool expect_error,
base::FilePath unzip_dir_root) {
base::FilePath original_zip_path;
ASSERT_TRUE(
base::PathService::Get(chrome::DIR_TEST_DATA, &original_zip_path));
original_zip_path = original_zip_path.AppendASCII("extensions")
.AppendASCII("zipfile_installer")
.AppendASCII(zip_name);
ASSERT_TRUE(base::PathExists(original_zip_path)) << original_zip_path.value();
zipfile_installer_ = ZipFileInstaller::Create(
GetExtensionFileTaskRunner(),
MakeRegisterInExtensionServiceCallback(service()));
if (GetParam()) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&ZipFileInstaller::InstallZipFileToUnpackedExtensionsDir,
zipfile_installer_, original_zip_path,
unzip_dir_root.empty()
? service()->unpacked_install_directory()
: unzip_dir_root));
} else {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&ZipFileInstaller::InstallZipFileToTempDir,
zipfile_installer_, original_zip_path));
}
observer_.WaitForInstall(expect_error);
task_environment()->RunUntilIdle();
}
INSTANTIATE_TEST_SUITE_P(
,
ZipFileInstallerLocationTest,
// extensions_features::kExtensionsZipFileInstalledInProfileDir enabled.
testing::Bool(),
[](const testing::TestParamInfo<ZipFileInstallerLocationTest::ParamType>&
info) { return info.param ? "ProfileDir" : "TempDir"; });
// Tests that a normal .zip is installed into the expected install path.
TEST_P(ZipFileInstallerLocationTest, GoodZip) {
RunInstaller(/*zip_name=*/"good.zip",
/*expect_error=*/false);
// Expect extension install directory to be immediate subdir of expected
// temp install directory. E.g. /a/b/c/d == /a/b/c + /d.
//
// Make sure we're comparing absolute paths to avoid failures like
// https://crbug.com/1453669 on macOS 14.
base::FilePath absolute_last_extension_installed_path =
base::MakeAbsoluteFilePath(observer_.last_extension_installed_path);
base::FilePath absolute_expected_extension_install_directory =
base::MakeAbsoluteFilePath(expected_extension_install_directory_.Append(
observer_.last_extension_installed_path.BaseName()));
EXPECT_EQ(absolute_last_extension_installed_path,
absolute_expected_extension_install_directory);
}
/*
base::FilePath absolute_extension_path =
base::MakeAbsoluteFilePath(extension->path());
base::FilePath absolute_expected_extension_install_directory =
base::MakeAbsoluteFilePath(expected_extension_install_directory_.Append(
extension->path().BaseName()));
*/
TEST_P(ZipFileInstallerLocationTest, BadZip) {
// Manifestless archive.
RunInstaller(/*zip_name=*/"bad.zip",
/*expect_error=*/true);
}
// Tests installing the same .zip twice results in two separate install
// directories.
TEST_P(ZipFileInstallerLocationTest, MultipleSameZipInstallSeparately) {
RunInstaller(/*zip_name=*/"good.zip",
/*expect_error=*/false);
base::FilePath dir_temp;
base::PathService::Get(base::DIR_TEMP, &dir_temp);
base::FilePath first_install_path = observer_.last_extension_installed_path;
// Expect extension install directory to be immediate subdir of expected
// unpacked install directory. E.g. /a/b/c/d == /a/b/c + /d.
//
// Make sure we're comparing absolute paths to avoid failures like
// https://crbug.com/1453669 on macOS 14.
base::FilePath absolute_last_extension_installed_path =
base::MakeAbsoluteFilePath(observer_.last_extension_installed_path);
base::FilePath absolute_expected_extension_install_directory =
base::MakeAbsoluteFilePath(expected_extension_install_directory_.Append(
observer_.last_extension_installed_path.BaseName()));
EXPECT_EQ(absolute_last_extension_installed_path,
absolute_expected_extension_install_directory);
RunInstaller(/*zip_name=*/"good.zip",
/*expect_error=*/false);
base::FilePath second_install_path = observer_.last_extension_installed_path;
// Expect extension install directory to be immediate subdir of expected
// unpacked install directory. E.g. /a/b/c/d == /a/b/c + /d.
absolute_last_extension_installed_path =
base::MakeAbsoluteFilePath(observer_.last_extension_installed_path);
absolute_expected_extension_install_directory =
base::MakeAbsoluteFilePath(expected_extension_install_directory_.Append(
observer_.last_extension_installed_path.BaseName()));
EXPECT_EQ(absolute_last_extension_installed_path,
absolute_expected_extension_install_directory);
// Confirm that the two extensions are installed in two separate
// directories.
EXPECT_NE(first_install_path, second_install_path);
}
// Tests that we error when we cannot create the parent directory of where to
// install the .zips to.
TEST_P(ZipFileInstallerLocationTest, CannotCreateContainingDirectoryZip) {
// This test is only relevant to the new feature.
if (!GetParam()) {
return;
}
// TODO(crbug.com/1378775): Have this expect a specific error rather than just
// an error since other things can cause an error.
RunInstaller(
/*zip_name=*/"good.zip", /*expect_error=*/true, /*unzip_dir_root=*/
#if !BUILDFLAG(IS_WIN)
base::FilePath(
FILE_PATH_LITERAL("/NonExistentDirectory/UnpackedExtensions"))
#else
// Windows will create unexpected paths so we use explicitly disallowed
// characters in the Windows filesystem to ensure creating this directory
// fails.
base::FilePath(
FILE_PATH_LITERAL("|<IllegalWinDirName>|/UnpackedExtensions"))
#endif // !BUILDFLAG(IS_WIN)
);
}
// Tests that a .zip with a public key installs with the expected extension ID
// and to the correct path.
TEST_P(ZipFileInstallerLocationTest, ZipWithPublicKey) {
RunInstaller(/*zip_name=*/"public_key.zip",
/*expect_error=*/false);
const char kIdForPublicKey[] = "ikppjpenhoddphklkpdfdfdabbakkpal";
EXPECT_EQ(observer_.last_extension_installed, kIdForPublicKey);
// Make sure we compare absolute paths to avoid failures like
// https://crbug.com/1453669 on macOS 14.
base::FilePath absolute_last_extension_installed_path =
base::MakeAbsoluteFilePath(observer_.last_extension_installed_path);
base::FilePath absolute_expected_extension_install_directory =
base::MakeAbsoluteFilePath(expected_extension_install_directory_.Append(
observer_.last_extension_installed_path.BaseName()));
EXPECT_EQ(absolute_last_extension_installed_path,
absolute_expected_extension_install_directory);
}
} // namespace extensions