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

#include "v8/include/v8.h"

#include <chrono>
#include <functional>
#include <iostream>
#include <mutex>
#include <thread>
#include <tuple>

#include "base/check.h"
#include "base/compiler_specific.h"
#include "base/files/file_path.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "build/build_config.h"
#include "testing/libfuzzer/libfuzzer_exports.h"
#include "v8/include/libplatform/libplatform.h"

using v8::MaybeLocal;
using std::ref;
using std::lock_guard;
using std::mutex;
using std::chrono::time_point;
using std::chrono::steady_clock;
using std::chrono::seconds;
using std::chrono::duration_cast;

static const seconds kSleepSeconds(1);

// Because of the sleep we do, the actual max will be:
// kSleepSeconds + kMaxExecutionSeconds.
// TODO(metzman): Determine if having such a short timeout causes too much
// indeterminism.
static const seconds kMaxExecutionSeconds(7);

// Inspired by/copied from d8 code, this allocator will return nullptr when
// an allocation request is made that puts currently_allocated_ over
// kAllocationLimit (1 GB). Should handle the current allocations done by V8.
class MockArrayBufferAllocator : public v8::ArrayBuffer::Allocator {
  std::unique_ptr<Allocator> allocator_ =
      std::unique_ptr<Allocator>(NewDefaultAllocator());

  const size_t kAllocationLimit = 1000 * 1024 * 1024;
  // TODO(metzman): Determine if this approach where we keep track of state
  // between runs is a good idea. Maybe we should simply prevent allocations
  // over a certain size regardless of previous allocations.
  size_t currently_allocated_;
  mutex mtx_;

 public:
  MockArrayBufferAllocator()
      : v8::ArrayBuffer::Allocator(), currently_allocated_(0) {}

  void* Allocate(size_t length) override {
    lock_guard<mutex> mtx_locker(mtx_);
    if (length + currently_allocated_ > kAllocationLimit) {
      return nullptr;
    }
    currently_allocated_ += length;
    return allocator_->Allocate(length);
  }

  void* AllocateUninitialized(size_t length) override {
    lock_guard<mutex> mtx_locker(mtx_);
    if (length + currently_allocated_ > kAllocationLimit) {
      return nullptr;
    }
    currently_allocated_ += length;
    return allocator_->AllocateUninitialized(length);
  }

  void Free(void* ptr, size_t length) override {
    lock_guard<mutex> mtx_locker(mtx_);
    currently_allocated_ -= length;
    return allocator_->Free(ptr, length);
  }
};

void terminate_execution(v8::Isolate* isolate,
                         mutex& mtx,
                         bool& is_running,
                         time_point<steady_clock>& start_time) {
  while (true) {
    std::this_thread::sleep_for(kSleepSeconds);
    lock_guard<mutex> mtx_locker(mtx);
    if (is_running) {
      if (duration_cast<seconds>(steady_clock::now() - start_time) >
          kMaxExecutionSeconds) {
        isolate->TerminateExecution();
        is_running = false;
        std::cout << "Terminated" << std::endl;
        fflush(0);
      }
    }
  }
}

struct Environment {
  Environment() {
    platform_ = v8::platform::NewDefaultPlatform(
        0, v8::platform::IdleTaskSupport::kDisabled,
        v8::platform::InProcessStackDumping::kDisabled, nullptr);

    v8::V8::InitializePlatform(platform_.get());
    v8::V8::Initialize();
    v8::Isolate::CreateParams create_params;

    mock_arraybuffer_allocator = std::make_unique<MockArrayBufferAllocator>();

    create_params.array_buffer_allocator = mock_arraybuffer_allocator.get();
    isolate = v8::Isolate::New(create_params);
    terminator_thread = std::thread(terminate_execution, isolate, ref(mtx),
                                    ref(is_running), ref(start_time));
  }
  std::unique_ptr<MockArrayBufferAllocator> mock_arraybuffer_allocator;
  mutex mtx;
  std::thread terminator_thread;
  raw_ptr<v8::Isolate> isolate;
  std::unique_ptr<v8::Platform> platform_;
  time_point<steady_clock> start_time;
  bool is_running = true;
};

// Explicitly specify some attributes to avoid issues with the linker dead-
// stripping the following function on macOS, as it is not called directly
// by fuzz target. LibFuzzer runtime uses dlsym() to resolve that function.
extern "C" __attribute__((used)) __attribute__((visibility("default"))) int
LLVMFuzzerInitialize(int* argc, char*** argv) {
// TODO(crbug.com/515765355): Consider centralizing this fallback logic in V8
// code.
#if BUILDFLAG(IS_ANDROID)
  base::FilePath assets_dir;
  CHECK(base::PathService::Get(base::DIR_ASSETS, &assets_dir));
  v8::V8::InitializeICUDefaultLocation(assets_dir.value().c_str());
  base::FilePath snapshot_path = assets_dir.Append("snapshot_blob.bin");
  v8::V8::InitializeExternalStartupDataFromFile(snapshot_path.value().c_str());
#else
  v8::V8::InitializeICUDefaultLocation((*argv)[0]);
  v8::V8::InitializeExternalStartupData((*argv)[0]);
#endif  // BUILDFLAG(IS_ANDROID)
  v8::V8::SetFlagsFromCommandLine(argc, *argv, true);
  return 0;
}

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
  static Environment* env = new Environment();

  if (size < 1)
    return 0;

  v8::Isolate::Scope isolate_scope(env->isolate);
  v8::HandleScope handle_scope(env->isolate);
  v8::Local<v8::Context> context = v8::Context::New(env->isolate);
  v8::Context::Scope context_scope(context);

  std::string source_string =
      std::string(reinterpret_cast<const char*>(data), size);

  MaybeLocal<v8::String> source_v8_string = v8::String::NewFromUtf8(
      env->isolate, source_string.c_str(), v8::NewStringType::kNormal);

  if (source_v8_string.IsEmpty())
    return 0;

  v8::TryCatch try_catch(env->isolate);
  MaybeLocal<v8::Script> script =
      v8::Script::Compile(context, source_v8_string.ToLocalChecked());

  if (script.IsEmpty())
    return 0;

  auto local_script = script.ToLocalChecked();
  env->mtx.lock();
  env->start_time = steady_clock::now();
  env->mtx.unlock();

  std::ignore = local_script->Run(context);

  lock_guard<mutex> mtx_locker(env->mtx);
  env->is_running = false;
  return 0;
}
