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

#include <stdint.h>

#include <utility>
#include <vector>

#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/no_destructor.h"
#include "base/task/bind_post_task.h"
#include "components/services/storage/privileged/cpp/bucket_client_info.h"
#include "components/services/storage/privileged/mojom/indexed_db_client_state_checker.mojom.h"
#include "components/services/storage/privileged/mojom/indexed_db_control.mojom.h"
#include "components/services/storage/privileged/mojom/indexed_db_control_test.mojom.h"
#include "components/services/storage/public/cpp/buckets/bucket_locator.h"
#include "content/browser/indexed_db/idb_factory_mojolpm_fuzzer.pb.h"
#include "content/browser/indexed_db/instance/bucket_context.h"
#include "content/public/browser/blob_handle.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/test_browser_context.h"
#include "content/test/fuzzer/mojolpm_fuzzer_support.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "storage/browser/quota/quota_manager.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom-mojolpm.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom.h"
#include "third_party/libprotobuf-mutator/src/src/libfuzzer/libfuzzer_macro.h"
#include "url/origin.h"

using url::Origin;

// This fuzzer exercises the mojo interfaces exposed by the browser process to
// implement the IndexedDB web API in the renderer process. These interfaces are
// defined in //third_party/blink/public/mojom/indexeddb/indexeddb.mojom. The
// IDBFactory interface is the entry point for all IndexedDB operations.

const char* const kCmdline[] = {"idb_factory_mojolpm_fuzzer", nullptr};

// Custom environment that uses mock time so the fuzzer can exercise
// time-dependent code paths (e.g. idle tasks, grace period close) that would
// otherwise never/rarely be executed within the per-action timeout.
class IdbFuzzerEnvironment : public content::mojolpm::FuzzerEnvironment {
 public:
  IdbFuzzerEnvironment(int argc, const char* const* argv)
      : FuzzerEnvironment(argc, argv),
        task_environment_(
            base::test::TaskEnvironment::TimeSource::MOCK_TIME,
            base::test::TaskEnvironment::MainThreadType::DEFAULT,
            base::test::TaskEnvironment::ThreadPoolExecutionMode::ASYNC,
            base::test::TaskEnvironment::ThreadingMode::MULTIPLE_THREADS,
            content::BrowserTaskEnvironment::REAL_IO_THREAD) {}

 private:
  content::BrowserTaskEnvironment task_environment_;
};

IdbFuzzerEnvironment& GetEnvironment() {
  static base::NoDestructor<IdbFuzzerEnvironment> environment(1, kCmdline);
  return *environment;
}

scoped_refptr<base::SequencedTaskRunner> GetFuzzerTaskRunner() {
  return GetEnvironment().fuzzer_task_runner();
}

// TODO(crbug.com/448235811): Hook up the actual state checker since it lives in
// the browser process.
class MockIndexedDBClientStateChecker
    : public storage::mojom::IndexedDBClientStateChecker {
 public:
  MockIndexedDBClientStateChecker() = default;
  ~MockIndexedDBClientStateChecker() override = default;

  // storage::mojom::IndexedDBClientStateChecker:
  void DisallowInactiveClient(
      int32_t connection_id,
      storage::mojom::DisallowInactiveClientReason reason,
      mojo::PendingReceiver<storage::mojom::IndexedDBClientKeepActive>
          keep_active,
      storage::mojom::IndexedDBClientStateChecker::
          DisallowInactiveClientCallback callback) override {}
  void MakeClone(
      mojo::PendingReceiver<storage::mojom::IndexedDBClientStateChecker>
          checker) override {}
};

// Per-testcase state needed to run the interface being tested.
//
// The lifetime of this is scoped to a single testcase, and it is created and
// destroyed from the fuzzer sequence.
//
// For IDBFactory, this needs the basic common Browser process state provided
// by TestBrowserContext, and the storage service.
//
// Since the Browser process will host one IDBFactory per bucket, we emulate
// this by allowing the fuzzer to create (and destroy) multiple IDBFactory
// instances.
class IdbFactoryTestcase
    : public mojolpm::Testcase<content::fuzzing::idb_factory::proto::Testcase,
                               content::fuzzing::idb_factory::proto::Action> {
 public:
  using ProtoTestcase = content::fuzzing::idb_factory::proto::Testcase;
  using ProtoAction = content::fuzzing::idb_factory::proto::Action;

  explicit IdbFactoryTestcase(const ProtoTestcase& testcase);

  void SetUp(base::OnceClosure done_closure) override;
  void TearDown(base::OnceClosure done_closure) override;

  void RunAction(const ProtoAction& action,
                 base::OnceClosure done_closure) override;

 private:
  void SetUpOnUIThread(base::OnceClosure done_closure);
  void SetUpOnFuzzerThread(base::OnceClosure done_closure);

  void TearDownOnUIThread(base::OnceClosure done_closure);
  void TearDownOnFuzzerThread(base::OnceClosure done_closure);

  // These are called from the UI thread.
  void FlushBucketSequence(base::OnceClosure done_closure);
  void BindIndexedDB(
      storage::BucketClientInfo client_info,
      mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
          checker_remote,
      mojo::PendingReceiver<blink::mojom::IDBFactory> receiver);

  // Helpers called from the fuzzer thread.
  void CreateAndAddIdbFactory(uint32_t id, base::OnceClosure done_closure)
      VALID_CONTEXT_REQUIRED(sequence_checker_);
  void CreateAndAddBlob(uint32_t id,
                        const std::string& content,
                        base::OnceClosure done_closure)
      VALID_CONTEXT_REQUIRED(sequence_checker_);

  const bool in_memory_;
  const base::AutoReset<std::optional<bool>> sqlite_override_;
  const bool use_default_bucket_;

  // These are set up (and used) on the UI thread.
  storage::BucketLocator bucket_locator_;
  std::unique_ptr<content::TestBrowserContext> browser_context_;
  mojo::Remote<storage::mojom::IndexedDBControlTest> indexed_db_control_test_;

  // These live on the fuzzer thread for now.
  MockIndexedDBClientStateChecker mock_client_state_checker_;
  mojo::ReceiverSet<storage::mojom::IndexedDBClientStateChecker>
      checker_receivers_ GUARDED_BY_CONTEXT(sequence_checker_);
};

IdbFactoryTestcase::IdbFactoryTestcase(
    const content::fuzzing::idb_factory::proto::Testcase& testcase)
    : Testcase<ProtoTestcase, ProtoAction>(testcase),
      in_memory_(testcase.in_memory()),
      sqlite_override_(
          content::indexed_db::BucketContext::OverrideShouldUseSqliteForTesting(
              testcase.use_sqlite())),
      use_default_bucket_(testcase.use_default_bucket()) {
  // IdbFactoryTestcase is created on the main thread, but the actions that
  // we want to validate the sequencing of take place on the fuzzer sequence.
  DETACH_FROM_SEQUENCE(sequence_checker_);
}

void IdbFactoryTestcase::SetUp(base::OnceClosure done_closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&IdbFactoryTestcase::SetUpOnUIThread,
                     base::Unretained(this), std::move(done_closure)));
}

void IdbFactoryTestcase::SetUpOnUIThread(base::OnceClosure done_closure) {
  browser_context_ = std::make_unique<content::TestBrowserContext>();
  browser_context_->set_is_off_the_record(in_memory_);
  browser_context_->GetDefaultStoragePartition()
      ->GetIndexedDBControl()
      .BindTestInterfaceForTesting(
          indexed_db_control_test_.BindNewPipeAndPassReceiver());

  auto continue_setup = base::BindPostTask(
      GetFuzzerTaskRunner(),
      base::BindOnce(&IdbFactoryTestcase::SetUpOnFuzzerThread,
                     base::Unretained(this), std::move(done_closure)));

  const blink::StorageKey kStorageKey = blink::StorageKey::CreateFirstParty(
      Origin::Create(GURL("https://example.com")));
  if (use_default_bucket_) {
    // `IndexedDBContextImpl` will create the bucket when binding the factory.
    bucket_locator_ = storage::BucketLocator::ForDefaultBucket(kStorageKey);
    std::move(continue_setup).Run();
  } else {
    // The bucket needs to be created beforehand.
    browser_context_->GetDefaultStoragePartition()
        ->GetQuotaManager()
        ->proxy()
        ->UpdateOrCreateBucket(
            storage::BucketInitParams(kStorageKey, "custom_bucket"),
            content::GetUIThreadTaskRunner({}),
            base::BindOnce(
                [](storage::BucketLocator& bucket_locator,
                   base::OnceClosure continue_setup,
                   storage::QuotaErrorOr<storage::BucketInfo> bucket_info) {
                  CHECK(bucket_info.has_value());
                  bucket_locator = bucket_info->ToBucketLocator();
                  std::move(continue_setup).Run();
                },
                std::ref(bucket_locator_), std::move(continue_setup)));
  }
}

void IdbFactoryTestcase::SetUpOnFuzzerThread(base::OnceClosure done_closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  mojolpm::GetContext()->StartTestcase();
  std::move(done_closure).Run();
}

void IdbFactoryTestcase::TearDown(base::OnceClosure done_closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&IdbFactoryTestcase::TearDownOnUIThread,
                     base::Unretained(this), std::move(done_closure)));
}

void IdbFactoryTestcase::TearDownOnUIThread(base::OnceClosure done_closure) {
  browser_context_.reset();
  indexed_db_control_test_.reset();
  GetFuzzerTaskRunner()->PostTask(
      FROM_HERE,
      base::BindOnce(&IdbFactoryTestcase::TearDownOnFuzzerThread,
                     base::Unretained(this), std::move(done_closure)));
}

void IdbFactoryTestcase::TearDownOnFuzzerThread(
    base::OnceClosure done_closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  checker_receivers_.Clear();
  mojolpm::GetContext()->EndTestcase();
  std::move(done_closure).Run();
}

void IdbFactoryTestcase::RunAction(const ProtoAction& action,
                                   base::OnceClosure done_closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  const auto ThreadId_UI =
      content::fuzzing::idb_factory::proto::RunThreadAction_ThreadId_UI;
  const auto ThreadId_IO =
      content::fuzzing::idb_factory::proto::RunThreadAction_ThreadId_IO;
  const auto ThreadId_Bucket =
      content::fuzzing::idb_factory::proto::RunThreadAction_ThreadId_Bucket;

  switch (action.action_case()) {
    case ProtoAction::kRunThread:
      // These actions ensure that any tasks currently queued on the named
      // thread have chance to run before the fuzzer continues.
      //
      // We don't provide any particular guarantees here; this does not mean
      // that the named thread is idle, nor does it prevent any other threads
      // from running (or the consequences of any resulting callbacks, for
      // example).
      switch (action.run_thread().id()) {
        case ThreadId_UI:
          content::GetUIThreadTaskRunner({})->PostTaskAndReply(
              FROM_HERE, base::DoNothing(), std::move(done_closure));
          break;
        case ThreadId_IO:
          content::GetIOThreadTaskRunner({})->PostTaskAndReply(
              FROM_HERE, base::DoNothing(), std::move(done_closure));
          break;
        case ThreadId_Bucket:
          content::GetUIThreadTaskRunner({})->PostTask(
              FROM_HERE,
              base::BindOnce(&IdbFactoryTestcase::FlushBucketSequence,
                             base::Unretained(this),
                             base::BindPostTask(GetFuzzerTaskRunner(),
                                                std::move(done_closure))));
          break;
      }
      return;

    case ProtoAction::kNewIdbFactory:
      CreateAndAddIdbFactory(action.new_idb_factory().id(),
                             std::move(done_closure));
      return;

    case ProtoAction::kNewBlob:
      CreateAndAddBlob(action.new_blob().id(), action.new_blob().content(),
                       std::move(done_closure));
      return;

    case ProtoAction::kIdbFactoryRemoteAction:
      mojolpm::HandleRemoteAction(action.idb_factory_remote_action());
      break;

    case ProtoAction::kIdbDatabaseAssociatedRemoteAction:
      mojolpm::HandleAssociatedRemoteAction(
          action.idb_database_associated_remote_action());
      break;

    case ProtoAction::kIdbTransactionAssociatedRemoteAction:
      mojolpm::HandleAssociatedRemoteAction(
          action.idb_transaction_associated_remote_action());
      break;

    case ProtoAction::kIdbCursorAssociatedRemoteAction:
      mojolpm::HandleAssociatedRemoteAction(
          action.idb_cursor_associated_remote_action());
      break;

    case ProtoAction::kBlobRemoteAction:
      mojolpm::HandleRemoteAction(action.blob_remote_action());
      break;

    case ProtoAction::ACTION_NOT_SET:
      break;
  }

  GetFuzzerTaskRunner()->PostTask(FROM_HERE, std::move(done_closure));
}

void IdbFactoryTestcase::FlushBucketSequence(base::OnceClosure done_closure) {
  indexed_db_control_test_->FlushBucketSequenceForTesting(
      bucket_locator_, std::move(done_closure));
}

void IdbFactoryTestcase::BindIndexedDB(
    storage::BucketClientInfo client_info,
    mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
        checker_remote,
    mojo::PendingReceiver<blink::mojom::IDBFactory> receiver) {
  browser_context_->GetDefaultStoragePartition()
      ->GetIndexedDBControl()
      .BindIndexedDB(bucket_locator_, std::move(client_info),
                     std::move(checker_remote), std::move(receiver));
}

void IdbFactoryTestcase::CreateAndAddIdbFactory(
    uint32_t id,
    base::OnceClosure done_closure) {
  storage::BucketClientInfo client_info;
  mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
      checker_remote;
  checker_receivers_.Add(&mock_client_state_checker_,
                         checker_remote.InitWithNewPipeAndPassReceiver(),
                         GetFuzzerTaskRunner());

  mojo::Remote<blink::mojom::IDBFactory> factory_remote;
  auto factory_receiver = factory_remote.BindNewPipeAndPassReceiver();

  // Use of Unretained is safe since `this` is guaranteed to live at least until
  // `done_closure` is invoked.
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&IdbFactoryTestcase::BindIndexedDB, base::Unretained(this),
                     std::move(client_info), std::move(checker_remote),
                     std::move(factory_receiver)));

  // Since the `PendingReceiver` to `IDBFactory` is consumed asynchronously by
  // `BindIndexedDB`, flush the remote before running `done_closure`.
  //
  // Note that `done_closure` may synchronously delete the remote (if the next
  // action is a reset of `IDBFactory`, for instance). To prevent a
  // use-after-free inside `FlushAsyncForTesting`, post a task to run
  // `done_closure` when the flush completes instead of running it directly.
  uint32_t lookup_id =
      mojolpm::GetContext()->AddInstance(id, std::move(factory_remote));
  mojolpm::GetContext()
      ->GetInstance<mojo::Remote<blink::mojom::IDBFactory>>(lookup_id)
      ->FlushAsyncForTesting(
          base::BindPostTask(GetFuzzerTaskRunner(), std::move(done_closure)));
}

void IdbFactoryTestcase::CreateAndAddBlob(uint32_t id,
                                          const std::string& content,
                                          base::OnceClosure done_closure) {
  std::vector<uint8_t> owned_bytes(content.begin(), content.end());
  base::span<const uint8_t> data = base::as_byte_span(owned_bytes);
  base::OnceClosure release_bytes =
      base::DoNothingWithBoundArgs(std::move(owned_bytes));
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          &content::BrowserContext::CreateMemoryBackedBlob,
          base::Unretained(browser_context_.get()), data, "text/plain",
          base::BindPostTask(
              GetFuzzerTaskRunner(),
              base::BindOnce(
                  [](uint32_t id, std::unique_ptr<content::BlobHandle> handle) {
                    if (handle) {
                      mojolpm::GetContext()->AddInstance(
                          id,
                          mojo::Remote<blink::mojom::Blob>(handle->PassBlob()));
                    }
                  },
                  id)
                  .Then(std::move(release_bytes))
                  .Then(std::move(done_closure)))));
}

DEFINE_BINARY_PROTO_FUZZER(
    const content::fuzzing::idb_factory::proto::Testcase& proto_testcase) {
  if (!proto_testcase.actions_size() || !proto_testcase.sequences_size() ||
      !proto_testcase.sequence_indexes_size()) {
    return;
  }

  GetEnvironment();

  IdbFactoryTestcase testcase(proto_testcase);

  // Unretained is safe here, because `main_run_loop` has to finish before
  // testcase goes out of scope.
  base::RunLoop main_run_loop;
  GetFuzzerTaskRunner()->PostTask(
      FROM_HERE,
      base::BindOnce(&mojolpm::RunTestcase<IdbFactoryTestcase>,
                     base::Unretained(&testcase), GetFuzzerTaskRunner(),
                     main_run_loop.QuitClosure()));
  main_run_loop.Run();
}
