diff --git a/lib/internal/inspector/webstorage.js b/lib/internal/inspector/webstorage.js new file mode 100644 index 00000000000000..2bb6f5e6f8c743 --- /dev/null +++ b/lib/internal/inspector/webstorage.js @@ -0,0 +1,107 @@ +'use strict'; + +const { Storage } = internalBinding('webstorage'); +const { DOMStorage } = require('inspector'); +const path = require('path'); +const { getOptionValue } = require('internal/options'); + +class InspectorLocalStorage extends Storage { + setItem(key, value) { + const oldValue = this.getItem(key); + super.setItem(key, value); + if (oldValue == null) { + itemAdded(key, value, true); + } else { + itemUpdated(key, oldValue, value, true); + } + } + + removeItem(key) { + super.removeItem(key); + itemRemoved(key, true); + } + + clear() { + super.clear(); + itemsCleared(true); + } +} + +const InspectorSessionStorage = class extends Storage { + setItem(key, value) { + const oldValue = this.getItem(key); + super.setItem(key, value); + if (oldValue == null) { + itemAdded(key, value, false); + } else { + itemUpdated(key, oldValue, value, false); + } + } + + removeItem(key) { + super.removeItem(key); + itemRemoved(key, false); + } + + clear() { + super.clear(); + itemsCleared(false); + } +}; + +function itemAdded(key, value, isLocalStorage) { + DOMStorage.domStorageItemAdded({ + key, + newValue: value, + storageId: { + securityOrigin: '', + isLocalStorage, + storageKey: getStorageKey(), + }, + }); +} + +function itemUpdated(key, oldValue, newValue, isLocalStorage) { + DOMStorage.domStorageItemUpdated({ + key, + oldValue, + newValue, + storageId: { + securityOrigin: '', + isLocalStorage, + storageKey: getStorageKey(), + }, + }); +} + +function itemRemoved(key, isLocalStorage) { + DOMStorage.domStorageItemRemoved({ + key, + storageId: { + securityOrigin: '', + isLocalStorage, + storageKey: getStorageKey(), + }, + }); +} + +function itemsCleared(isLocalStorage) { + DOMStorage.domStorageItemsCleared({ + storageId: { + securityOrigin: '', + isLocalStorage, + storageKey: getStorageKey(), + }, + }); +} + +function getStorageKey() { + const localStorageFile = getOptionValue('--localstorage-file'); + const resolvedAbsolutePath = path.resolve(localStorageFile); + return 'file://' + resolvedAbsolutePath; +} + +module.exports = { + InspectorLocalStorage, + InspectorSessionStorage, +}; diff --git a/lib/internal/webstorage.js b/lib/internal/webstorage.js index 47c71676995f09..5d4978d9187982 100644 --- a/lib/internal/webstorage.js +++ b/lib/internal/webstorage.js @@ -5,6 +5,7 @@ const { const { getOptionValue } = require('internal/options'); const { kConstructorKey, Storage } = internalBinding('webstorage'); const { getValidatedPath } = require('internal/fs/utils'); +const { InspectorLocalStorage, InspectorSessionStorage } = require('internal/inspector/webstorage'); const kInMemoryPath = ':memory:'; module.exports = { Storage }; @@ -36,9 +37,12 @@ ObjectDefineProperties(module.exports, { return undefined; } - lazyLocalStorage = new Storage(kConstructorKey, getValidatedPath(localStorageLocation)); + if (getOptionValue('--experimental-storage-inspection')) { + lazyLocalStorage = new InspectorLocalStorage(kConstructorKey, getValidatedPath(localStorageLocation), true); + } else { + lazyLocalStorage = new Storage(kConstructorKey, getValidatedPath(localStorageLocation)); + } } - return lazyLocalStorage; }, }, @@ -48,7 +52,11 @@ ObjectDefineProperties(module.exports, { enumerable: true, get() { if (lazySessionStorage === undefined) { - lazySessionStorage = new Storage(kConstructorKey, kInMemoryPath); + if (getOptionValue('--experimental-storage-inspection')) { + lazySessionStorage = new InspectorSessionStorage(kConstructorKey, kInMemoryPath, false); + } else { + lazySessionStorage = new Storage(kConstructorKey, kInMemoryPath); + } } return lazySessionStorage; diff --git a/src/inspector/dom_storage_agent.cc b/src/inspector/dom_storage_agent.cc index d300266548ca87..aebfe845c2e3d1 100644 --- a/src/inspector/dom_storage_agent.cc +++ b/src/inspector/dom_storage_agent.cc @@ -1,6 +1,9 @@ #include "dom_storage_agent.h" +#include #include "env-inl.h" #include "inspector/inspector_object_utils.h" +#include "util.h" +#include "v8-exception.h" #include "v8-isolate.h" namespace node { @@ -85,14 +88,27 @@ protocol::DispatchResponse DOMStorageAgent::getDOMStorageItems( "DOMStorage domain is not enabled"); } bool is_local_storage = storageId->getIsLocalStorage(); - const std::unordered_map& storage_map = - is_local_storage ? local_storage_map_ : session_storage_map_; + std::optional storage_map = + is_local_storage ? std::make_optional(local_storage_map_) + : std::make_optional(session_storage_map_); + if (storage_map->empty()) { + auto web_storage_obj = getWebStorage(is_local_storage); + if (web_storage_obj) { + StorageMap all_items = web_storage_obj.value()->GetAll(); + storage_map = std::make_optional(std::move(all_items)); + } + } + auto result = std::make_unique>>(); - for (const auto& pair : storage_map) { + for (const auto& pair : *storage_map) { auto item = std::make_unique>(); - item->push_back(pair.first); - item->push_back(pair.second); + item->push_back(protocol::StringUtil::fromUTF16( + reinterpret_cast(pair.first.data()), + pair.first.size())); + item->push_back(protocol::StringUtil::fromUTF16( + reinterpret_cast(pair.second.data()), + pair.second.size())); result->push_back(std::move(item)); } *items = std::move(result); @@ -219,7 +235,7 @@ void DOMStorageAgent::registerStorage(Local context, .ToLocal(&storage_map_obj)) { return; } - std::unordered_map& storage_map = + StorageMap& storage_map = is_local_storage ? local_storage_map_ : session_storage_map_; Local property_names; if (!storage_map_obj->GetOwnPropertyNames(context).ToLocal(&property_names)) { @@ -235,9 +251,35 @@ void DOMStorageAgent::registerStorage(Local context, if (!storage_map_obj->Get(context, key_value).ToLocal(&value_value)) { return; } - node::Utf8Value key_utf8(isolate, key_value); - node::Utf8Value value_utf8(isolate, value_value); - storage_map[*key_utf8] = *value_utf8; + node::TwoByteValue key_utf16(isolate, key_value); + node::TwoByteValue value_utf16(isolate, value_value); + storage_map[std::u16string(reinterpret_cast(*key_utf16), + key_utf16.length())] = + std::u16string(reinterpret_cast(*value_utf16), + value_utf16.length()); + } +} + +std::optional DOMStorageAgent::getWebStorage( + bool is_local_storage) { + v8::Isolate* isolate = env_->isolate(); + v8::HandleScope handle_scope(isolate); + v8::Local global = env_->context()->Global(); + v8::Local web_storage_val; + v8::TryCatch try_catch(isolate); + if (!global + ->Get(env_->context(), + is_local_storage + ? FIXED_ONE_BYTE_STRING(isolate, "localStorage") + : FIXED_ONE_BYTE_STRING(isolate, "sessionStorage")) + .ToLocal(&web_storage_val) || + !web_storage_val->IsObject() || try_catch.HasCaught()) { + return std::nullopt; + } else { + node::webstorage::Storage* storage; + ASSIGN_OR_RETURN_UNWRAP( + &storage, web_storage_val.As(), std::nullopt); + return storage; } } diff --git a/src/inspector/dom_storage_agent.h b/src/inspector/dom_storage_agent.h index 954f041d40ef58..a6eecbb8d37c95 100644 --- a/src/inspector/dom_storage_agent.h +++ b/src/inspector/dom_storage_agent.h @@ -1,9 +1,11 @@ #ifndef SRC_INSPECTOR_DOM_STORAGE_AGENT_H_ #define SRC_INSPECTOR_DOM_STORAGE_AGENT_H_ +#include #include #include "env.h" #include "node/inspector/protocol/DOMStorage.h" +#include "node_webstorage.h" #include "notification_emitter.h" #include "v8.h" @@ -50,9 +52,12 @@ class DOMStorageAgent : public protocol::DOMStorage::Backend, DOMStorageAgent& operator=(const DOMStorageAgent&) = delete; private: + typedef std::unordered_map StorageMap; + std::optional getWebStorage( + bool is_local_storage); std::unique_ptr frontend_; - std::unordered_map local_storage_map_ = {}; - std::unordered_map session_storage_map_ = {}; + StorageMap local_storage_map_ = {}; + StorageMap session_storage_map_ = {}; bool enabled_ = false; Environment* env_; }; diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 5ef8d06933700c..08b029aec36476 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -144,6 +144,7 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { "wasi", // Experimental. #if !HAVE_SQLITE "internal/webstorage", // Experimental. + "internal/inspector/webstorage", #endif "internal/test/binding", "internal/v8_prof_polyfill", }; diff --git a/src/node_webstorage.cc b/src/node_webstorage.cc index 224f49e8596cf1..25f2826573aa37 100644 --- a/src/node_webstorage.cc +++ b/src/node_webstorage.cc @@ -1,4 +1,6 @@ #include "node_webstorage.h" +#include +#include #include "base_object-inl.h" #include "debug_utils-inl.h" #include "env-inl.h" @@ -7,6 +9,7 @@ #include "node_errors.h" #include "node_mem-inl.h" #include "path.h" +#include "simdutf.h" #include "sqlite3.h" #include "util-inl.h" @@ -278,6 +281,35 @@ MaybeLocal Storage::Enumerate() { return Array::New(env()->isolate(), values.data(), values.size()); } +std::unordered_map Storage::GetAll() { + if (!Open().IsJust()) { + return {}; + } + + 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); + std::unordered_map result; + while ((r = sqlite3_step(stmt.get())) == SQLITE_ROW) { + CHECK(sqlite3_column_type(stmt.get(), 0) == SQLITE_BLOB); + CHECK(sqlite3_column_type(stmt.get(), 1) == SQLITE_BLOB); + 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(sqlite3_column_blob(stmt.get(), 0))); + auto value_uint16( + reinterpret_cast(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)); + } + return result; +} + MaybeLocal Storage::Length() { if (!Open().IsJust()) { return {}; diff --git a/src/node_webstorage.h b/src/node_webstorage.h index c2548d32e993fd..938a2333194b76 100644 --- a/src/node_webstorage.h +++ b/src/node_webstorage.h @@ -3,6 +3,7 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include #include "base_object.h" #include "node_mem.h" #include "sqlite3.h" @@ -40,6 +41,7 @@ class Storage : public BaseObject { v8::MaybeLocal LoadKey(const int index); v8::Maybe Remove(v8::Local key); v8::Maybe Store(v8::Local key, v8::Local value); + std::unordered_map GetAll(); SET_MEMORY_INFO_NAME(Storage) SET_SELF_SIZE(Storage) diff --git a/test/parallel/test-inspector-dom-storage.js b/test/parallel/test-inspector-dom-storage.js index f1cc3bbff3d076..a380f18762d5fb 100644 --- a/test/parallel/test-inspector-dom-storage.js +++ b/test/parallel/test-inspector-dom-storage.js @@ -3,13 +3,13 @@ const common = require('../common'); const assert = require('assert'); +common.skipIfSQLiteMissing(); common.skipIfInspectorDisabled(); const { DOMStorage, Session } = require('node:inspector/promises'); const { pathToFileURL } = require('node:url'); const path = require('node:path'); - -async function test() { +async function testRegisterStorage() { const session = new Session(); await session.connect(); @@ -26,6 +26,7 @@ async function test() { await checkStorage(true); await checkStorage(false); + session.disconnect(); async function checkStorage(isLocalStorage) { DOMStorage.registerStorage({ @@ -35,6 +36,7 @@ async function test() { key2: 'value2', [1]: 2, [true]: 'booleanKey', + ['ключ']: 'значение', }, }); const result = await session.post('DOMStorage.getDOMStorageItems', { @@ -43,14 +45,126 @@ async function test() { securityOrigin: 'node-inspector://default-dom-storage', }, }); - const sortedEntries = result.entries.sort((a, b) => a[0].localeCompare(b[0])); + const sortedEntries = result.entries.sort((a, b) => + a[0].localeCompare(b[0]), + ); assert.deepStrictEqual(sortedEntries, [ ['1', '2'], ['key1', 'value1'], ['key2', 'value2'], ['true', 'booleanKey'], + ['ключ', 'значение'], ]); } } +async function testGetData() { + await test(true); + await test(false); + + async function test(isLocalStorage) { + const webStorage = isLocalStorage ? localStorage : sessionStorage; + const session = new Session(); + webStorage.clear(); + await session.connect(); + + const storageKey = await session.post('Storage.getStorageKey'); + await session.post('DOMStorage.enable'); + + webStorage.setItem('key1', 'value'); + webStorage.setItem('key2', 1); + webStorage.setItem('key3', JSON.stringify({ a: 1 })); + webStorage.setItem('ключ', 'значение'); + + const result = await session.post('DOMStorage.getDOMStorageItems', { + storageId: { + isLocalStorage, + securityOrigin: '', + storageKey: storageKey.storageKey, + }, + }); + assert.strictEqual(result.entries.length, 4); + const entries = Object.fromEntries(result.entries); + assert.strictEqual(entries.key1, 'value'); + assert.strictEqual(entries.key2, '1'); + assert.strictEqual(entries.key3, JSON.stringify({ a: 1 })); + assert.strictEqual(entries['ключ'], 'значение'); + session.disconnect(); + } +} + +async function testEvents() { + await test(true); + await test(false); + async function test(isLocalStorage) { + const webStorage = isLocalStorage ? localStorage : sessionStorage; + webStorage.clear(); + const session = new Session(); + await session.connect(); + await session.post('DOMStorage.enable'); + const storageKey = await session.post('Storage.getStorageKey'); + session.on( + 'DOMStorage.domStorageItemAdded', + common.mustCall(({ params }) => { + assert.strictEqual(params.key, 'key'); + assert.strictEqual(params.newValue, 'value'); + assert.deepStrictEqual(params.storageId, { + securityOrigin: '', + isLocalStorage, + storageKey: storageKey.storageKey, + }); + }), + ); + webStorage.setItem('key', 'value'); + + session.on( + 'DOMStorage.domStorageItemUpdated', + common.mustCall(({ params }) => { + assert.strictEqual(params.key, 'key'); + assert.strictEqual(params.oldValue, 'value'); + assert.strictEqual(params.newValue, 'newValue'); + assert.deepStrictEqual(params.storageId, { + securityOrigin: '', + isLocalStorage, + storageKey: storageKey.storageKey, + }); + }), + ); + + webStorage.setItem('key', 'newValue'); + + session.on( + 'DOMStorage.domStorageItemRemoved', + common.mustCall(({ params }) => { + assert.strictEqual(params.key, 'key'); + assert.deepStrictEqual(params.storageId, { + securityOrigin: '', + isLocalStorage, + storageKey: storageKey.storageKey, + }); + }), + ); + + webStorage.removeItem('key'); + + session.on( + 'DOMStorage.domStorageItemsCleared', + common.mustCall(({ params }) => { + assert.deepStrictEqual(params.storageId, { + securityOrigin: '', + isLocalStorage, + storageKey: storageKey.storageKey, + }); + }), + ); + webStorage.clear(); + session.disconnect(); + } +} + +async function test() { + await testRegisterStorage(); + await testGetData(); + await testEvents(); +} test().then(common.mustCall());