// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "extensions/browser/extension_creator.h"

#include <stddef.h>

#include <optional>
#include <string>
#include <vector>

#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/strings/string_util.h"
#include "components/crx_file/crx_creator.h"
#include "components/crx_file/id_util.h"
#include "crypto/keypair.h"
#include "extensions/browser/extension_creator_filter.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_l10n_util.h"
#include "extensions/common/file_util.h"
#include "extensions/strings/grit/extensions_strings.h"
#include "third_party/zlib/google/zip.h"
#include "ui/base/l10n/l10n_util.h"

namespace extensions {

ExtensionCreator::ExtensionCreator() : error_type_(kOtherError) {}

bool FileConflicts(const base::FilePath& file_path) {
#if BUILDFLAG(IS_ANDROID)
  // In Android, the GetOrCreateEmptyFilesUnderDownloads method returns either
  // an existing file or a newly created empty file's content URI. Apply a size
  // check here to determine if the file is a pre-existing, non-empty one.
  if (file_path.IsContentUri()) {
    std::optional<int64_t> file_size = base::GetFileSize(file_path);
    return file_size.has_value() && file_size.value() > 0;
  }
#endif  // BUILDFLAG(IS_ANDROID)
  return base::PathExists(file_path);
}

bool ExtensionCreator::InitializeInput(
    const base::FilePath& extension_dir,
    const base::FilePath& crx_path,
    const base::FilePath& private_key_path,
    const base::FilePath& private_key_output_path,
    int run_flags) {
#if BUILDFLAG(IS_ANDROID)
  // The path must be either normal path or a virtual document path to allow
  // Append.
  CHECK(!extension_dir.IsContentUri());
#endif  // BUILDFLAG(IS_ANDROID)

  // Validate input |extension_dir|.
  if (extension_dir.value().empty() || !base::DirectoryExists(extension_dir)) {
    error_message_ =
        l10n_util::GetStringUTF16(IDS_EXTENSION_DIRECTORY_NO_EXISTS);
    return false;
  }

  base::FilePath absolute_extension_dir =
      base::MakeAbsoluteFilePath(extension_dir);
  if (absolute_extension_dir.empty()) {
    error_message_ =
        l10n_util::GetStringUTF16(IDS_EXTENSION_CANT_GET_ABSOLUTE_PATH);
    return false;
  }

  // Validate input |private_key| (if provided).
  if (!private_key_path.value().empty() &&
      !base::PathExists(private_key_path)) {
    error_message_ =
        l10n_util::GetStringUTF16(IDS_EXTENSION_PRIVATE_KEY_INVALID_PATH);
    return false;
  }

  // If an |output_private_key| path is given but no key specified in this pack
  // request, make sure it doesn't over-write an existing private key not used
  // for current extension.
  if (private_key_path.value().empty() &&
      !private_key_output_path.value().empty() &&
      FileConflicts(private_key_output_path)) {
    error_message_ =
        l10n_util::GetStringUTF16(IDS_EXTENSION_PRIVATE_KEY_EXISTS);
    return false;
  }

  // Check whether crx file already exists. Should be last check, as this is
  // a warning only.
  if (!(run_flags & kOverwriteCRX) && FileConflicts(crx_path)) {
    error_message_ = l10n_util::GetStringUTF16(IDS_EXTENSION_CRX_EXISTS);
    error_type_ = kCRXExists;
    return false;
  }

  return true;
}

bool ExtensionCreator::ValidateExtension(const base::FilePath& extension_dir,
                                         int run_flags) {
  int create_flags =
      Extension::FOLLOW_SYMLINKS_ANYWHERE | Extension::ERROR_ON_PRIVATE_KEY;
  if (run_flags & kRequireModernManifestVersion) {
    create_flags |= Extension::REQUIRE_MODERN_MANIFEST_VERSION;
  }

  // Loading the extension does a lot of useful validation of the structure.
  scoped_refptr<Extension> extension(file_util::LoadExtension(
      extension_dir, mojom::ManifestLocation::kInternal, create_flags,
      &error_message_));

  return !!extension.get() &&
         extension_l10n_util::ValidateExtensionLocales(
             extension_dir, *extension.get()->manifest()->value(),
             &error_message_);
}

std::optional<crypto::keypair::PrivateKey> ExtensionCreator::ReadInputKey(
    const base::FilePath& private_key_path) {
  if (!base::PathExists(private_key_path)) {
    error_message_ =
        l10n_util::GetStringUTF16(IDS_EXTENSION_PRIVATE_KEY_NO_EXISTS);
    return std::nullopt;
  }

  std::string private_key_contents;
  if (!base::ReadFileToString(private_key_path, &private_key_contents)) {
    error_message_ =
        l10n_util::GetStringUTF16(IDS_EXTENSION_PRIVATE_KEY_FAILED_TO_READ);
    return std::nullopt;
  }

  std::string private_key_bytes;
  if (!Extension::ParsePEMKeyBytes(private_key_contents, &private_key_bytes)) {
    error_message_ =
        l10n_util::GetStringUTF16(IDS_EXTENSION_PRIVATE_KEY_INVALID);
    return std::nullopt;
  }

  auto private_key = crypto::keypair::PrivateKey::FromPrivateKeyInfo(
      base::as_byte_span(private_key_bytes));
  if (!private_key || !private_key->IsRsa()) {
    error_message_ =
        l10n_util::GetStringUTF16(IDS_EXTENSION_PRIVATE_KEY_INVALID_FORMAT);
    return std::nullopt;
  }

  return private_key;
}

std::optional<crypto::keypair::PrivateKey> ExtensionCreator::GenerateKey(
    const base::FilePath& output_private_key_path) {
  auto key_pair = crypto::keypair::PrivateKey::GenerateRsa2048();

  std::vector<uint8_t> private_key_vector = key_pair.ToPrivateKeyInfo();
  std::string private_key_bytes(
      reinterpret_cast<char*>(&private_key_vector.front()),
      private_key_vector.size());

  std::string private_key;
  if (!Extension::ProducePEM(private_key_bytes, &private_key)) {
    error_message_ =
        l10n_util::GetStringUTF16(IDS_EXTENSION_PRIVATE_KEY_FAILED_TO_OUTPUT);
    return std::nullopt;
  }
  std::string pem_output;
  if (!Extension::FormatPEMForFileOutput(private_key, &pem_output, false)) {
    error_message_ =
        l10n_util::GetStringUTF16(IDS_EXTENSION_PRIVATE_KEY_FAILED_TO_OUTPUT);
    return std::nullopt;
  }

  if (!output_private_key_path.empty()) {
    if (!base::WriteFile(output_private_key_path, pem_output)) {
      error_message_ =
          l10n_util::GetStringUTF16(IDS_EXTENSION_PRIVATE_KEY_FAILED_TO_OUTPUT);
      return std::nullopt;
    }
  }

  return key_pair;
}

bool ExtensionCreator::CreateZip(const base::FilePath& extension_dir,
                                 const base::FilePath& temp_path,
                                 base::FilePath* zip_path) {
  *zip_path = temp_path.Append(FILE_PATH_LITERAL("extension.zip"));

  scoped_refptr<ExtensionCreatorFilter> filter =
      base::MakeRefCounted<ExtensionCreatorFilter>(extension_dir);
  zip::FilterCallback filter_cb =
      base::BindRepeating(&ExtensionCreatorFilter::ShouldPackageFile, filter);

  // TODO(crbug.com/40584446): Surface a warning to the user for files excluded
  // from being packed.
  if (!zip::ZipWithFilterCallback(extension_dir, *zip_path,
                                  std::move(filter_cb))) {
    error_message_ =
        l10n_util::GetStringUTF16(IDS_EXTENSION_FAILED_DURING_PACKAGING);
    return false;
  }

  return true;
}

bool ExtensionCreator::CreateCrx(
    const base::FilePath& zip_path,
    const crypto::keypair::PrivateKey& private_key,
    const base::FilePath& crx_path,
    const std::optional<std::string>& compressed_verified_contents) {
  crx_file::CreatorResult result;
  if (compressed_verified_contents.has_value()) {
    result = crx_file::CreateCrxWithVerifiedContentsInHeader(
        crx_path, zip_path, private_key, compressed_verified_contents.value());
  } else {
    result = crx_file::Create(crx_path, zip_path, private_key);
  }
  switch (result) {
    case crx_file::CreatorResult::OK:
      return true;
    case crx_file::CreatorResult::ERROR_SIGNING_FAILURE:
      error_message_ =
          l10n_util::GetStringUTF16(IDS_EXTENSION_ERROR_WHILE_SIGNING);
      return false;
    case crx_file::CreatorResult::ERROR_FILE_NOT_WRITABLE:
      error_message_ =
          l10n_util::GetStringUTF16(IDS_EXTENSION_SHARING_VIOLATION);
      return false;
    case crx_file::CreatorResult::ERROR_FILE_NOT_READABLE:
    case crx_file::CreatorResult::ERROR_FILE_WRITE_FAILURE:
      return false;
  }
  return false;
}

bool ExtensionCreator::CreateCrxAndPerformCleanup(
    const base::FilePath& extension_dir,
    const base::FilePath& crx_path,
    const crypto::keypair::PrivateKey& private_key,
    const std::optional<std::string>& compressed_verified_contents) {
  base::ScopedTempDir temp_dir;
  if (!temp_dir.CreateUniqueTempDir()) {
    return false;
  }

  base::FilePath zip_path;
  bool result =
      CreateZip(extension_dir, temp_dir.GetPath(), &zip_path) &&
      CreateCrx(zip_path, private_key, crx_path, compressed_verified_contents);
  base::DeleteFile(zip_path);
  return result;
}

bool ExtensionCreator::Run(const base::FilePath& extension_dir,
                           const base::FilePath& crx_path,
                           const base::FilePath& private_key_path,
                           const base::FilePath& output_private_key_path,
                           int run_flags) {
  // Check input diretory and read manifest.
  if (!InitializeInput(extension_dir, crx_path, private_key_path,
                       output_private_key_path, run_flags)) {
    return false;
  }

  if (!ValidateExtension(extension_dir, run_flags)) {
    return false;
  }

  // Initialize Key Pair
  std::optional<crypto::keypair::PrivateKey> key_pair;
  if (!private_key_path.value().empty()) {
    key_pair = ReadInputKey(private_key_path);
  } else {
    key_pair = GenerateKey(output_private_key_path);
  }
  if (!key_pair) {
    DCHECK(!error_message_.empty()) << "Set proper error message.";
    return false;
  }

  return CreateCrxAndPerformCleanup(extension_dir, crx_path, *key_pair,
                                    std::nullopt);
}

}  // namespace extensions
