@ 261105.16
current
#include "node_webstorage.h"
#include <string>
#include <unordered_map>
#include "base_object-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "memory_tracker-inl.h"
#include "node.h"
#include "node_errors.h"
#include "node_mem-inl.h"
#include "path.h"
#include "simdutf.h"
#include "sqlite3.h"
#include "util-inl.h"

namespace node {
namespace webstorage {

using v8::Array;
using v8::Boolean;
using v8::Context;
using v8::DontDelete;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::IndexedPropertyHandlerConfiguration;
using v8::Integer;
using v8::Intercepted;
using v8::Isolate;
using v8::JustVoid;
using v8::Local;
using v8::LocalVector;
using v8::Map;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Name;
using v8::NamedPropertyHandlerConfiguration;
using v8::NewStringType;
using v8::Nothing;
using v8::Null;
using v8::Object;
using v8::PropertyAttribute;
using v8::PropertyCallbackInfo;
using v8::PropertyDescriptor;
using v8::PropertyHandlerFlags;
using v8::Signature;
using v8::String;
using v8::Value;

#define THROW_SQLITE_ERROR(env, r)                                             \
  THROW_ERR_INVALID_STATE((env), sqlite3_errstr((r)))

#define CHECK_ERROR_OR_THROW(env, expr, expected, ret)                         \
  do {                                                                         \
    int r_ = (expr);                                                           \
    if (r_ != (expected)) {                                                    \
      THROW_SQLITE_ERROR((env), r_);                                           \
      return (ret);                                                            \
    }                                                                          \
  } while (0)

// The backing file is a user-specified path, and the schema below is created
// with IF NOT EXISTS, so a file that already holds tables of those names is
// adopted as-is and its values may have any type. A wrong type is therefore a
// statement about untrusted input, not a broken internal invariant.
#define CHECK_COLUMN_TYPE_OR_THROW(env, stmt, idx, expected, detail, ret)      \
  do {                                                                         \
    if (sqlite3_column_type((stmt), (idx)) != (expected)) {                    \
      THROW_ERR_INVALID_STATE((env),                                           \
                              "localStorage database is malformed: " detail);  \
      return (ret);                                                            \
    }                                                                          \
  } while (0)

static void ThrowQuotaExceededException(Local<Context> context) {
  Isolate* isolate = Isolate::GetCurrent();
  auto quota_exceeded_str =
      FIXED_ONE_BYTE_STRING(isolate, "QuotaExceededError");
  auto err_message =
      FIXED_ONE_BYTE_STRING(isolate, "Setting the value exceeded the quota");
  Local<Object> per_context_bindings;
  Local<Value> quota_exceeded_ctor_val;
  if (!GetPerContextExports(context).ToLocal(&per_context_bindings) ||
      !per_context_bindings->Get(context, quota_exceeded_str)
           .ToLocal(&quota_exceeded_ctor_val)) {
    return;
  }
  CHECK(quota_exceeded_ctor_val->IsFunction());
  Local<Function> quota_exceeded_ctor = quota_exceeded_ctor_val.As<Function>();
  Local<Value> argv[] = {err_message};
  Local<Value> exception;

  if (!quota_exceeded_ctor->NewInstance(context, arraysize(argv), argv)
           .ToLocal(&exception)) {
    return;
  }

  isolate->ThrowException(exception);
}

Storage::Storage(Environment* env,
                 Local<Object> object,
                 std::string_view location)
    : BaseObject(env, object) {
  MakeWeak();
  symbols_.Reset(env->isolate(), Map::New(env->isolate()));
  db_ = nullptr;
  location_ = std::string(location);
}

Storage::~Storage() {
  db_ = nullptr;
}

void Storage::MemoryInfo(MemoryTracker* tracker) const {
  tracker->TrackField("symbols", symbols_);
  tracker->TrackField("location", location_);
}

Maybe<void> Storage::Open() {
  static const int kCurrentSchemaVersion = 1;
  static constexpr std::string_view get_schema_version_sql =
      "SELECT schema_version FROM nodejs_webstorage_state";
  static constexpr std::string_view init_sql_v0 =
      "PRAGMA encoding = 'UTF-16le';"
      "PRAGMA busy_timeout = 3000;"
      "PRAGMA journal_mode = WAL;"
      "PRAGMA synchronous = NORMAL;"
      "PRAGMA temp_store = memory;"
      "PRAGMA optimize;"
      ""
      "CREATE TABLE IF NOT EXISTS nodejs_webstorage("
      "  key BLOB NOT NULL,"
      "  value BLOB NOT NULL,"
      "  PRIMARY KEY(key)"
      ") STRICT;"
      ""
      "CREATE TABLE IF NOT EXISTS nodejs_webstorage_state("
      // max_size is 10MB. This can be made configurable in the future.
      "  max_size INTEGER NOT NULL DEFAULT 10485760,"
      "  total_size INTEGER NOT NULL,"
      "  schema_version INTEGER NOT NULL DEFAULT 0,"
      "  single_row_ INTEGER NOT NULL DEFAULT 1 CHECK(single_row_ = 1),"
      "  PRIMARY KEY(single_row_)"
      ") STRICT;"
      ""
      "CREATE TRIGGER IF NOT EXISTS nodejs_quota_insert "
      "AFTER INSERT ON nodejs_webstorage "
      "FOR EACH ROW "
      "BEGIN "
      "  UPDATE nodejs_webstorage_state"
      "    SET total_size = total_size + OCTET_LENGTH(NEW.key) +"
      "      OCTET_LENGTH(NEW.value);"
      "  SELECT RAISE(ABORT, 'QuotaExceeded') WHERE EXISTS ("
      "    SELECT 1 FROM nodejs_webstorage_state WHERE total_size > max_size"
      "  );"
      "END;"
      ""
      "CREATE TRIGGER IF NOT EXISTS nodejs_quota_update "
      "AFTER UPDATE ON nodejs_webstorage "
      "FOR EACH ROW "
      "BEGIN "
      "  UPDATE nodejs_webstorage_state"
      "    SET total_size = total_size + "
      "      ((OCTET_LENGTH(NEW.key) + OCTET_LENGTH(NEW.value)) -"
      "      (OCTET_LENGTH(OLD.key) + OCTET_LENGTH(OLD.value)));"
      "  SELECT RAISE(ABORT, 'QuotaExceeded') WHERE EXISTS ("
      "    SELECT 1 FROM nodejs_webstorage_state WHERE total_size > max_size"
      "  );"
      "END;"
      ""
      "CREATE TRIGGER IF NOT EXISTS nodejs_quota_delete "
      "AFTER DELETE ON nodejs_webstorage "
      "FOR EACH ROW "
      "BEGIN "
      "  UPDATE nodejs_webstorage_state"
      "    SET total_size = total_size - (OCTET_LENGTH(OLD.key) +"
      "      OCTET_LENGTH(OLD.value));"
      "END;"
      ""
      "INSERT OR IGNORE INTO nodejs_webstorage_state (total_size) VALUES (0);";

  sqlite3* db = db_.get();
  if (db != nullptr) {
    return JustVoid();
  }

  int r = sqlite3_open(location_.c_str(), &db);
  // Adopt the connection before anything below can return early, so that a
  // failure does not leak it. sqlite3_open() allocates a connection to be
  // closed even when it fails. This is declared ahead of the statement below
  // so that the statement is finalized first; sqlite3_close() fails while a
  // statement is still open, and conn_deleter treats that as fatal.
  auto conn = conn_unique_ptr(db);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Nothing<void>());
  r = sqlite3_exec(db, init_sql_v0.data(), nullptr, nullptr, nullptr);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Nothing<void>());

  // Get the current schema version, used to determine schema migrations.
  sqlite3_stmt* s = nullptr;
  r = sqlite3_prepare_v2(db,
                         get_schema_version_sql.data(),
                         get_schema_version_sql.size(),
                         &s,
                         nullptr);
  auto stmt = stmt_unique_ptr(s);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Nothing<void>());
  CHECK_ERROR_OR_THROW(
      env(), sqlite3_step(stmt.get()), SQLITE_ROW, Nothing<void>());
  CHECK_COLUMN_TYPE_OR_THROW(env(),
                             stmt.get(),
                             0,
                             SQLITE_INTEGER,
                             "expected schema_version to be an integer",
                             Nothing<void>());
  int schema_version = sqlite3_column_int(stmt.get(), 0);
  stmt = nullptr;  // Force finalization.

  if (schema_version > kCurrentSchemaVersion) {
    THROW_ERR_INVALID_STATE(
        env(), "localStorage was created with a newer version of Node.js");
    return Nothing<void>();
  }

  if (schema_version < kCurrentSchemaVersion) {
    // Run any migrations and update the schema version.
    std::string set_user_version_sql =
        "UPDATE nodejs_webstorage_state SET schema_version = " +
        std::to_string(kCurrentSchemaVersion) + ";";
    r = sqlite3_exec(
        db, set_user_version_sql.c_str(), nullptr, nullptr, nullptr);
    CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Nothing<void>());
  }

  db_ = std::move(conn);
  return JustVoid();
}

void Storage::New(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  Realm* realm = Realm::GetCurrent(args);

  if (!args[0]->StrictEquals(realm->isolate_data()->constructor_key_symbol())) {
    THROW_ERR_ILLEGAL_CONSTRUCTOR(env);
    return;
  }

  CHECK(args.IsConstructCall());
  CHECK(args[1]->IsString());

  BufferValue location(env->isolate(), args[1]);
  CHECK_NOT_NULL(*location);
  // Only call namespaced path if the location is not "in memory".
  if (location.ToStringView() != kInMemoryPath) {
    ToNamespacedPath(env, &location);
  }

  new Storage(env, args.This(), location.ToStringView());
}

Maybe<void> Storage::Clear() {
  if (!Open().IsJust()) {
    return Nothing<void>();
  }

  static constexpr std::string_view sql = "DELETE FROM nodejs_webstorage";
  sqlite3_stmt* s = nullptr;
  CHECK_ERROR_OR_THROW(
      env(),
      sqlite3_prepare_v2(db_.get(), sql.data(), sql.size(), &s, nullptr),
      SQLITE_OK,
      Nothing<void>());
  auto stmt = stmt_unique_ptr(s);
  CHECK_ERROR_OR_THROW(
      env(), sqlite3_step(stmt.get()), SQLITE_DONE, Nothing<void>());
  return JustVoid();
}

MaybeLocal<Array> Storage::Enumerate() {
  if (!Open().IsJust()) {
    return Local<Array>();
  }

  static constexpr std::string_view sql = "SELECT key FROM nodejs_webstorage";
  sqlite3_stmt* s = nullptr;
  int r = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size(), &s, nullptr);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Array>());
  auto stmt = stmt_unique_ptr(s);
  LocalVector<Value> values(env()->isolate());
  Local<Value> value;
  while ((r = sqlite3_step(stmt.get())) == SQLITE_ROW) {
    CHECK_COLUMN_TYPE_OR_THROW(env(),
                               stmt.get(),
                               0,
                               SQLITE_BLOB,
                               "expected key to be a blob",
                               Local<Array>());
    auto size = sqlite3_column_bytes(stmt.get(), 0) / sizeof(uint16_t);
    if (!String::NewFromTwoByte(env()->isolate(),
                                reinterpret_cast<const uint16_t*>(
                                    sqlite3_column_blob(stmt.get(), 0)),
                                NewStringType::kNormal,
                                size)
             .ToLocal(&value)) {
      return Local<Array>();
    }
    values.emplace_back(value);
  }
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_DONE, Local<Array>());
  return Array::New(env()->isolate(), values.data(), values.size());
}

std::optional<std::unordered_map<std::u16string, std::u16string>>
Storage::GetAll() {
  if (!Open().IsJust()) {
    return std::nullopt;
  }

  static constexpr std::string_view sql =
      "SELECT key, value FROM nodejs_webstorage";
  sqlite3_stmt* s = nullptr;
  int r = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size(), &s, nullptr);
  auto stmt = stmt_unique_ptr(s);
  // Unlike the other accessors, this one has no JavaScript caller to throw at,
  // so every failure below is reported to the inspector agent instead.
  if (r != SQLITE_OK) {
    return std::nullopt;
  }
  std::unordered_map<std::u16string, std::u16string> result;
  while ((r = sqlite3_step(stmt.get())) == SQLITE_ROW) {
    if (sqlite3_column_type(stmt.get(), 0) != SQLITE_BLOB ||
        sqlite3_column_type(stmt.get(), 1) != SQLITE_BLOB) {
      return std::nullopt;
    }
    auto key_size = sqlite3_column_bytes(stmt.get(), 0) / sizeof(uint16_t);
    auto value_size = sqlite3_column_bytes(stmt.get(), 1) / sizeof(uint16_t);
    auto key_uint16(
        reinterpret_cast<const char16_t*>(sqlite3_column_blob(stmt.get(), 0)));
    auto value_uint16(
        reinterpret_cast<const char16_t*>(sqlite3_column_blob(stmt.get(), 1)));

    std::u16string key(key_uint16, key_size);
    std::u16string value(value_uint16, value_size);

    result.emplace(std::move(key), std::move(value));
  }
  if (r != SQLITE_DONE) {
    return std::nullopt;
  }
  return result;
}

MaybeLocal<Value> Storage::Length() {
  if (!Open().IsJust()) {
    return {};
  }

  static constexpr std::string_view sql =
      "SELECT count(*) FROM nodejs_webstorage";
  sqlite3_stmt* s = nullptr;
  int r = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size(), &s, nullptr);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Value>());
  auto stmt = stmt_unique_ptr(s);
  CHECK_ERROR_OR_THROW(
      env(), sqlite3_step(stmt.get()), SQLITE_ROW, Local<Value>());
  // Unlike the reads above, this one is not a claim about the file's contents:
  // count(*) is an integer whatever the table holds.
  CHECK(sqlite3_column_type(stmt.get(), 0) == SQLITE_INTEGER);
  int result = sqlite3_column_int(stmt.get(), 0);
  return Integer::New(env()->isolate(), result);
}

MaybeLocal<Value> Storage::Load(Local<Name> key) {
  if (key->IsSymbol()) {
    auto symbol_map = symbols_.Get(env()->isolate());
    return symbol_map->Get(env()->context(), key);
  }

  if (!Open().IsJust()) {
    return {};
  }

  static constexpr std::string_view sql =
      "SELECT value FROM nodejs_webstorage WHERE key = ? LIMIT 1";
  sqlite3_stmt* s = nullptr;
  int r = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size(), &s, nullptr);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Value>());
  auto stmt = stmt_unique_ptr(s);
  TwoByteValue utf16key(env()->isolate(), key);
  auto key_size = utf16key.length() * sizeof(uint16_t);
  r = sqlite3_bind_blob(stmt.get(), 1, utf16key.out(), key_size, SQLITE_STATIC);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Value>());
  r = sqlite3_step(stmt.get());
  if (r == SQLITE_ROW) {
    CHECK_COLUMN_TYPE_OR_THROW(env(),
                               stmt.get(),
                               0,
                               SQLITE_BLOB,
                               "expected value to be a blob",
                               Local<Value>());
    auto size = sqlite3_column_bytes(stmt.get(), 0) / sizeof(uint16_t);
    return String::NewFromTwoByte(env()->isolate(),
                                  reinterpret_cast<const uint16_t*>(
                                      sqlite3_column_blob(stmt.get(), 0)),
                                  NewStringType::kNormal,
                                  size)
        .As<Value>();
  } else if (r != SQLITE_DONE) {
    THROW_SQLITE_ERROR(env(), r);
    return {};
  } else {
    return Null(env()->isolate());
  }
}

MaybeLocal<Value> Storage::LoadKey(const int index) {
  if (!Open().IsJust()) {
    return {};
  }

  static constexpr std::string_view sql =
      "SELECT key FROM nodejs_webstorage LIMIT 1 OFFSET ?";
  sqlite3_stmt* s = nullptr;
  int r = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size(), &s, nullptr);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Value>());
  auto stmt = stmt_unique_ptr(s);
  r = sqlite3_bind_int(stmt.get(), 1, index);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Local<Value>());

  r = sqlite3_step(stmt.get());
  if (r == SQLITE_ROW) {
    CHECK_COLUMN_TYPE_OR_THROW(env(),
                               stmt.get(),
                               0,
                               SQLITE_BLOB,
                               "expected key to be a blob",
                               Local<Value>());
    auto size = sqlite3_column_bytes(stmt.get(), 0) / sizeof(uint16_t);
    return String::NewFromTwoByte(env()->isolate(),
                                  reinterpret_cast<const uint16_t*>(
                                      sqlite3_column_blob(stmt.get(), 0)),
                                  NewStringType::kNormal,
                                  size)
        .As<Value>();
  } else if (r != SQLITE_DONE) {
    THROW_SQLITE_ERROR(env(), r);
    return {};
  } else {
    return Null(env()->isolate());
  }
}

Maybe<void> Storage::Remove(Local<Name> key) {
  if (key->IsSymbol()) {
    auto symbol_map = symbols_.Get(env()->isolate());
    Maybe<bool> result = symbol_map->Delete(env()->context(), key);
    return result.IsNothing() ? Nothing<void>() : JustVoid();
  }

  if (!Open().IsJust()) {
    return Nothing<void>();
  }

  static constexpr std::string_view sql =
      "DELETE FROM nodejs_webstorage WHERE key = ?";
  sqlite3_stmt* s = nullptr;
  int r = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size(), &s, nullptr);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Nothing<void>());
  auto stmt = stmt_unique_ptr(s);
  TwoByteValue utf16key(env()->isolate(), key);
  auto key_size = utf16key.length() * sizeof(uint16_t);
  r = sqlite3_bind_blob(stmt.get(), 1, utf16key.out(), key_size, SQLITE_STATIC);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Nothing<void>());
  CHECK_ERROR_OR_THROW(
      env(), sqlite3_step(stmt.get()), SQLITE_DONE, Nothing<void>());
  return JustVoid();
}

Maybe<void> Storage::Store(Local<Name> key, Local<Value> value) {
  if (key->IsSymbol()) {
    auto symbol_map = symbols_.Get(env()->isolate());
    MaybeLocal<Map> result = symbol_map->Set(env()->context(), key, value);
    return result.IsEmpty() ? Nothing<void>() : JustVoid();
  }

  Local<String> val;
  if (!value->ToString(env()->context()).ToLocal(&val)) {
    return Nothing<void>();
  }

  if (!Open().IsJust()) {
    return Nothing<void>();
  }

  static constexpr std::string_view sql =
      "INSERT INTO nodejs_webstorage (key, value) VALUES (?, ?)"
      "  ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value"
      "  WHERE EXCLUDED.key = key";
  sqlite3_stmt* s = nullptr;
  TwoByteValue utf16key(env()->isolate(), key);
  TwoByteValue utf16val(env()->isolate(), val);
  int r = sqlite3_prepare_v2(db_.get(), sql.data(), sql.size(), &s, nullptr);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Nothing<void>());
  auto stmt = stmt_unique_ptr(s);
  auto key_size = utf16key.length() * sizeof(uint16_t);
  r = sqlite3_bind_blob(stmt.get(), 1, utf16key.out(), key_size, SQLITE_STATIC);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Nothing<void>());
  auto val_size = utf16val.length() * sizeof(uint16_t);
  r = sqlite3_bind_blob(stmt.get(), 2, utf16val.out(), val_size, SQLITE_STATIC);
  CHECK_ERROR_OR_THROW(env(), r, SQLITE_OK, Nothing<void>());

  r = sqlite3_step(stmt.get());
  if (r == SQLITE_CONSTRAINT) {
    ThrowQuotaExceededException(env()->context());
    return Nothing<void>();
  }

  CHECK_ERROR_OR_THROW(env(), r, SQLITE_DONE, Nothing<void>());
  return JustVoid();
}

static void Clear(const FunctionCallbackInfo<Value>& info) {
  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.This());
  storage->Clear();
}

static void GetItem(const FunctionCallbackInfo<Value>& info) {
  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.This());
  Environment* env = Environment::GetCurrent(info);

  if (info.Length() < 1) {
    return THROW_ERR_MISSING_ARGS(
        env, "Failed to execute 'getItem' on 'Storage': 1 argument required");
  }

  Local<String> prop;
  if (!info[0]->ToString(env->context()).ToLocal(&prop)) {
    return;
  }

  Local<Value> result;
  if (!storage->Load(prop).ToLocal(&result)) {
    return;
  }
  info.GetReturnValue().Set(result);
}

static void Key(const FunctionCallbackInfo<Value>& info) {
  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.This());
  Environment* env = Environment::GetCurrent(info);
  int index;

  if (info.Length() < 1) {
    return THROW_ERR_MISSING_ARGS(
        env, "Failed to execute 'key' on 'Storage': 1 argument required");
  }

  if (!info[0]->Int32Value(env->context()).To(&index)) {
    return;
  }

  if (index < 0) {
    info.GetReturnValue().SetNull();
    return;
  }

  Local<Value> result;
  if (storage->LoadKey(index).ToLocal(&result)) {
    info.GetReturnValue().Set(result);
  }
}

static void RemoveItem(const FunctionCallbackInfo<Value>& info) {
  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.This());
  Environment* env = Environment::GetCurrent(info);
  Local<String> prop;

  if (info.Length() < 1) {
    return THROW_ERR_MISSING_ARGS(
        env,
        "Failed to execute 'removeItem' on 'Storage': 1 argument required");
  }

  if (!info[0]->ToString(env->context()).ToLocal(&prop)) {
    return;
  }

  storage->Remove(prop);
}

static void SetItem(const FunctionCallbackInfo<Value>& info) {
  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.This());
  Environment* env = Environment::GetCurrent(info);

  if (info.Length() < 2) {
    return THROW_ERR_MISSING_ARGS(
        env, "Failed to execute 'setItem' on 'Storage': 2 arguments required");
  }

  Local<String> prop;
  if (!info[0]->ToString(env->context()).ToLocal(&prop)) {
    return;
  }

  storage->Store(prop, info[1]);
}

template <typename T>
static bool ShouldIntercept(Local<Name> property,
                            const PropertyCallbackInfo<T>& info) {
  Environment* env = Environment::GetCurrent(info);
  Local<Value> proto = info.Holder()->GetPrototype();

  if (proto->IsObject()) {
    bool has_prop;

    if (!proto.As<Object>()->Has(env->context(), property).To(&has_prop)) {
      return false;
    }

    if (has_prop) {
      return false;
    }
  }

  return true;
}

static Intercepted StorageGetter(Local<Name> property,
                                 const PropertyCallbackInfo<Value>& info) {
  if (!ShouldIntercept(property, info)) {
    return Intercepted::kNo;
  }

  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder(), Intercepted::kNo);
  Local<Value> result;

  if (storage->Load(property).ToLocal(&result) && !result->IsNull()) {
    info.GetReturnValue().Set(result);
  }

  return Intercepted::kYes;
}

static Intercepted StorageSetter(Local<Name> property,
                                 Local<Value> value,
                                 const PropertyCallbackInfo<Boolean>& info) {
  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder(), Intercepted::kNo);

  if (storage->Store(property, value).IsNothing()) {
    info.GetReturnValue().SetFalse();
  }

  return Intercepted::kYes;
}

static Intercepted StorageQuery(Local<Name> property,
                                const PropertyCallbackInfo<Integer>& info) {
  if (!ShouldIntercept(property, info)) {
    return Intercepted::kNo;
  }

  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder(), Intercepted::kNo);
  Local<Value> result;
  if (!storage->Load(property).ToLocal(&result) || result->IsNull()) {
    return Intercepted::kNo;
  }

  info.GetReturnValue().Set(0);
  return Intercepted::kYes;
}

static Intercepted StorageDeleter(Local<Name> property,
                                  const PropertyCallbackInfo<Boolean>& info) {
  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder(), Intercepted::kNo);

  info.GetReturnValue().Set(storage->Remove(property).IsJust());

  return Intercepted::kYes;
}

static void StorageEnumerator(const PropertyCallbackInfo<Array>& info) {
  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder());
  Local<Array> result;
  if (!storage->Enumerate().ToLocal(&result)) {
    return;
  }
  info.GetReturnValue().Set(result);
}

static Intercepted StorageDefiner(Local<Name> property,
                                  const PropertyDescriptor& desc,
                                  const PropertyCallbackInfo<Boolean>& info) {
  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.Holder(), Intercepted::kNo);

  if (desc.has_value()) {
    return StorageSetter(property, desc.value(), info);
  }

  return Intercepted::kYes;
}

static Intercepted IndexedGetter(uint32_t index,
                                 const PropertyCallbackInfo<Value>& info) {
  Environment* env = Environment::GetCurrent(info);
  Local<Name> name = Uint32ToString(env->context(), index);
  return StorageGetter(name, info);
}

static Intercepted IndexedSetter(uint32_t index,
                                 Local<Value> value,
                                 const PropertyCallbackInfo<Boolean>& info) {
  Environment* env = Environment::GetCurrent(info);
  Local<Name> name = Uint32ToString(env->context(), index);
  return StorageSetter(name, value, info);
}

static Intercepted IndexedQuery(uint32_t index,
                                const PropertyCallbackInfo<Integer>& info) {
  Environment* env = Environment::GetCurrent(info);
  Local<Name> name = Uint32ToString(env->context(), index);
  return StorageQuery(name, info);
}

static Intercepted IndexedDeleter(uint32_t index,
                                  const PropertyCallbackInfo<Boolean>& info) {
  Environment* env = Environment::GetCurrent(info);
  Local<Name> name = Uint32ToString(env->context(), index);
  return StorageDeleter(name, info);
}

static Intercepted IndexedDefiner(uint32_t index,
                                  const PropertyDescriptor& desc,
                                  const PropertyCallbackInfo<Boolean>& info) {
  Environment* env = Environment::GetCurrent(info);
  Local<Name> name = Uint32ToString(env->context(), index);
  return StorageDefiner(name, desc, info);
}

static void StorageLengthGetter(const FunctionCallbackInfo<Value>& info) {
  Storage* storage;
  ASSIGN_OR_RETURN_UNWRAP(&storage, info.This());
  Local<Value> result;
  if (!storage->Length().ToLocal(&result)) {
    return;
  }
  info.GetReturnValue().Set(result);
}

static void Initialize(Local<Object> target,
                       Local<Value> unused,
                       Local<Context> context,
                       void* priv) {
  Environment* env = Environment::GetCurrent(context);
  Isolate* isolate = env->isolate();
  auto ctor_tmpl = NewFunctionTemplate(isolate, Storage::New);
  auto inst_tmpl = ctor_tmpl->InstanceTemplate();

  inst_tmpl->SetInternalFieldCount(Storage::kInternalFieldCount);
  inst_tmpl->SetHandler(NamedPropertyHandlerConfiguration(
      StorageGetter,
      StorageSetter,
      StorageQuery,
      StorageDeleter,
      StorageEnumerator,
      StorageDefiner,
      nullptr,
      Local<Value>(),
      PropertyHandlerFlags::kHasNoSideEffect));
  inst_tmpl->SetHandler(IndexedPropertyHandlerConfiguration(
      IndexedGetter,
      IndexedSetter,
      IndexedQuery,
      IndexedDeleter,
      nullptr,
      IndexedDefiner,
      nullptr,
      Local<Value>(),
      PropertyHandlerFlags::kHasNoSideEffect));

  Local<Signature> length_signature = Signature::New(isolate, ctor_tmpl);
  Local<FunctionTemplate> length_getter = FunctionTemplate::New(
      isolate, StorageLengthGetter, Local<Value>(), length_signature);
  ctor_tmpl->PrototypeTemplate()->SetAccessorProperty(env->length_string(),
                                                      length_getter,
                                                      Local<FunctionTemplate>(),
                                                      DontDelete);

  SetProtoMethod(isolate, ctor_tmpl, "clear", Clear);
  SetProtoMethodNoSideEffect(isolate, ctor_tmpl, "getItem", GetItem);
  SetProtoMethodNoSideEffect(isolate, ctor_tmpl, "key", Key);
  SetProtoMethod(isolate, ctor_tmpl, "removeItem", RemoveItem);
  SetProtoMethod(isolate, ctor_tmpl, "setItem", SetItem);
  SetConstructorFunction(context, target, "Storage", ctor_tmpl);

  auto symbol = env->isolate_data()->constructor_key_symbol();
  target
      ->DefineOwnProperty(context,
                          FIXED_ONE_BYTE_STRING(isolate, "kConstructorKey"),
                          symbol,
                          PropertyAttribute::ReadOnly)
      .Check();
}

}  // namespace webstorage
}  // namespace node

NODE_BINDING_CONTEXT_AWARE_INTERNAL(webstorage, node::webstorage::Initialize)