// Copyright 2018 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/image_sanitizer.h"

#include <optional>
#include <tuple>

#include "base/debug/dump_without_crashing.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/task/sequenced_task_runner.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/common/extension_resource_path_normalizer.h"
#include "services/data_decoder/public/cpp/decode_image.h"
#include "ui/gfx/codec/png_codec.h"

namespace extensions {

namespace {

// We don't expect icons and other extension's images to be big.
// We use this limit to prevent from opening too large images.
const int kMaxImageCanvas = 4096 * 4096;  // 16MB

// Reads the file in |path| and then deletes it.
// Returns a tuple containing: the file content, whether the read was
// successful, whether the delete was successful.
std::tuple<std::vector<uint8_t>, bool, bool> ReadAndDeleteBinaryFile(
    const base::FilePath& path) {
  std::vector<uint8_t> contents;
  bool read_success = false;
  std::optional<int64_t> file_size = base::GetFileSize(path);
  if (file_size.has_value()) {
    int64_t size = file_size.value();
    contents.resize(size);
    read_success =
        base::ReadFile(path, reinterpret_cast<char*>(contents.data()), size) ==
        size;
  }
  bool delete_success = base::DeleteFile(path);
  return std::make_tuple(std::move(contents), read_success, delete_success);
}

bool WriteFile(const base::FilePath& path,
               const std::vector<unsigned char>& data) {
  return base::WriteFile(path, data);
}

}  // namespace

// static
std::unique_ptr<ImageSanitizer> ImageSanitizer::CreateAndStart(
    scoped_refptr<Client> client,
    const base::FilePath& image_dir,
    const std::set<base::FilePath>& image_paths,
    const scoped_refptr<base::SequencedTaskRunner>& io_task_runner) {
  std::unique_ptr<ImageSanitizer> sanitizer(
      new ImageSanitizer(client, image_dir, image_paths, io_task_runner));
  sanitizer->Start();
  return sanitizer;
}

ImageSanitizer::ImageSanitizer(
    scoped_refptr<Client> client,
    const base::FilePath& image_dir,
    const std::set<base::FilePath>& image_relative_paths,
    const scoped_refptr<base::SequencedTaskRunner>& io_task_runner)
    : image_dir_(image_dir),
      image_paths_(image_relative_paths),
      client_(std::move(client)),
      io_task_runner_(io_task_runner) {
  DCHECK(client_);
}

ImageSanitizer::~ImageSanitizer() = default;
ImageSanitizer::Client::~Client() = default;

void ImageSanitizer::Start() {
  if (image_paths_.empty()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&ImageSanitizer::ReportSuccess,
                                  weak_factory_.GetWeakPtr()));
    return;
  }

  std::set<base::FilePath> normalized_image_paths;
  for (const base::FilePath& path : image_paths_) {
    // Normalize paths as |image_paths_| can contain duplicates like "icon.png"
    // and "./icon.png" to avoid unpacking the same image twice.
    base::FilePath normalized_path;
    if (path.IsAbsolute() || path.ReferencesParent() ||
        !NormalizeExtensionResourcePath(path, &normalized_path)) {
      // Report the error asynchronously so the caller stack has chance to
      // unwind.
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&ImageSanitizer::ReportError,
                                    weak_factory_.GetWeakPtr(),
                                    Status::kImagePathError, path));
      return;
    }
    normalized_image_paths.insert(normalized_path);
  }
  // Update |image_paths_| as some of the path might have been changed by
  // normalization.
  image_paths_ = std::move(normalized_image_paths);

  // Note that we use 2 for loops instead of one to prevent a race and flakyness
  // in tests: if |image_paths_| contains 2 paths, a valid one that points to a
  // file that does not exist and an invalid one, there is a race that can cause
  // either error to be reported (kImagePathError or kFileReadError).
  for (const base::FilePath& path : image_paths_) {
    base::FilePath full_image_path = image_dir_.Append(path);
    io_task_runner_->PostTaskAndReplyWithResult(
        FROM_HERE, base::BindOnce(&ReadAndDeleteBinaryFile, full_image_path),
        base::BindOnce(&ImageSanitizer::ImageFileRead,
                       weak_factory_.GetWeakPtr(), path));
  }
}

void ImageSanitizer::ImageFileRead(const base::FilePath& image_path,
                                   std::vector<uint8_t> contents,
                                   bool read_successful,
                                   bool delete_successful) {
  if (!read_successful) {
    ReportError(Status::kFileReadError, image_path);
    return;
  }
  if (!delete_successful) {
    ReportError(Status::kFileDeleteError, image_path);
    return;
  }
  data_decoder::DecodeImage(
      client_->GetDataDecoder(), contents,
      data_decoder::mojom::ImageCodec::kDefault,
      /*shrink_to_fit=*/false, kMaxImageCanvas, gfx::Size(),
      base::BindOnce(&ImageSanitizer::ImageDecoded, weak_factory_.GetWeakPtr(),
                     image_path));
}

void ImageSanitizer::ImageDecoded(const base::FilePath& image_path,
                                  const SkBitmap& decoded_image) {
  if (decoded_image.isNull()) {
    ReportError(Status::kDecodingError, image_path);
    return;
  }
  if (decoded_image.colorType() != kN32_SkColorType) {
    // The renderer should be sending us N32 32bpp bitmaps in reply, otherwise
    // this can lead to out-of-bounds mistakes when transferring the pixels out
    // of the bitmap into other buffers.
    base::debug::DumpWithoutCrashing();
    ReportError(Status::kDecodingError, image_path);
    return;
  }

  io_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(gfx::PNGCodec::EncodeBGRASkBitmap, decoded_image,
                     /*discard_transparency=*/false),
      base::BindOnce(&ImageSanitizer::ImageReencoded,
                     weak_factory_.GetWeakPtr(), image_path));

  client_->OnImageDecoded(image_path, decoded_image);
  // Note that the `client` callback could potentially delete `this` object.
}

void ImageSanitizer::ImageReencoded(
    const base::FilePath& image_path,
    std::optional<std::vector<uint8_t>> result) {
  bool success = result.has_value();
  if (!success) {
    ReportError(Status::kEncodingError, image_path);
    return;
  }

  io_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&WriteFile, image_dir_.Append(image_path),
                     std::move(result.value())),
      base::BindOnce(&ImageSanitizer::ImageWritten, weak_factory_.GetWeakPtr(),
                     image_path));
}

void ImageSanitizer::ImageWritten(const base::FilePath& image_path,
                                  bool success) {
  if (!success) {
    ReportError(Status::kFileWriteError, image_path);
    return;
  }
  // We have finished with this path.
  size_t removed_count = image_paths_.erase(image_path);
  DCHECK_EQ(1U, removed_count);

  if (image_paths_.empty()) {
    // This was the last path, we are done.
    ReportSuccess();
  }
}

void ImageSanitizer::ReportSuccess() {
  // Reset `client_` early, before the callback potentially deletes `this`.
  scoped_refptr<Client> client = std::move(client_);
  DCHECK(!client_);

  // The `client_` callback is the last statement, because it can potentially
  // delete `this` object.
  client->OnImageSanitizationDone(Status::kSuccess, base::FilePath());
}

void ImageSanitizer::ReportError(Status status, const base::FilePath& path) {
  // Prevent any other task from reporting, we want to notify only once.
  weak_factory_.InvalidateWeakPtrs();

  // Reset `client_` early, before the callback potentially deletes `this`.
  scoped_refptr<Client> client = std::move(client_);
  DCHECK(!client_);

  // The `client_` callback is the last statement, because it can potentially
  // delete `this` object.
  client->OnImageSanitizationDone(status, path);
}

}  // namespace extensions
