// 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/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/browser/load_error_reporter.h"
#include "extensions/buildflags/buildflags.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)
#include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

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();
    expected_extension_install_directory_ =
        registrar()->unpacked_install_directory();
  }

  // Install the .zip in the test directory with `zip_name` and `expect_error`
  // if it should fail.
  void RunInstaller(const std::string& zip_name,
                    bool expect_error,
                    base::FilePath unzip_dir_root = base::FilePath());

 protected:
  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(profile()));

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(&ZipFileInstaller::InstallZipFileToUnpackedExtensionsDir,
                     zipfile_installer_, original_zip_path,
                     unzip_dir_root.empty()
                         ? registrar()->unpacked_install_directory()
                         : unzip_dir_root));
  observer_.WaitForInstall(expect_error);
  task_environment()->RunUntilIdle();
}

// Tests that a normal .zip is installed into the expected install path.
TEST_F(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/40916554 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);
}

TEST_F(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_F(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/40916554 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_F(ZipFileInstallerLocationTest, CannotCreateContainingDirectoryZip) {
  // TODO(crbug.com/40875193): 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_F(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/40916554 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
